001package ai.lilystyle.analytics_android; 002 003import android.content.Context; 004import android.content.SharedPreferences; 005import android.os.Handler; 006import android.os.Looper; 007import android.util.Log; 008 009import org.jetbrains.annotations.NotNull; 010import org.json.JSONObject; 011 012import java.util.HashMap; 013import java.util.Locale; 014import java.util.Map; 015import java.util.concurrent.ConcurrentHashMap; 016import java.util.concurrent.ExecutorService; 017import java.util.concurrent.LinkedBlockingDeque; 018import java.util.concurrent.ThreadPoolExecutor; 019import java.util.concurrent.TimeUnit; 020 021import okhttp3.OkHttpClient; 022 023 024/** 025 * Core class for integrating Lily.Ai analytics. 026 * 027 * <p>First obtain api-key and token to access Lily.Ai analytics. 028 * <p>Place the obtained api-key and token in meta-data tags in your AndroidManifest.xml inside application tag: 029 * <pre> 030 * {@code 031 * <application ...> 032 * <meta-data android:name="ai.lily.analytics.api_token" android:value="PLACE_YOUR_API_TOKEN_HERE" /> 033 * <meta-data android:name="ai.lily.analytics.api_key" android:value="PLACE_YOUR_API_KEY_HERE" /> 034 * <meta-data android:name="ai.lily.analytics.endpoint" android:value="PLACE_YOUR_ENDPOINT_HERE" /> 035 * </application> 036 * } 037 * </pre> 038 * 039 * <p>Now you can call {@link #getInstance(Context)} to obtain LilyAi instance 040 * 041 * <p>You might want to keep your api-key and token more secure. In this case obfuscate those as you please and 042 * to obtain LilyAi instance use {@link #getInstance(Context, String)} with your token or {@link #getInstance(Context, String, String, String)} with both. 043 * 044 * <p>When initiated LilyAi tries to restore a unique id (UUID or lpid) of the user from it's shared preferences or 045 * generates a new one, if not present, and stores it. You can override this by calling {@link #setUUID(String)} with your own id for the user. 046 * Also you can use {@link #getUUID()} to check what UUID is currently used by LilyAi or call {@link #resetUUID()} to 047 * drop the stored UUID value and generate a new random UUID. 048 * 049 * <p>Once you obtained an instance of LilyAi and done with setting up the UUID you can call either of {@link #track(JSONObject)} or 050 * {@link #track(JSONObject, LilyAiListener)} methods to send your data. 051 */ 052public final class LilyAi { 053 054 private final static Map<String, LilyAi> instancesMap = new HashMap<>(); 055 056 private final SharedPreferences prefs; 057 private final String baseUrl; 058 private final Map<String, String> headers = new ConcurrentHashMap<>(); 059 private final TrackingDataPersistentStorage trackingDataStorage; 060 private long lastTrackId; 061 private final String token; 062 private final String apiKey; 063 private final ExecutorService executorService; 064 private String uuid; 065 private String sessionId; 066 private Long sessionStartTime; 067 private Long sessionLastEventOccurred; 068 private long sessionDuration; 069 private final OkHttpClient okHttpClient = RequestsHelper.getOkHttpClient(); 070 private final Handler handler; 071 072 073 private LilyAi(Context context, String baseUrl, String token, String apiKey) { 074 this.baseUrl = baseUrl; 075 this.token = token; 076 this.apiKey = apiKey; 077 prefs = context.getApplicationContext().getSharedPreferences(Constants.PREFS_FILE_NAME+(apiKey+token+baseUrl).hashCode(), Context.MODE_PRIVATE); 078 executorService = new ThreadPoolExecutor(Constants.THREAD_POOL_SIZE, Constants.THREAD_POOL_SIZE, 079 1, TimeUnit.MINUTES, new LinkedBlockingDeque<Runnable>(), new WorkerThreadFactory()); 080 uuid = Utils.getUUID(prefs, true); 081 sessionId = prefs.getString(Constants.SESSION_ID_PREF_NAME, null); 082 sessionStartTime = prefs.getLong(Constants.SESSION_START_PREF_NAME, 0); 083 sessionLastEventOccurred = prefs.getLong(Constants.SESSION_LAST_EVENT_PREF_NAME, 0); 084 sessionDuration = prefs.getLong(Constants.SESSION_DURATION, Constants.DEFAULT_SESSION_DURATION); 085 lastTrackId = prefs.getLong(Constants.LAST_TRACKED_ID, 0); 086 trackingDataStorage = TrackingDataPersistentStorage.getInstance(context.getApplicationContext(), 087 String.format(Locale.getDefault(), Constants.TRACKED_DATA_STORAGE_DIR, baseUrl.hashCode()), 088 new TrackingDataPersistentStorageListener() { 089 @Override 090 public void withUndeliveredData(TrackingData trackingData) { 091 synchronized (trackingDataStorage) { 092 if (!trackingDataStorage.isSending(trackingData)) { 093 trackingDataStorage.put(trackingData, true); 094 executorService.submit(new Worker(LilyAi.this.baseUrl, trackingData, true, okHttpClient, workerListener, null)); 095 } 096 } 097 } 098 }); 099 handler = new Handler(Looper.getMainLooper()); 100 } 101 102 /** 103 * Get instance of LilyAI. 104 * 105 * <p>To use this method you should provide api-key and token in meta-data tags in your AndroidManifest.xml inside application tag: 106 * <pre> 107 * {@code 108 * <application ...> 109 * <meta-data android:name="ai.lily.analytics.api_token" android:value="PLACE_YOUR_API_TOKEN_HERE" /> 110 * <meta-data android:name="ai.lily.analytics.api_key" android:value="PLACE_YOUR_API_KEY_HERE" /> 111 * <meta-data android:name="ai.lily.analytics.endpoint" android:value="PLACE_YOUR_ENDPOINT_HERE" /> 112 * </application> 113 * } 114 * </pre> 115 * 116 * @param context The application context. 117 * @return an instance of LilyAI associated with provided token and api-key. 118 */ 119 public static LilyAi getInstance(Context context) { 120 return getInstance(context, null, null, null); 121 } 122 123 /** 124 * Get instance of LilyAI. 125 * 126 * <p>To use this method you should provide api-key in meta-data tag in your AndroidManifest.xml inside application tag: 127 * <pre> 128 * {@code 129 * <application ...> 130 * <meta-data android:name="ai.lily.analytics.api_key" android:value="PLACE_YOUR_API_KEY_HERE" /> 131 * <meta-data android:name="ai.lily.analytics.endpoint" android:value="PLACE_YOUR_ENDPOINT_HERE" /> 132 * </application> 133 * } 134 * </pre> 135 * 136 * @param context The application context. 137 * @param token Your api token string. 138 * @return an instance of LilyAI associated with provided token and api-key. 139 */ 140 public static LilyAi getInstance(Context context, String token) { 141 return getInstance(context, null, token, null); 142 } 143 144 /** 145 * Get instance of LilyAI. 146 * 147 * @param context The application context. 148 * @param token Your api token string. 149 * @param apiKey Your api-key string. 150 * @return an instance of LilyAI associated with provided token and api-key. 151 */ 152 public static LilyAi getInstance(Context context, String baseUrl, String token, String apiKey) { 153 if (context == null) { 154 Log.e(Constants.LOG_TAG, "Context can't be null."); 155 return null; 156 } 157 158 if (baseUrl == null) { 159 baseUrl = Utils.getMetadataString(context, Constants.META_BASE_URL_KEY); 160 if (baseUrl == null) { 161 Log.e(Constants.LOG_TAG, "Application meta-data " + Constants.META_BASE_URL_KEY + 162 " is not set. Set it or provide it with getInstance() method."); 163 return null; 164 } 165 } 166 167 if (token == null) { 168 token = Utils.getMetadataString(context, Constants.META_API_TOKEN_KEY); 169 if (token == null) { 170 Log.e(Constants.LOG_TAG, "Application meta-data " + Constants.META_API_TOKEN_KEY + 171 " is not set. Set it or provide it with getInstance() method."); 172 return null; 173 } 174 } 175 176 if (apiKey == null) { 177 apiKey = Utils.getMetadataString(context, Constants.META_API_KEY_KEY); 178 if (apiKey == null) { 179 Log.e(Constants.LOG_TAG, "Application meta-data " + Constants.META_API_KEY_KEY + 180 " is not set. Set it or provide it with getInstance() method."); 181 return null; 182 } 183 } 184 185 synchronized (instancesMap) { 186 LilyAi instance = instancesMap.get(apiKey+token+baseUrl); 187 if (instance == null) { 188 instance = new LilyAi(context, baseUrl, token, apiKey); 189 instancesMap.put(apiKey+token+baseUrl, instance); 190 } 191 return instance; 192 } 193 } 194 195 /** 196 * Reset the stored UUID to a new random value. 197 */ 198 public void resetUUID() { 199 uuid = Utils.getUUID(prefs, false); 200 sessionId = null; 201 prefs.edit().putString(Constants.SESSION_ID_PREF_NAME, null).apply(); 202 } 203 204 /** 205 * Set a new custom UUID value. 206 * 207 * @param uuid New UUID value to store and use. Can't be null or empty string. 208 */ 209 public void setUUID(@NotNull String uuid) { 210 if (uuid.isEmpty()) { 211 Log.e(Constants.LOG_TAG, "LilyAI UUID can't be null or empty string! Ignoring setUUID() call with argument "+ uuid); 212 return; 213 } 214 if (!uuid.equals(this.uuid)) { 215 sessionId = null; 216 prefs.edit().putString(Constants.SESSION_ID_PREF_NAME, null).apply(); 217 } 218 this.uuid = uuid; 219 Utils.updateUUID(prefs, uuid); 220 } 221 222 /** 223 * Get current UUID value stored by LilyAI. 224 * 225 * @return UUID string 226 */ 227 public String getUUID() { 228 return uuid; 229 } 230 231 /** 232 * Set session duration time. 233 * 234 * @param sessionDuration max session duration time (millis) 235 */ 236 public void setSessionDuration(long sessionDuration) { 237 this.sessionDuration = sessionDuration; 238 prefs.edit().putLong(Constants.SESSION_DURATION, sessionDuration).apply(); 239 } 240 241 /** 242 * Send your data to LilyAI. 243 * Data is sent asynchronously. 244 * Same as {@link #track(JSONObject, LilyAiListener)} with listener set to null. 245 * 246 * @param data Your JSON data. 247 */ 248 public void track(JSONObject data) { 249 track(data, null); 250 } 251 252 /** 253 * Send your data to LilyAI. 254 * Data is sent asynchronously. 255 * Methods of {@link LilyAiListener} will be called on the MainLooper. 256 * 257 * @param data Your JSON data. If data is null {@link LilyAiListener#onError(String)} will be called with message "JSON data is null". 258 * @param listener Callbacks for data send success or error. Can be null. 259 */ 260 public void track(JSONObject data, final LilyAiListener listener) { 261 if (data == null) { 262 if (listener != null) { 263 handler.post(new Runnable() { 264 @Override 265 public void run() { 266 listener.onError("JSON data is null"); 267 } 268 }); 269 } 270 return; 271 } 272 if (sessionLastEventOccurred != null && sessionLastEventOccurred > 0 && System.currentTimeMillis() - sessionLastEventOccurred > sessionDuration) { 273 sessionId = null; 274 } 275 if (sessionId == null) { 276 sessionId = getUUID() + "-" + System.currentTimeMillis(); 277 sessionStartTime = System.currentTimeMillis(); 278 prefs.edit().putString(Constants.SESSION_ID_PREF_NAME, sessionId) 279 .putLong(Constants.SESSION_START_PREF_NAME, sessionStartTime) 280 .apply(); 281 } 282 addHeader("lsid", sessionId); 283 addHeader("lsstart", (sessionStartTime != null && sessionStartTime > 0) ? sessionStartTime.toString() : null); 284 addHeader("lsend", String.valueOf(System.currentTimeMillis() + sessionDuration)); 285 sessionLastEventOccurred = System.currentTimeMillis(); 286 prefs.edit().putLong(Constants.SESSION_LAST_EVENT_PREF_NAME, sessionLastEventOccurred).apply(); 287 addHeader("x-api-key", apiKey); 288 addHeader("Api-Token", token); 289 addHeader("lpid", uuid); 290 synchronized (trackingDataStorage) { 291 TrackingData newData = new TrackingData(lastTrackId++, headers, data); 292 trackingDataStorage.put(newData, true); 293 executorService.submit(new Worker(baseUrl, newData, false, okHttpClient, workerListener, listener)); 294 trackingDataStorage.getNotDelivered(-1, Constants.THREAD_POOL_SIZE); 295 } 296 } 297 298 public void addHeader(String name, String value) { 299 if (name != null) { 300 if (value == null) { 301 headers.remove(name); 302 } else { 303 headers.put(name, value); 304 } 305 } 306 } 307 308 public void setUserID(String uid) { 309 addHeader("uid", uid); 310 } 311 312 public void setAnalyticsProviderID(String aid) { 313 addHeader("aid", aid); 314 } 315 316 public void setAnalyticsSessionID(String sid) { 317 addHeader("sid", sid); 318 } 319 320 public void setHashedUserEmail(String uem) { 321 addHeader("uem", uem); 322 } 323 324 public void setReferer(String src, String mdm, String pgpath, String pgcat, String pgtype) { 325 addHeader("src", src); 326 addHeader("mdm", mdm); 327 addHeader("pgpath", pgpath); 328 addHeader("pgcat", pgcat); 329 addHeader("pgtype", pgtype); 330 } 331 332 public void setExperimentId(String expid) { 333 addHeader("expid", expid); 334 } 335 336 public void setUserSourceIP(String sip) { 337 addHeader("sip", sip); 338 } 339 340 public void setVisitorId(String vid) { 341 addHeader("vid", vid); 342 } 343 344 private final WorkerListener workerListener = new WorkerListener() { 345 @Override 346 public void onSuccess(TrackingData data, boolean isFromRetry, final LilyAiListener lilyAiListener) { 347 synchronized (trackingDataStorage) { 348 trackingDataStorage.delivered(data); 349 if (isFromRetry) { 350 trackingDataStorage.getNotDelivered(data.id, 1); 351 } 352 } 353 if (lilyAiListener != null) { 354 handler.post(new Runnable() { 355 @Override 356 public void run() { 357 lilyAiListener.onSuccess(); 358 } 359 }); 360 } 361 } 362 363 @Override 364 public void onError(TrackingData data, boolean isFromRetry, int code, final String message, final LilyAiListener lilyAiListener) { 365 synchronized (trackingDataStorage) { 366 if (code <= 0 || (code >= 500 && code < 600)) { 367 trackingDataStorage.put(data, false); 368 } else { 369 trackingDataStorage.delivered(data); 370 if (isFromRetry) { 371 trackingDataStorage.getNotDelivered(data.id, 1); 372 } 373 } 374 } 375 376 if (lilyAiListener != null) { 377 handler.post(new Runnable() { 378 @Override 379 public void run() { 380 lilyAiListener.onError(message); 381 } 382 }); 383 } 384 } 385 }; 386}