diff --git a/android-sdk/build.gradle b/android-sdk/build.gradle index fb0ae30b..b34b55ef 100644 --- a/android-sdk/build.gradle +++ b/android-sdk/build.gradle @@ -64,6 +64,12 @@ dependencies { // Test testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:4.6.1' + testImplementation("androidx.test:core:1.5.0") + testImplementation("androidx.arch.core:core-testing:2.2.0") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") + testImplementation("com.google.truth:truth:1.1.4") + testImplementation("io.mockk:mockk:1.13.5") + testImplementation("org.json:json:20140107") } tasks.register('androidJavadoc', Javadoc) { diff --git a/android-sdk/src/androidTest/java/com/blueshift/ExampleInstrumentedTest.java b/android-sdk/src/androidTest/java/com/blueshift/ExampleInstrumentedTest.java deleted file mode 100644 index 1062d033..00000000 --- a/android-sdk/src/androidTest/java/com/blueshift/ExampleInstrumentedTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.blueshift; - -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Instrumentation test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() throws Exception { - // Context of the app under test. -// Context appContext = InstrumentationRegistry.getTargetContext(); - -// assertEquals("com.blueshift.test", appContext.getPackageName()); - } -} diff --git a/android-sdk/src/androidTest/java/com/blueshift/core/events/BlueshiftEventRepositoryImplTest.kt b/android-sdk/src/androidTest/java/com/blueshift/core/events/BlueshiftEventRepositoryImplTest.kt new file mode 100644 index 00000000..0219afcf --- /dev/null +++ b/android-sdk/src/androidTest/java/com/blueshift/core/events/BlueshiftEventRepositoryImplTest.kt @@ -0,0 +1,112 @@ +package com.blueshift.core.events + +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.runBlocking +import org.json.JSONObject +import org.junit.After +import org.junit.Before +import org.junit.Test + +class BlueshiftEventRepositoryImplTest { + private lateinit var repository: BlueshiftEventRepositoryImpl + + @Before + fun setUp() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + repository = BlueshiftEventRepositoryImpl(context) + } + + @After + fun tearDown() = runBlocking { + repository.clear() + } + + @Test + fun insertEvent_insertsEventsToTheSQLiteDatabase() = runBlocking { + val name = "test_event" + val json = "{\"key\":\"val\"}" + val timestamp = System.currentTimeMillis() + val event = BlueshiftEvent( + eventName = name, eventParams = JSONObject(json), timestamp = timestamp + ) + + repository.insertEvent(event) + val events = repository.readOneBatch() + + assert(events.size == 1) + assert(events[0].eventName == name) + assert(events[0].eventParams.toString() == json) + assert(events[0].timestamp == timestamp) + } + + @Test + fun deleteEvents_deletesEventsFromTheSQLiteDatabase() = runBlocking { + val name = "test_event" + val json = "{\"key\":\"val\"}" + val timestamp = 0L + val event = BlueshiftEvent( + eventName = name, eventParams = JSONObject(json), timestamp = timestamp + ) + + repository.insertEvent(event) + var events = repository.readOneBatch() + + assert(events.size == 1) + + repository.deleteEvents(events) + events = repository.readOneBatch() + + assert(events.isEmpty()) + } + + @Test + fun readOneBatch_retrievesAListOfHundredEventsWhenCountIsNotSpecified() = runBlocking { + for (i in 1..200) { + val name = "test_event_$i" + val json = "{\"key\":\"val\"}" + val timestamp = 0L + val event = BlueshiftEvent( + eventName = name, eventParams = JSONObject(json), timestamp = timestamp + ) + repository.insertEvent(event) + } + + val events = repository.readOneBatch() + assert(events.size == 100) + } + + @Test + fun readOneBatch_retrievesAListOfTenEventsWhenCountIsSetToTen() = runBlocking { + for (i in 1..200) { + val name = "test_event_$i" + val json = "{\"key\":\"val\"}" + val timestamp = 0L + val event = BlueshiftEvent( + eventName = name, eventParams = JSONObject(json), timestamp = timestamp + ) + repository.insertEvent(event) + } + + val events = repository.readOneBatch(batchCount = 10) + assert(events.size == 10) + } + + @Test + fun readOneBatch_retrievesAListOfEventsInTheSameOrderTheyAreStoredInTheDatabase() = + runBlocking { + for (i in 1..10) { + val name = "test_event_$i" + val json = "{\"key\":\"val\"}" + val timestamp = 0L + val event = BlueshiftEvent( + eventName = name, eventParams = JSONObject(json), timestamp = timestamp + ) + repository.insertEvent(event) + } + + val events = repository.readOneBatch(batchCount = 10) + for (i in 1..9) { + assert((events[i].id - events[i - 1].id) == 1L) + } + } +} diff --git a/android-sdk/src/androidTest/java/com/blueshift/core/network/BlueshiftNetworkRequestRepositoryImplTest.kt b/android-sdk/src/androidTest/java/com/blueshift/core/network/BlueshiftNetworkRequestRepositoryImplTest.kt new file mode 100644 index 00000000..a948e196 --- /dev/null +++ b/android-sdk/src/androidTest/java/com/blueshift/core/network/BlueshiftNetworkRequestRepositoryImplTest.kt @@ -0,0 +1,234 @@ +package com.blueshift.core.network + +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.runBlocking +import org.json.JSONObject +import org.junit.After +import org.junit.Before +import org.junit.Test + +class BlueshiftNetworkRequestRepositoryImplTest { + private lateinit var repository: BlueshiftNetworkRequestRepositoryImpl + + @Before + fun setUp() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + repository = BlueshiftNetworkRequestRepositoryImpl(context) + } + + @After + fun tearDown() = runBlocking { + repository.clear() + } + + @Test + fun insertRequest_insertsRequestsToTheSQLiteDatabase(): Unit = runBlocking { + val url = "https://example.com" + val method = BlueshiftNetworkRequest.Method.GET + val header = JSONObject(mapOf("Content-Type" to "application/json")) + val body = JSONObject() + val authRequired = true + val retryBalance = 1 + val retryTimestamp = 1234567890L + val timestamp = 1234567890L + + val request = BlueshiftNetworkRequest( + url = url, + method = method, + header = header, + body = body, + authorizationRequired = authRequired, + retryAttemptBalance = retryBalance, + retryAttemptTimestamp = retryTimestamp, + timestamp = timestamp + ) + + repository.insertRequest(request) + val request2 = repository.readNextRequest() + + assert(request2 != null) + request2?.let { + assert(it.url == url) + assert(it.method == method) + assert(it.header.toString() == header.toString()) + assert(it.body.toString() == body.toString()) + assert(it.authorizationRequired == authRequired) + assert(it.retryAttemptBalance == retryBalance) + assert(it.retryAttemptTimestamp == retryTimestamp) + assert(it.timestamp == timestamp) + } + } + + @Test + fun updateRequest_updatesRequestsInTheSQLiteDatabase(): Unit = runBlocking { + val url = "https://example.com" + val method = BlueshiftNetworkRequest.Method.GET + val header = JSONObject(mapOf("Content-Type" to "application/json")) + val body = JSONObject() + val authRequired = true + val retryBalance = 1 + val retryTimestamp = 1234567890L + val timestamp = 1234567890L + + val request = BlueshiftNetworkRequest( + url = url, + method = method, + header = header, + body = body, + authorizationRequired = authRequired, + retryAttemptBalance = retryBalance, + retryAttemptTimestamp = retryTimestamp, + timestamp = timestamp + ) + + repository.insertRequest(request) + + // the code only allows updating the following two fields. + val retryBalance2 = 2 + val retryTimestamp2 = 9876543210L + + val request2 = repository.readNextRequest() + request2?.let { + it.retryAttemptBalance = retryBalance2 + it.retryAttemptTimestamp = retryTimestamp2 + + repository.updateRequest(it) + } + + val request3 = repository.readNextRequest() + assert(request3 != null) + request3?.let { + assert(it.retryAttemptBalance == retryBalance2) + assert(it.retryAttemptTimestamp == retryTimestamp2) + } + } + + @Test + fun deleteRequest_deletesRequestsInTheSQLiteDatabase(): Unit = runBlocking { + val url = "https://example.com" + val method = BlueshiftNetworkRequest.Method.GET + val header = JSONObject(mapOf("Content-Type" to "application/json")) + val body = JSONObject() + val authRequired = true + val retryBalance = 1 + val retryTimestamp = 1234567890L + val timestamp = 1234567890L + + val request = BlueshiftNetworkRequest( + url = url, + method = method, + header = header, + body = body, + authorizationRequired = authRequired, + retryAttemptBalance = retryBalance, + retryAttemptTimestamp = retryTimestamp, + timestamp = timestamp + ) + + repository.insertRequest(request) + + val request2 = repository.readNextRequest() + request2?.let { + repository.deleteRequest(it) + } + + val request3 = repository.readNextRequest() + assert(request3 == null) + } + + @Test + fun readNextRequest_shouldReturnTheFirstRequestWhenAllRequestsInTheQueueRetryAttemptBalanceGreaterThanZero(): Unit = + runBlocking { + for (i in 1..3) { + val url = "https://api.com/$i" + val method = BlueshiftNetworkRequest.Method.GET + val header = JSONObject(mapOf("Content-Type" to "application/json")) + val body = JSONObject() + val authRequired = true + val retryBalance = 3 + val retryTimestamp = 0L + val timestamp = System.currentTimeMillis() + val request = BlueshiftNetworkRequest( + url = url, + method = method, + header = header, + body = body, + authorizationRequired = authRequired, + retryAttemptBalance = retryBalance, + retryAttemptTimestamp = retryTimestamp, + timestamp = timestamp + ) + repository.insertRequest(request) + } + + val request = repository.readNextRequest() + assert(request != null) + request?.let { + assert(it.url == "https://api.com/1") + } + } + + @Test + fun readNextRequest_shouldReturnNullWhenAllRequestsInTheQueueHasRetryAttemptBalanceEqualToZero(): Unit = + runBlocking { + for (i in 1..3) { + val url = "https://api.com/$i" + val method = BlueshiftNetworkRequest.Method.GET + val header = JSONObject(mapOf("Content-Type" to "application/json")) + val body = JSONObject() + val authRequired = true + val retryBalance = 0 + val retryTimestamp = 0L + val timestamp = System.currentTimeMillis() + val request = BlueshiftNetworkRequest( + url = url, + method = method, + header = header, + body = body, + authorizationRequired = authRequired, + retryAttemptBalance = retryBalance, + retryAttemptTimestamp = retryTimestamp, + timestamp = timestamp + ) + repository.insertRequest(request) + } + + val request = repository.readNextRequest() + assert(request == null) + } + + @Test + fun readNextRequest_shouldReturnTheRequestWithRetryAttemptTimestampLessThanCurrentTime(): Unit = + runBlocking { + for (i in 1..2) { + val fiveMinutes = 5 * 60 * 1000 + val url = "https://api.com/$i" + val method = BlueshiftNetworkRequest.Method.GET + val header = JSONObject(mapOf("Content-Type" to "application/json")) + val body = JSONObject() + val authRequired = true + val retryBalance = 1 + val retryTimestamp = if (i % 2 == 0) System.currentTimeMillis() + fiveMinutes else System.currentTimeMillis() - fiveMinutes + val timestamp = System.currentTimeMillis() + val request = BlueshiftNetworkRequest( + url = url, + method = method, + header = header, + body = body, + authorizationRequired = authRequired, + retryAttemptBalance = retryBalance, + retryAttemptTimestamp = retryTimestamp, + timestamp = timestamp + ) + repository.insertRequest(request) + } + + // i = 1 -> current time - 5min + // i = 2 -> current time + 5min + val request = repository.readNextRequest() + assert(request != null) + request?.let { + assert(it.url == "https://api.com/1") + } + } +} diff --git a/android-sdk/src/main/AndroidManifest.xml b/android-sdk/src/main/AndroidManifest.xml index 6b3173a5..02efbf35 100644 --- a/android-sdk/src/main/AndroidManifest.xml +++ b/android-sdk/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + @@ -57,27 +56,15 @@ - - - - - - - - - + - - \ No newline at end of file + diff --git a/android-sdk/src/main/java/com/blueshift/BlueShiftPreference.java b/android-sdk/src/main/java/com/blueshift/BlueShiftPreference.java index 004795fa..4b5b2f3c 100644 --- a/android-sdk/src/main/java/com/blueshift/BlueShiftPreference.java +++ b/android-sdk/src/main/java/com/blueshift/BlueShiftPreference.java @@ -2,7 +2,6 @@ import android.content.Context; import android.content.SharedPreferences; -import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.core.app.NotificationManagerCompat; @@ -13,8 +12,8 @@ * This class is responsible for tracking the preferences of sdk. * * @author Rahul Raveendran V P - * Created on 17/11/16 @ 1:07 PM - * https://github.com/rahulrvp + * Created on 17/11/16 @ 1:07 PM + * https://github.com/rahulrvp */ public class BlueShiftPreference { @@ -22,12 +21,48 @@ public class BlueShiftPreference { private static final String PREF_FILE = "com.blueshift.sdk_preferences"; private static final String PREF_KEY_APP_VERSION = "blueshift_app_version"; private static final String PREF_KEY_DEVICE_ID = "blueshift_device_id"; + private static final String PREF_KEY_DEVICE_TOKEN = "blueshift_device_token"; private static final String PREF_KEY_PUSH_ENABLED = "blueshift_push_enabled"; + private static final String PREF_KEY_LEGACY_SYNC_COMPLETE = "blueshift_legacy_sync_complete"; private static final String PREF_KEY_APP_OPEN_TRACKED_AT = "blueshift_app_open_tracked_at"; private static final String PREF_FILE_EMAIL = "BsftEmailPrefFile"; private static final String TAG = "BlueShiftPreference"; + static String getSavedDeviceToken(Context context) { + String token = null; + + SharedPreferences preferences = getBlueshiftPreferences(context); + if (preferences != null) { + token = preferences.getString(PREF_KEY_DEVICE_TOKEN, token); + } + + return token; + } + + static void saveDeviceToken(Context context, String token) { + SharedPreferences preferences = getBlueshiftPreferences(context); + if (preferences != null) { + preferences.edit().putString(PREF_KEY_DEVICE_TOKEN, token).apply(); + } + } + + static void markLegacyEventSyncAsComplete(Context context) { + SharedPreferences preferences = getBlueshiftPreferences(context); + if (preferences != null) { + preferences.edit().putBoolean(PREF_KEY_LEGACY_SYNC_COMPLETE, true).apply(); + } + } + + static boolean isLegacyEventSyncComplete(Context context) { + boolean isComplete = false; + SharedPreferences preferences = getBlueshiftPreferences(context); + if (preferences != null) { + isComplete = preferences.getBoolean(PREF_KEY_LEGACY_SYNC_COMPLETE, isComplete); + } + return isComplete; + } + static void saveAppVersionString(Context context, String appVersionString) { SharedPreferences preferences = getBlueshiftPreferences(context); if (preferences != null) { diff --git a/android-sdk/src/main/java/com/blueshift/Blueshift.java b/android-sdk/src/main/java/com/blueshift/Blueshift.java index 8318de38..1f3f8386 100644 --- a/android-sdk/src/main/java/com/blueshift/Blueshift.java +++ b/android-sdk/src/main/java/com/blueshift/Blueshift.java @@ -14,10 +14,18 @@ import androidx.core.content.ContextCompat; import com.blueshift.batch.BulkEventManager; -import com.blueshift.batch.Event; import com.blueshift.batch.EventsTable; import com.blueshift.batch.FailedEventsTable; -import com.blueshift.httpmanager.Method; +import com.blueshift.core.BlueshiftEventManager; +import com.blueshift.core.BlueshiftLambdaQueue; +import com.blueshift.core.BlueshiftNetworkRequestQueueManager; +import com.blueshift.core.app.BlueshiftInstallationStatus; +import com.blueshift.core.app.BlueshiftInstallationStatusHelper; +import com.blueshift.core.events.BlueshiftEventRepositoryImpl; +import com.blueshift.core.network.BlueshiftNetworkConfiguration; +import com.blueshift.core.network.BlueshiftNetworkRepositoryImpl; +import com.blueshift.core.network.BlueshiftNetworkRequestRepositoryImpl; +import com.blueshift.core.schedule.network.BlueshiftNetworkChangeScheduler; import com.blueshift.httpmanager.Request; import com.blueshift.inappmessage.InAppActionCallback; import com.blueshift.inappmessage.InAppApiCallback; @@ -38,7 +46,6 @@ import com.blueshift.util.CommonUtils; import com.blueshift.util.DeviceUtils; import com.blueshift.util.NetworkUtils; -import com.google.gson.Gson; import org.json.JSONException; import org.json.JSONObject; @@ -47,21 +54,19 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; -import java.util.List; -import java.util.Map; import java.util.Set; /** * @author Rahul Raveendran V P * Created on 17/2/15 @ 3:08 PM - * https://github.com/rahulrvp + * ... */ public class Blueshift { private static final String LOG_TAG = Blueshift.class.getSimpleName(); private static final String UTF8_SPACE = "%20"; private Context mContext; - private static Configuration mConfiguration; + private Configuration mConfiguration; private static Blueshift instance = null; private static BlueshiftPushListener blueshiftPushListener; private static BlueshiftInAppListener blueshiftInAppListener; @@ -162,6 +167,9 @@ public static void setTrackingEnabled(Context context, boolean isEnabled, boolea EventsTable.getInstance(context).deleteAllAsync(); // Failed events table. These will also get batched periodically. FailedEventsTable.getInstance(context).deleteAllAsync(); + + // Delete the data from events and request queue + BlueshiftEventManager.INSTANCE.clearAsync(); } } @@ -354,122 +362,117 @@ public void getLiveContentByCustomerId(@NonNull String slot, HashMap { + trackEvent(BlueshiftConstants.EVENT_APP_INSTALL, helper.getEventAttributes(status, previousAppVersion), false); + BlueShiftPreference.saveAppVersionString(mContext, appVersion); + } + case APP_UPDATE -> { + trackEvent(BlueshiftConstants.EVENT_APP_UPDATE, helper.getEventAttributes(status, previousAppVersion), false); + BlueShiftPreference.saveAppVersionString(mContext, appVersion); + } } - // pull latest font from server + + handleAppOpenEvent(mContext); + InAppMessageIconFont.getInstance(mContext).updateFont(mContext); + InAppManager.fetchInAppFromServer(mContext, null); - // fetch from API - if (mConfiguration != null && !mConfiguration.isInAppManualTriggerEnabled()) { - InAppManager.fetchInAppFromServer(mContext, null); - } + doAutomaticIdentifyChecks(mContext); } - /** - * This method checks for app installs and app updates by looking at the app version changes. - * When a change is detected, an event will be sent to Blueshift to report the same. - */ - void doAppVersionChecks(Context context) { - List result = inferAppVersionChangeEvent(context); - if (result != null && result.size() == 2) { - String eventName = (String) result.get(0); - HashMap extras = (HashMap) result.get(1); - trackEvent(eventName, extras, false); + private void doAutomaticIdentifyChecks(Context context) { + if (BlueShiftPreference.didPushPermissionStatusChange(context)) { + BlueShiftPreference.saveCurrentPushPermissionStatus(context); + + BlueshiftLogger.d(LOG_TAG, "A change in push permission detected, sending an identify event."); + identifyUser(null, false); } } - List inferAppVersionChangeEvent(Context context) { - List result = new ArrayList<>(); - - if (context != null) { - final String appVersionString = CommonUtils.getAppVersion(context); - - if (appVersionString != null) { - String storedAppVersionString = BlueShiftPreference.getStoredAppVersionString(context); - if (storedAppVersionString == null) { - BlueshiftLogger.d(LOG_TAG, "appVersion: Stored value NOT available"); - - // When stored value for appVersion is absent. It could be because.. - // 1 - The app is freshly installed - // 2 - The app is updated, but the old version did not have blueshift SDK - // 3 - The app is updated, but the old version had blueshift SDK's old version. - // - // Case 1 & 2 will be treated as app_install. - // Case 3 will be treated as app_update. We do this by checking the availability - // of the database file created by the older version of blueshift SDK. If present, - // it is app update, else it is app install. - - File database = context.getDatabasePath("blueshift_db.sqlite3"); - if (database.exists()) { - // case 3 - BlueshiftLogger.d(LOG_TAG, "appVersion: db file found at " + database.getAbsolutePath()); - - HashMap extras = new HashMap<>(); - extras.put(BlueshiftConstants.KEY_APP_UPDATED_AT, CommonUtils.getCurrentUtcTimestamp()); - - result.add(BlueshiftConstants.EVENT_APP_UPDATE); - result.add(extras); - } else { - // cases 1 & 2 - BlueshiftLogger.d(LOG_TAG, "appVersion: db file NOT found at " + database.getAbsolutePath()); - - HashMap extras = new HashMap<>(); - extras.put(BlueshiftConstants.KEY_APP_INSTALLED_AT, CommonUtils.getCurrentUtcTimestamp()); - - result.add(BlueshiftConstants.EVENT_APP_INSTALL); - result.add(extras); - } + void doNetworkConfigurations(Configuration configuration) { + BlueshiftNetworkConfiguration.INSTANCE.configureBasicAuthentication(configuration.getApiKey(), ""); + BlueshiftNetworkConfiguration.INSTANCE.setDatacenter(configuration.getRegion()); + } - BlueShiftPreference.saveAppVersionString(context, appVersionString); - } else { - BlueshiftLogger.d(LOG_TAG, "appVersion: Stored value available"); + void handleAppOpenEvent(Context context) { + boolean isAutoAppOpenEnabled = BlueshiftUtils.isAutomaticAppOpenFiringEnabled(context); + boolean canSendAppOpenNow = BlueshiftUtils.canAutomaticAppOpenBeSentNow(context); + if (isAutoAppOpenEnabled && canSendAppOpenNow) { + trackAppOpen(false); + // mark the tracking time + long now = System.currentTimeMillis() / 1000; + BlueShiftPreference.setAppOpenTrackedAt(context, now); + } + } - // When a stored value for appVersion is found, we compare it with the existing - // app version value. If there is a change, we consider it as app_update. - // - // PS: Android will not let you downgrade the app version without installing the old - // version, so it will always be an app upgrade event. - if (!storedAppVersionString.equals(appVersionString)) { - BlueshiftLogger.d(LOG_TAG, "appVersion: Stored value and current value doesn't match (stored = " + storedAppVersionString + ", current = " + appVersionString + ")"); + void initializeLegacyEventSyncModule(Context context) { + // Do not schedule any jobs. We have the new events module to do that. - HashMap extras = new HashMap<>(); - extras.put(BlueshiftConstants.KEY_PREVIOUS_APP_VERSION, storedAppVersionString); - extras.put(BlueshiftConstants.KEY_APP_UPDATED_AT, CommonUtils.getCurrentUtcTimestamp()); + if (!BlueShiftPreference.isLegacyEventSyncComplete(context)) { + // Cleanup any cached events by sending them to Blueshift. + BlueshiftExecutor.getInstance().runOnNetworkThread(() -> { + try { + Request request = RequestQueueTable.getInstance(context).getFirstRecord(); + ArrayList> fEvents = FailedEventsTable.getInstance(context).getBulkEventParameters(1); + ArrayList> bEvents = EventsTable.getInstance(context).getBulkEventParameters(1); - result.add(BlueshiftConstants.EVENT_APP_UPDATE); - result.add(extras); + if (request != null || !fEvents.isEmpty() || !bEvents.isEmpty()) { + BlueshiftLogger.d(LOG_TAG, "Initiating legacy events sync... (request queue = " + (request != null ? "not empty" : "empty") + ", fEvents = " + fEvents.size() + ", bEvents = " + bEvents.size() + ")"); - BlueShiftPreference.saveAppVersionString(context, appVersionString); + // Move any pending bulk events in the db to request queue. + BulkEventManager.enqueueBulkEvents(context); + // Sync the http request queue. + RequestQueue.getInstance().sync(context); } else { - BlueshiftLogger.d(LOG_TAG, "appVersion: Stored value and current value matches (stored = " + storedAppVersionString + ", current = " + appVersionString + ")"); + BlueshiftLogger.d(LOG_TAG, "Legacy events sync is done!"); + // The request queue is empty and the event tables are also empty. + // This could mean that there is nothing left to sync. + BlueShiftPreference.markLegacyEventSyncAsComplete(context); } + } catch (Exception e) { + BlueshiftLogger.e(LOG_TAG, e); } - } + }); } + } + + void initializeEventSyncModule(Context context, Configuration configuration) { + BlueshiftNetworkChangeScheduler.INSTANCE.scheduleWithJobScheduler(context, configuration); + + try (BlueshiftNetworkRequestRepositoryImpl networkRequestRepository = new BlueshiftNetworkRequestRepositoryImpl(context)) { + try (BlueshiftEventRepositoryImpl eventRepository = new BlueshiftEventRepositoryImpl(context)) { + BlueshiftEventManager.INSTANCE.initialize(eventRepository, networkRequestRepository, BlueshiftLambdaQueue.INSTANCE); + } catch (Exception e) { + BlueshiftLogger.e(LOG_TAG, e); + } - return result; + BlueshiftNetworkRepositoryImpl networkRepository = new BlueshiftNetworkRepositoryImpl(); + BlueshiftNetworkRequestQueueManager.INSTANCE.initialize(networkRequestRepository, networkRepository); + } catch (Exception e) { + BlueshiftLogger.e(LOG_TAG, e); + } } /** @@ -478,12 +481,12 @@ List inferAppVersionChangeEvent(Context context) { * * @param context valid context object */ - private void initAppIcon(Context context) { + private void initAppIcon(Context context, @NonNull Configuration configuration) { try { - if (mConfiguration != null && mConfiguration.getAppIcon() == 0) { + if (configuration.getAppIcon() == 0) { if (context != null) { ApplicationInfo applicationInfo = context.getApplicationInfo(); - mConfiguration.setAppIcon(applicationInfo.icon); + configuration.setAppIcon(applicationInfo.icon); } } } catch (Exception e) { @@ -524,118 +527,6 @@ private boolean hasValidCredentials() { return true; } - /** - * Appending the optional user info to params - * - * @param params source hash map to append details - * @return params - updated params object - */ - private HashMap appendOptionalUserInfo(HashMap params) { - if (params != null) { - UserInfo userInfo = UserInfo.getInstance(mContext); - if (userInfo != null) { - params.put(BlueshiftConstants.KEY_FIRST_NAME, userInfo.getFirstname()); - params.put(BlueshiftConstants.KEY_LAST_NAME, userInfo.getLastname()); - params.put(BlueshiftConstants.KEY_GENDER, userInfo.getGender()); - if (userInfo.getJoinedAt() > 0) { - params.put(BlueshiftConstants.KEY_JOINED_AT, userInfo.getJoinedAt()); - } - if (userInfo.getDateOfBirth() != null) { - params.put(BlueshiftConstants.KEY_DATE_OF_BIRTH, userInfo.getDateOfBirth().getTime() / 1000); - } - params.put(BlueshiftConstants.KEY_FACEBOOK_ID, userInfo.getFacebookId()); - params.put(BlueshiftConstants.KEY_EDUCATION, userInfo.getEducation()); - - if (userInfo.isUnsubscribed()) { - // we don't need to send this key if it set to false - params.put(BlueshiftConstants.KEY_UNSUBSCRIBED_PUSH, true); - } - - if (userInfo.getDetails() != null) { - params.putAll(userInfo.getDetails()); - } - } - } - - return params; - } - - /** - * Private method that receives params and send to server using request queue. - * - * @param params hash-map filled with parameters required for api call - * @param canBatchThisEvent flag to indicate if this event can be sent in bulk event API - * @return true if everything works fine, else false - */ - boolean sendEvent(String eventName, HashMap params, boolean canBatchThisEvent) { - String apiKey = BlueshiftUtils.getApiKey(mContext); - if (apiKey == null || apiKey.isEmpty()) { - BlueshiftLogger.e(LOG_TAG, "Please set a valid API key in your configuration object before initialization."); - return false; - } else { - BlueshiftJSONObject eventParams = new BlueshiftJSONObject(); - - try { - eventParams.put(BlueshiftConstants.KEY_EVENT, eventName); - eventParams.put(BlueshiftConstants.KEY_TIMESTAMP, CommonUtils.getCurrentUtcTimestamp()); - } catch (JSONException e) { - BlueshiftLogger.e(LOG_TAG, e); - } - - BlueshiftAttributesApp appInfo = BlueshiftAttributesApp.getInstance().sync(mContext); - eventParams.putAll(appInfo); - - BlueshiftAttributesUser userInfo = BlueshiftAttributesUser.getInstance().sync(mContext); - eventParams.putAll(userInfo); - - if (params != null && params.size() > 0) { - eventParams.putAll(params); - } - - HashMap map = eventParams.toHasMap(); - - if (canBatchThisEvent) { - Event event = new Event(); - event.setEventParams(map); - - BlueshiftLogger.i(LOG_TAG, "Adding event to events table for batching."); - - EventsTable.getInstance(mContext).insert(event); - } else { - // Creating the request object. - Request request = new Request(); - request.setPendingRetryCount(RequestQueue.DEFAULT_RETRY_COUNT); - request.setUrl(BlueshiftConstants.EVENT_API_URL(mContext)); - request.setMethod(Method.POST); - request.setParamJson(new Gson().toJson(map)); - - BlueshiftLogger.i(LOG_TAG, "Adding real-time event to request queue."); - - // Adding the request to the queue. - RequestQueue.getInstance().add(mContext, request); - } - - return true; - } - } - - private String getUrlParams(final HashMap params) { - StringBuilder bodyBuilder = new StringBuilder(); - - if (params != null) { - Iterator> iterator = params.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry param = iterator.next(); - bodyBuilder.append(param.getKey()).append('=').append(param.getValue()); - if (iterator.hasNext()) { - bodyBuilder.append('&'); - } - } - } - - return bodyBuilder.toString(); - } - /** * Method to send generic events * @@ -646,19 +537,19 @@ private String getUrlParams(final HashMap params) { @SuppressWarnings("WeakerAccess") public void trackEvent(@NonNull final String eventName, final HashMap params, final boolean canBatchThisEvent) { if (Blueshift.isTrackingEnabled(mContext)) { - BlueshiftExecutor.getInstance().runOnDiskIOThread( - new Runnable() { - @Override - public void run() { - try { - boolean tracked = sendEvent(eventName, params, canBatchThisEvent); - BlueshiftLogger.d(LOG_TAG, "Event tracking { name: " + eventName + ", status: " + tracked + " }"); - } catch (Exception e) { - BlueshiftLogger.e(LOG_TAG, e); - } - } - } - ); + String apiKey = BlueshiftUtils.getApiKey(mContext); + if (apiKey == null || apiKey.isEmpty()) { + BlueshiftLogger.e(LOG_TAG, "Please set a valid API key in your configuration object before initialization."); + } else { + //noinspection ConstantValue + if (eventName != null) { + // Java allows passing null as eventName when calling trackEvent, but Kotlin + // will crash the app if eventName is null in the next line. + BlueshiftEventManager.INSTANCE.trackEventWithData(mContext, eventName, params, canBatchThisEvent); + } + + doAutomaticIdentifyChecks(mContext); + } } else { BlueshiftLogger.i(LOG_TAG, "Blueshift SDK's event tracking is disabled. Dropping event: " + eventName); } @@ -1284,25 +1175,7 @@ void trackUniversalLinks(Uri uri) { } } - String reqUrl = BlueshiftConstants.TRACK_API_URL(mContext) + "?" + builder.toString(); - - final Request request = new Request(); - request.setPendingRetryCount(RequestQueue.DEFAULT_RETRY_COUNT); - request.setUrl(reqUrl); - request.setMethod(Method.GET); - - BlueshiftLogger.d(LOG_TAG, reqUrl); - BlueshiftLogger.i(LOG_TAG, "Adding real-time event to request queue."); - - BlueshiftExecutor.getInstance().runOnDiskIOThread( - new Runnable() { - @Override - public void run() { - // Adding the request to the queue. - RequestQueue.getInstance().add(mContext, request); - } - } - ); + BlueshiftEventManager.INSTANCE.enqueueCampaignEvent(builder.toString()); } } } catch (Exception e) { @@ -1409,18 +1282,7 @@ private boolean sendNotificationEvent(String action, HashMap cam // replace whitespace with %20 to avoid URL damage. paramsUrl = paramsUrl.replace(" ", UTF8_SPACE); - String reqUrl = BlueshiftConstants.TRACK_API_URL(mContext) + "?" + paramsUrl; - - Request request = new Request(); - request.setPendingRetryCount(RequestQueue.DEFAULT_RETRY_COUNT); - request.setUrl(reqUrl); - request.setMethod(Method.GET); - - BlueshiftLogger.d(LOG_TAG, reqUrl); - BlueshiftLogger.i(LOG_TAG, "Adding real-time event to request queue."); - - // Adding the request to the queue. - RequestQueue.getInstance().add(mContext, request); + BlueshiftEventManager.INSTANCE.enqueueCampaignEvent(paramsUrl); return true; } else { diff --git a/android-sdk/src/main/java/com/blueshift/BlueshiftAppPreferences.java b/android-sdk/src/main/java/com/blueshift/BlueshiftAppPreferences.java index 8dd7811b..38c186f3 100644 --- a/android-sdk/src/main/java/com/blueshift/BlueshiftAppPreferences.java +++ b/android-sdk/src/main/java/com/blueshift/BlueshiftAppPreferences.java @@ -1,5 +1,6 @@ package com.blueshift; +import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; @@ -50,11 +51,12 @@ private static JSONObject getCachedInstance(Context context) { return null; } + @SuppressLint("ApplySharedPref") public void save(Context context) { if (context != null) { SharedPreferences preferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE); if (preferences != null) { - preferences.edit().putString(PREF_KEY, instance.toString()).apply(); + preferences.edit().putString(PREF_KEY, instance.toString()).commit(); } } } diff --git a/android-sdk/src/main/java/com/blueshift/BlueshiftAttributesApp.java b/android-sdk/src/main/java/com/blueshift/BlueshiftAttributesApp.java index 35aa195a..0b467144 100644 --- a/android-sdk/src/main/java/com/blueshift/BlueshiftAttributesApp.java +++ b/android-sdk/src/main/java/com/blueshift/BlueshiftAttributesApp.java @@ -3,8 +3,6 @@ import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; import android.location.Location; import android.location.LocationManager; import android.os.Build; @@ -102,7 +100,7 @@ public void init(Context context) { addInAppEnabledStatus(context); addPushEnabledStatus(context); addFirebaseInstanceId(context); - addFirebaseToken(context); + addDeviceToken(context); addDeviceId(context); addDeviceAdId(context); addDeviceLocation(context); @@ -373,30 +371,36 @@ private void setDeviceLocation(Location location) { } } - private void addFirebaseToken(Context context) { + private void addDeviceToken(Context context) { if (!BlueshiftUtils.isPushEnabled(context)) return; + String cachedToken = BlueShiftPreference.getSavedDeviceToken(context); + if (cachedToken != null) { + setFirebaseToken(cachedToken); + } + try { - addFirebaseToken(); + addFirebaseToken(context); } catch (Exception e) { // tickets#8919 reported an issue with fcm token fetch. this is the // fix for the same. we are manually calling initializeApp and trying // to get token again. FirebaseApp.initializeApp(context); try { - addFirebaseToken(); + addFirebaseToken(context); } catch (Exception e1) { BlueshiftLogger.e(TAG, e1); } } } - private void addFirebaseToken() { + private void addFirebaseToken(Context context) { try { FirebaseMessaging.getInstance().getToken() .addOnSuccessListener(new OnSuccessListener() { @Override public void onSuccess(String token) { + BlueShiftPreference.saveDeviceToken(context, token); setFirebaseToken(token); } }) @@ -480,8 +484,9 @@ private void setFirebaseInstanceId(String instanceId) { } } - public void updateFirebaseToken(String newToken) { + public void updateFirebaseToken(Context context, String newToken) { if (newToken != null) { + BlueShiftPreference.saveDeviceToken(context, newToken); setFirebaseToken(newToken); } } @@ -590,19 +595,21 @@ public BlueshiftAttributesApp sync(Context context) { BlueshiftLogger.e(TAG, e); } - try { - String adId = DeviceUtils.getAdvertisingId(context); - setDeviceAdId(adId); - } catch (Exception e) { - BlueshiftLogger.e(TAG, e); - } + BlueshiftExecutor.getInstance().runOnDiskIOThread(() -> { + try { + String adId = DeviceUtils.getAdvertisingId(context); + setDeviceAdId(adId); + } catch (Exception e) { + BlueshiftLogger.e(TAG, e); + } - try { - boolean isAdEnabled = DeviceUtils.isLimitAdTrackingEnabled(context); - setAdTrackingStatus(isAdEnabled); - } catch (Exception e) { - BlueshiftLogger.e(TAG, e); - } + try { + boolean isAdEnabled = DeviceUtils.isLimitAdTrackingEnabled(context); + setAdTrackingStatus(isAdEnabled); + } catch (Exception e) { + BlueshiftLogger.e(TAG, e); + } + }); return instance; } diff --git a/android-sdk/src/main/java/com/blueshift/BlueshiftLogger.java b/android-sdk/src/main/java/com/blueshift/BlueshiftLogger.java index cf0d39b5..6c1e643b 100644 --- a/android-sdk/src/main/java/com/blueshift/BlueshiftLogger.java +++ b/android-sdk/src/main/java/com/blueshift/BlueshiftLogger.java @@ -17,6 +17,9 @@ public class BlueshiftLogger { public static void setLogLevel(int logLevel) { sLogLevel = logLevel; + if (logLevel > 0) { + com.blueshift.core.common.BlueshiftLogger.INSTANCE.setEnabled(true); + } } private static String prepareMessage(String tag, String message) { diff --git a/android-sdk/src/main/java/com/blueshift/batch/AlarmReceiver.java b/android-sdk/src/main/java/com/blueshift/batch/AlarmReceiver.java index 91cf56e3..ffe34444 100644 --- a/android-sdk/src/main/java/com/blueshift/batch/AlarmReceiver.java +++ b/android-sdk/src/main/java/com/blueshift/batch/AlarmReceiver.java @@ -18,8 +18,13 @@ * @author Rahul Raveendran V P * Created on 25/8/16 @ 1:01 PM * https://github.com/rahulrvp + * + * @deprecated + * This class is deprecated and will be removed in a future release. The events module has been + * refactored to improve performance and reliability. This class is now used internally for legacy + * data migration and will not be supported going forward. */ - +@Deprecated public class AlarmReceiver extends BroadcastReceiver { private static final String LOG_TAG = "BatchAlarmReceiver"; diff --git a/android-sdk/src/main/java/com/blueshift/batch/BulkEvent.java b/android-sdk/src/main/java/com/blueshift/batch/BulkEvent.java index 7f1ae3a5..1099b440 100644 --- a/android-sdk/src/main/java/com/blueshift/batch/BulkEvent.java +++ b/android-sdk/src/main/java/com/blueshift/batch/BulkEvent.java @@ -7,7 +7,13 @@ * @author Rahul Raveendran V P * Created on 26/8/16 @ 3:04 PM * https://github.com/rahulrvp + * + * @deprecated + * This class is deprecated and will be removed in a future release. The events module has been + * refactored to improve performance and reliability. This class is now used internally for legacy + * data migration and will not be supported going forward. */ +@Deprecated public class BulkEvent { ArrayList> events; diff --git a/android-sdk/src/main/java/com/blueshift/batch/BulkEventJobService.java b/android-sdk/src/main/java/com/blueshift/batch/BulkEventJobService.java index b56f6bcf..f2c61978 100644 --- a/android-sdk/src/main/java/com/blueshift/batch/BulkEventJobService.java +++ b/android-sdk/src/main/java/com/blueshift/batch/BulkEventJobService.java @@ -9,6 +9,13 @@ import com.blueshift.BlueshiftExecutor; import com.blueshift.BlueshiftLogger; +/** + * @deprecated + * This class is deprecated and will be removed in a future release. The events module has been + * refactored to improve performance and reliability. This class is now used internally for legacy + * data migration and will not be supported going forward. + */ +@Deprecated @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public class BulkEventJobService extends JobService { private static final String TAG = "BulkEventJobService"; diff --git a/android-sdk/src/main/java/com/blueshift/batch/BulkEventManager.java b/android-sdk/src/main/java/com/blueshift/batch/BulkEventManager.java index 3503263b..63b15aea 100644 --- a/android-sdk/src/main/java/com/blueshift/batch/BulkEventManager.java +++ b/android-sdk/src/main/java/com/blueshift/batch/BulkEventManager.java @@ -31,7 +31,13 @@ * @author Rahul Raveendran V P * Created on 25/8/16 @ 3:05 PM * https://github.com/rahulrvp + * + * @deprecated + * This class is deprecated and will be removed in a future release. The events module has been + * refactored to improve performance and reliability. This class is now used internally for legacy + * data migration and will not be supported going forward. */ +@Deprecated public class BulkEventManager { private static final String LOG_TAG = BulkEventManager.class.getSimpleName(); diff --git a/android-sdk/src/main/java/com/blueshift/batch/Event.java b/android-sdk/src/main/java/com/blueshift/batch/Event.java index 493efb36..6e648c52 100644 --- a/android-sdk/src/main/java/com/blueshift/batch/Event.java +++ b/android-sdk/src/main/java/com/blueshift/batch/Event.java @@ -8,7 +8,13 @@ * @author Rahul Raveendran V P * Created on 24/8/16 @ 3:04 PM * https://github.com/rahulrvp + * + * @deprecated + * This class is deprecated and will be removed in a future release. The events module has been + * refactored to improve performance and reliability. This class is now used internally for legacy + * data migration and will not be supported going forward. */ +@Deprecated public class Event { private long mId; private HashMap mEventParams; diff --git a/android-sdk/src/main/java/com/blueshift/batch/EventsTable.java b/android-sdk/src/main/java/com/blueshift/batch/EventsTable.java index 91bbae2e..23d6dc49 100644 --- a/android-sdk/src/main/java/com/blueshift/batch/EventsTable.java +++ b/android-sdk/src/main/java/com/blueshift/batch/EventsTable.java @@ -19,7 +19,13 @@ * @author Rahul Raveendran V P * Created on 24/8/16 @ 3:07 PM * https://github.com/rahulrvp + * + * @deprecated + * This class is deprecated and will be removed in a future release. The events module has been + * refactored to improve performance and reliability. This class is now used internally for legacy + * data migration and will not be supported going forward. */ +@Deprecated public class EventsTable extends BaseSqliteTable { public static final String TABLE_NAME = "events"; diff --git a/android-sdk/src/main/java/com/blueshift/batch/FailedEventsTable.java b/android-sdk/src/main/java/com/blueshift/batch/FailedEventsTable.java index 5cf71e8f..053aa9bd 100644 --- a/android-sdk/src/main/java/com/blueshift/batch/FailedEventsTable.java +++ b/android-sdk/src/main/java/com/blueshift/batch/FailedEventsTable.java @@ -22,7 +22,13 @@ * These events will be taken to build the batch first. * The values from {@link EventsTable} will only be taken if this table has no entries, * or if this table contains less number of events than a max batch size. + * + * @deprecated + * This class is deprecated and will be removed in a future release. The events module has been + * refactored to improve performance and reliability. This class is now used internally for legacy + * data migration and will not be supported going forward. */ +@Deprecated public class FailedEventsTable extends BaseSqliteTable { public static final String TABLE_NAME = "failed_events"; diff --git a/android-sdk/src/main/java/com/blueshift/core/BlueshiftEventManager.kt b/android-sdk/src/main/java/com/blueshift/core/BlueshiftEventManager.kt new file mode 100644 index 00000000..f2c17e21 --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/BlueshiftEventManager.kt @@ -0,0 +1,158 @@ +package com.blueshift.core + +import android.content.Context +import com.blueshift.BlueshiftAttributesApp +import com.blueshift.BlueshiftAttributesUser +import com.blueshift.BlueshiftConstants +import com.blueshift.BlueshiftJSONObject +import com.blueshift.core.common.BlueshiftAPI +import com.blueshift.core.common.BlueshiftLogger +import com.blueshift.core.events.BlueshiftEvent +import com.blueshift.core.events.BlueshiftEventRepository +import com.blueshift.core.network.BlueshiftNetworkRequest +import com.blueshift.core.network.BlueshiftNetworkRequestRepository +import com.blueshift.util.CommonUtils +import com.blueshift.util.NetworkUtils +import org.json.JSONArray +import org.json.JSONObject + +object BlueshiftEventManager { + private const val TAG = "EventManager" + private lateinit var eventRepository: BlueshiftEventRepository + private lateinit var networkRequestRepository: BlueshiftNetworkRequestRepository + private lateinit var blueshiftLambdaQueue: BlueshiftLambdaQueue + + fun initialize( + eventRepository: BlueshiftEventRepository, + networkRequestRepository: BlueshiftNetworkRequestRepository, + blueshiftLambdaQueue: BlueshiftLambdaQueue + ) { + this.eventRepository = eventRepository + this.networkRequestRepository = networkRequestRepository + this.blueshiftLambdaQueue = blueshiftLambdaQueue + } + + /** + * This method acts as a bridge between the java version of the sdk and the kotlin version of + * this class. It takes in the legacy params we used to take in when calling the sendEvent + * method inside the Blueshift.java class and uses the new events module to send the events. + */ + fun trackEventWithData( + context: Context, eventName: String, data: HashMap?, isBatchEvent: Boolean + ) { + val eventParams = BlueshiftJSONObject() + eventParams.put(BlueshiftConstants.KEY_EVENT, eventName) + eventParams.put(BlueshiftConstants.KEY_TIMESTAMP, CommonUtils.getCurrentUtcTimestamp()) + + val appInfo = BlueshiftAttributesApp.getInstance().sync(context) + eventParams.putAll(appInfo) + + val userInfo = BlueshiftAttributesUser.getInstance().sync(context) + eventParams.putAll(userInfo) + + data?.forEach { eventParams.put(it.key, it.value) } + + val isConnected = NetworkUtils.isConnected(context) + val blueshiftEvent = BlueshiftEvent( + eventName = eventName, eventParams = eventParams, timestamp = System.currentTimeMillis() + ) + + // We should insert an event as batch event in two cases. + // 1. If the app asks us to make it a batch event + // 2. If the app didn't ask, but we had no internet connection at the time of tracking + enqueueEvent(blueshiftEvent, isBatchEvent || !isConnected) + } + + /** + * This method accepts and event and adds it into a queue for processing, once added to the db, + * the method will call the sync method to send the event to the server (if the event is real-time) + */ + fun enqueueEvent(event: BlueshiftEvent, isBatchEvent: Boolean) { + blueshiftLambdaQueue.push { + trackEvent(event, isBatchEvent) + // let's not call sync for bulk events. + // the sync will be called by the scheduler when it's time. + if (!isBatchEvent) BlueshiftNetworkRequestQueueManager.sync() + } + } + + suspend fun trackEvent(event: BlueshiftEvent, isBatchEvent: Boolean) { + if (isBatchEvent) { + BlueshiftLogger.d("$TAG: Inserting 1 batch event -> ${event.eventName}") + eventRepository.insertEvent(event) + } else { + val request = BlueshiftNetworkRequest( + url = BlueshiftAPI.eventURL(), + header = JSONObject(mapOf("Content-Type" to "application/json")), + authorizationRequired = true, + method = BlueshiftNetworkRequest.Method.POST, + body = event.eventParams, + ) + + BlueshiftLogger.d("$TAG: Inserting 1 real-time event -> ${event.eventName}") + networkRequestRepository.insertRequest(request) + } + } + + /** + * This method accepts a query string for a campaign event and adds it into a queue for processing, + * once added to the db, the method will call the sync method to send the event to the server. + */ + fun enqueueCampaignEvent(queryString: String) { + blueshiftLambdaQueue.push { + trackCampaignEvent(queryString) + BlueshiftNetworkRequestQueueManager.sync() + } + } + + suspend fun trackCampaignEvent(queryString: String) { + if (queryString.isNotEmpty()) { + val request = BlueshiftNetworkRequest( + url = BlueshiftAPI.trackURL(queryString), + method = BlueshiftNetworkRequest.Method.GET, + ) + networkRequestRepository.insertRequest(request) + } + } + + /** + * Deletes ALL entries from the batch events table as well as the network requests table. + */ + fun clearAsync() { + blueshiftLambdaQueue.push { clear() } + } + + suspend fun clear() { + eventRepository.clear() + networkRequestRepository.clear() + } + + suspend fun buildAndEnqueueBatchEvents() { + while (true) { + val events = eventRepository.readOneBatch() + // break the loop when there are no pending events available for making a batch + if (events.isEmpty()) break + + val eventsArray = JSONArray() + events.forEach { eventsArray.put(it.eventParams) } + + BlueshiftLogger.d("$TAG: Creating 1 bulk event with ${eventsArray.length()} event(s). Events = $eventsArray") + + val bulkEventPayload = JSONObject().apply { + put("events", eventsArray) + } + + val request = BlueshiftNetworkRequest( + url = BlueshiftAPI.bulkEventsURL(), + header = JSONObject(mapOf("Content-Type" to "application/json")), + authorizationRequired = true, + method = BlueshiftNetworkRequest.Method.POST, + body = bulkEventPayload, + ) + + networkRequestRepository.insertRequest(request) + + eventRepository.deleteEvents(events) + } + } +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/blueshift/core/BlueshiftLambdaQueue.kt b/android-sdk/src/main/java/com/blueshift/core/BlueshiftLambdaQueue.kt new file mode 100644 index 00000000..9b7571aa --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/BlueshiftLambdaQueue.kt @@ -0,0 +1,33 @@ +package com.blueshift.core + +import com.blueshift.core.common.BlueshiftLogger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +object BlueshiftLambdaQueue { + private val channel = Channel Unit>(Channel.UNLIMITED) + private val coroutineScope = CoroutineScope(Dispatchers.Default) + + init { + coroutineScope.launch { + for (block in channel) { + try { + block() + } catch (e: Exception) { + BlueshiftLogger.e(e.stackTraceToString()) + } + } + } + } + + fun push(block: suspend () -> Unit) { + channel.trySend { + runBlocking { + block() + } + }.isSuccess + } +} diff --git a/android-sdk/src/main/java/com/blueshift/core/BlueshiftNetworkRequestQueueManager.kt b/android-sdk/src/main/java/com/blueshift/core/BlueshiftNetworkRequestQueueManager.kt new file mode 100644 index 00000000..c121df7c --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/BlueshiftNetworkRequestQueueManager.kt @@ -0,0 +1,107 @@ +package com.blueshift.core + +import com.blueshift.core.common.BlueshiftLogger +import com.blueshift.core.network.BlueshiftNetworkConfiguration +import com.blueshift.core.network.BlueshiftNetworkRepository +import com.blueshift.core.network.BlueshiftNetworkRequest +import com.blueshift.core.network.BlueshiftNetworkRequestRepository +import java.net.HttpURLConnection.HTTP_OK +import java.util.concurrent.atomic.AtomicBoolean + +object BlueshiftNetworkRequestQueueManager { + private const val TAG = "NetworkRequestQueueManager" + private lateinit var networkRequestRepository: BlueshiftNetworkRequestRepository + private lateinit var networkRepository: BlueshiftNetworkRepository + private val isSyncing = AtomicBoolean(false) // to prevent concurrent access to the sync method + private val lock = Any() // to prevent concurrent access to the database + + fun initialize( + networkRequestRepository: BlueshiftNetworkRequestRepository, + networkRepository: BlueshiftNetworkRepository + ) { + synchronized(lock) { + this.networkRequestRepository = networkRequestRepository + this.networkRepository = networkRepository + } + } + + suspend fun insertNewRequest(request: BlueshiftNetworkRequest) { + networkRequestRepository.insertRequest(request) + } + + suspend fun updateRequest(request: BlueshiftNetworkRequest) { + networkRequestRepository.updateRequest(request) + } + + suspend fun deleteRequest(request: BlueshiftNetworkRequest) { + networkRequestRepository.deleteRequest(request) + } + + suspend fun readNextRequest(): BlueshiftNetworkRequest? { + return networkRequestRepository.readNextRequest() + } + + suspend fun sync() { + // Do not initiate the sync process if the authorization value is not available. + // + // Reason: If the authorization value is not set, it means that the SDK is not initialized + // properly. Without proper event api key in place, most of the events api calls would fail. + // So, we should not start the sync process until we get the authorization value set. + // + // Note: The campaign events doesn't require an event API key. However, syncing them without + // initializing the SDK would cause compliance issues. Hence we're blocking the sync completely + // until we get the authorization value set. + BlueshiftNetworkConfiguration.authorization?.let { basicAuth -> + // Prevent concurrent access to the sync method. + if (!isSyncing.compareAndSet(false, true)) { + BlueshiftLogger.d("$TAG: Sync is in-progress... Skipping the duplicate sync call.") + return + } + + try { + while (true) { + // break the look when networkRequest is null. + val networkRequest = readNextRequest() ?: break + BlueshiftLogger.d("$TAG: Dequeue -> (Request ID: ${networkRequest.id})") + + if (networkRequest.authorizationRequired) { + networkRequest.authorization = basicAuth + } + + val response = networkRepository.makeNetworkRequest( + networkRequest = networkRequest + ) + + if (response.responseCode == HTTP_OK) { + BlueshiftLogger.d("$TAG: Remove -> (Request ID: ${networkRequest.id})") + deleteRequest(networkRequest) + } else if (response.responseCode == 0) { + BlueshiftLogger.d("$TAG: No internet connection. Pause sync!") + break + } else { + networkRequest.retryAttemptBalance-- + + if (networkRequest.retryAttemptBalance > 0) { + val intervalMs = + BlueshiftNetworkConfiguration.requestRetryIntervalInMilliseconds + + networkRequest.retryAttemptTimestamp = + System.currentTimeMillis() + intervalMs + + // reset authorization to avoid storing it in db + networkRequest.authorization = null + + BlueshiftLogger.d("$TAG: Retry later -> (Request ID: ${networkRequest.id})") + updateRequest(networkRequest) + } else { + BlueshiftLogger.d("$TAG: Retry limit exceeded! Remove -> (Request ID: ${networkRequest.id})") + deleteRequest(networkRequest) + } + } + } + } finally { + isSyncing.set(false) + } + } + } +} diff --git a/android-sdk/src/main/java/com/blueshift/core/app/BlueshiftInstallationStatus.kt b/android-sdk/src/main/java/com/blueshift/core/app/BlueshiftInstallationStatus.kt new file mode 100644 index 00000000..514176c8 --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/app/BlueshiftInstallationStatus.kt @@ -0,0 +1,5 @@ +package com.blueshift.core.app + +enum class BlueshiftInstallationStatus { + APP_INSTALL, APP_UPDATE, NO_CHANGE +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/blueshift/core/app/BlueshiftInstallationStatusHelper.kt b/android-sdk/src/main/java/com/blueshift/core/app/BlueshiftInstallationStatusHelper.kt new file mode 100644 index 00000000..4b55bf0a --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/app/BlueshiftInstallationStatusHelper.kt @@ -0,0 +1,71 @@ +package com.blueshift.core.app + +import com.blueshift.BlueshiftConstants +import com.blueshift.core.common.BlueshiftLogger +import com.blueshift.util.CommonUtils +import java.io.File + +class BlueshiftInstallationStatusHelper { + + fun getInstallationStatus( + appVersion: String, previousAppVersion: String?, database: File + ): BlueshiftInstallationStatus { + var status = BlueshiftInstallationStatus.NO_CHANGE + + if (previousAppVersion == null) { + // When stored value for previousAppVersion is absent. It could be because.. + // 1 - The app is freshly installed + // 2 - The app is updated, but the old version did not have blueshift SDK + // 3 - The app is updated, but the old version had blueshift SDK's old version. + // + // Case 1 & 2 will be treated as app_install. + // Case 3 will be treated as app_update. We do this by checking the availability + // of the database file created by the older version of blueshift SDK. If present, + // it is app update, else it is app install. + + if (database.exists()) { + // case 3 + status = BlueshiftInstallationStatus.APP_UPDATE + BlueshiftLogger.d("App updated. Previous app had Blueshift SDK version older than v3.4.6.") + } else { + // case 1 OR 2 + status = BlueshiftInstallationStatus.APP_INSTALL + BlueshiftLogger.d("App installation detected.") + } + } else { + // When a stored value for previousAppVersion is found, we compare it with the existing + // app version value. If there is a change, we consider it as app_update. + // + // PS: Android will not let you downgrade the app version without installing the old + // version, so it will always be an app upgrade event. + if (appVersion != previousAppVersion) { + status = BlueshiftInstallationStatus.APP_UPDATE + BlueshiftLogger.d("App updated from $previousAppVersion to $appVersion.") + } + } + + return status + } + + fun getEventAttributes( + status: BlueshiftInstallationStatus, previousAppVersion: String? + ): HashMap { + return when (status) { + BlueshiftInstallationStatus.APP_INSTALL -> hashMapOf( + BlueshiftConstants.KEY_APP_INSTALLED_AT to CommonUtils.getCurrentUtcTimestamp() + ) + + BlueshiftInstallationStatus.APP_UPDATE -> { + val updatedMap: HashMap = hashMapOf( + BlueshiftConstants.KEY_APP_UPDATED_AT to CommonUtils.getCurrentUtcTimestamp() + ) + previousAppVersion?.let { + updatedMap[BlueshiftConstants.KEY_PREVIOUS_APP_VERSION] = it + } + updatedMap + } + + BlueshiftInstallationStatus.NO_CHANGE -> hashMapOf() + } + } +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/blueshift/core/common/BlueshiftAPI.kt b/android-sdk/src/main/java/com/blueshift/core/common/BlueshiftAPI.kt new file mode 100644 index 00000000..ff25e7ba --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/common/BlueshiftAPI.kt @@ -0,0 +1,27 @@ +package com.blueshift.core.common + +class BlueshiftAPI { + enum class Datacenter(val baseUrl: String) { + US("https://api.getblueshift.com/"), EU("https://api.eu.getblueshift.com/") + } + + companion object { + private var region = Datacenter.US + + fun setDatacenter(datacenter: Datacenter) { + region = datacenter + } + + fun trackURL(queryString: String): String { + return "${region.baseUrl}track?$queryString" + } + + fun eventURL(): String { + return "${region.baseUrl}api/v1/event" + } + + fun bulkEventsURL(): String { + return "${region.baseUrl}api/v1/bulkevents" + } + } +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/blueshift/core/common/BlueshiftLogger.kt b/android-sdk/src/main/java/com/blueshift/core/common/BlueshiftLogger.kt new file mode 100644 index 00000000..0e00c79e --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/common/BlueshiftLogger.kt @@ -0,0 +1,16 @@ +package com.blueshift.core.common + +import android.util.Log + +object BlueshiftLogger { + private const val TAG = "Blueshift" + var enabled = false + + fun d(message: String) { + if (enabled) Log.d(TAG, message) + } + + fun e(message: String) { + if (enabled) Log.e(TAG, message) + } +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/blueshift/core/database/BlueshiftSQLiteModel.kt b/android-sdk/src/main/java/com/blueshift/core/database/BlueshiftSQLiteModel.kt new file mode 100644 index 00000000..34dd9dd8 --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/database/BlueshiftSQLiteModel.kt @@ -0,0 +1,5 @@ +package com.blueshift.core.database + +abstract class BlueshiftSQLiteModel { + abstract val id: Long +} diff --git a/android-sdk/src/main/java/com/blueshift/core/database/BlueshiftSQLiteOpenHelper.kt b/android-sdk/src/main/java/com/blueshift/core/database/BlueshiftSQLiteOpenHelper.kt new file mode 100644 index 00000000..63eacbae --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/database/BlueshiftSQLiteOpenHelper.kt @@ -0,0 +1,158 @@ +package com.blueshift.core.database + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase.CursorFactory +import android.database.sqlite.SQLiteOpenHelper +import android.text.TextUtils +import com.blueshift.core.common.BlueshiftLogger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +abstract class BlueshiftSQLiteOpenHelper( + context: Context?, name: String?, factory: CursorFactory?, version: Int +) : SQLiteOpenHelper(context, name, factory, version) { + protected fun buildCreateTableQuery(): String? { + var query: String? = null + val tableName = tableName + if (!TextUtils.isEmpty(tableName)) { + val fieldTypeMap = fields.toMutableMap() + if (fieldTypeMap.isNotEmpty()) { + fieldTypeMap[ID] = FieldType.Autoincrement + val createTableQuery = StringBuilder("CREATE TABLE $tableName(") + val fieldNames: Set = fieldTypeMap.keys + var isFirstIteration = true + for (fieldName in fieldNames) { + if (isFirstIteration) { + isFirstIteration = false + } else { + createTableQuery.append(",") + } + val dataType = fieldTypeMap[fieldName].toString() + createTableQuery.append(fieldName).append(" ").append(dataType) + } + query = "$createTableQuery)" + } + } + return query + } + + protected fun getId(cursor: Cursor): Long { + return getLong(cursor, ID) + } + + protected fun getLong(cursor: Cursor, fieldName: String?): Long { + val index = cursor.getColumnIndex(fieldName) + return if (index >= 0) cursor.getLong(index) else 0 + } + + protected fun getString(cursor: Cursor, fieldName: String?): String? { + val index = cursor.getColumnIndex(fieldName) + return if (index >= 0) cursor.getString(index) else null + } + + suspend fun insert(t: T): Boolean { + return withContext(Dispatchers.IO) { + synchronized(this) { + val id: Long + val db = writableDatabase + id = db.insert(tableName, null, getContentValues(t)) + + id != ID_DEFAULT + } + } + } + + suspend fun update(t: T?) { + withContext(Dispatchers.IO) { + synchronized(this) { + val db = writableDatabase + if (db != null) { + if (t != null && t.id > 0) { + val count = db.update(tableName, getContentValues(t), "$ID=?", arrayOf("${t.id}")) + BlueshiftLogger.d("$TAG: Successfully updated $count record(s) in $tableName where id IN (${t.id})") + } + } + } + } + } + + suspend fun delete(t: T) { + withContext(Dispatchers.IO) { + synchronized(this) { + val db = writableDatabase + if (db != null) { + val id = t?.id + val count = db.delete(tableName, "$ID=?", arrayOf("$id")) + BlueshiftLogger.d("$TAG: Successfully deleted $count record(s) from $tableName where id IN ($id)") + } + } + } + } + + suspend fun deleteAll(whereClause: String?, selectionArgs: Array?) { + withContext(Dispatchers.IO) { + synchronized(this) { + val db = writableDatabase + if (db != null) { + val count = db.delete(tableName, whereClause, selectionArgs) + + val csv = selectionArgs?.joinToString { it.toString() } + BlueshiftLogger.d("$TAG: Successfully deleted $count record(s) from $tableName where id IN ($csv)") + } + } + } + } + + suspend fun findAll(): List { + return withContext(Dispatchers.IO) { + val records: MutableList = ArrayList() + synchronized(this) { + val db = readableDatabase + if (db != null) { + val cursor = db.query(tableName, null, null, null, null, null, null) + if (cursor != null) { + cursor.moveToFirst() + while (!cursor.isAfterLast) { + records.add(getObject(cursor)) + cursor.moveToNext() + } + cursor.close() + } + } + } + records + } + } + + protected abstract fun getObject(cursor: Cursor): T + protected abstract fun getContentValues(obj: T): ContentValues + protected abstract val tableName: String + protected abstract val fields: Map + + protected enum class FieldType { + String, Blob, Text, UniqueText, Autoincrement, Integer; + + override fun toString(): kotlin.String { + return when (this) { + String -> "STRING" + Blob -> "BLOB" + Text -> "TEXT" + UniqueText -> "TEXT NOT NULL UNIQUE" + Integer -> "INTEGER DEFAULT 0" + Autoincrement -> "INTEGER PRIMARY KEY AUTOINCREMENT" + } + } + } + + companion object { + const val TAG = "SQLiteOpenHelper" + const val ID_DEFAULT = -1L + const val ID = "_id" + const val _AND_ = " AND " + const val _AND = " AND" + const val _OR_ = " OR " + const val _OR = " OR" + } +} diff --git a/android-sdk/src/main/java/com/blueshift/core/events/BlueshiftEvent.kt b/android-sdk/src/main/java/com/blueshift/core/events/BlueshiftEvent.kt new file mode 100644 index 00000000..131d0232 --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/events/BlueshiftEvent.kt @@ -0,0 +1,11 @@ +package com.blueshift.core.events + +import com.blueshift.core.database.BlueshiftSQLiteModel +import org.json.JSONObject + +data class BlueshiftEvent( + override val id: Long = -1, + val eventName: String, + val eventParams: JSONObject, + val timestamp: Long, +) : BlueshiftSQLiteModel() diff --git a/android-sdk/src/main/java/com/blueshift/core/events/BlueshiftEventRepository.kt b/android-sdk/src/main/java/com/blueshift/core/events/BlueshiftEventRepository.kt new file mode 100644 index 00000000..462779ca --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/events/BlueshiftEventRepository.kt @@ -0,0 +1,8 @@ +package com.blueshift.core.events + +interface BlueshiftEventRepository { + suspend fun insertEvent(event: BlueshiftEvent) + suspend fun deleteEvents(events: List) + suspend fun readOneBatch(batchCount: Int = 100) : List + suspend fun clear() +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/blueshift/core/events/BlueshiftEventRepositoryImpl.kt b/android-sdk/src/main/java/com/blueshift/core/events/BlueshiftEventRepositoryImpl.kt new file mode 100644 index 00000000..ca41c224 --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/events/BlueshiftEventRepositoryImpl.kt @@ -0,0 +1,103 @@ +package com.blueshift.core.events + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import com.blueshift.core.database.BlueshiftSQLiteOpenHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject + +class BlueshiftEventRepositoryImpl( + context: Context? +) : BlueshiftSQLiteOpenHelper( + context, "com.blueshift.events.db", null, 1 +), BlueshiftEventRepository { + override fun onCreate(db: SQLiteDatabase?) { + db?.execSQL(buildCreateTableQuery()) + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + } + + override fun getContentValues(obj: BlueshiftEvent): ContentValues { + val contentValues = ContentValues() + if (obj.id != ID_DEFAULT) contentValues.put(ID, obj.id) + contentValues.put(NAME, obj.eventName) + contentValues.put(PARAMS, obj.eventParams.toString().toByteArray(charset = Charsets.UTF_8)) + contentValues.put(TIMESTAMP, obj.timestamp) + return contentValues + } + + override fun getObject(cursor: Cursor): BlueshiftEvent { + val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)) + val name = cursor.getString(cursor.getColumnIndexOrThrow(NAME)) ?: "" + var paramsJson = JSONObject() + val paramsByes = cursor.getBlob(cursor.getColumnIndexOrThrow(PARAMS)) + paramsByes?.let { + val params = String(it, charset = Charsets.UTF_8) + paramsJson = JSONObject(params) + } + val timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)) + + return BlueshiftEvent(id, name, paramsJson, timestamp) + } + + override val tableName: String = "events_table" + override val fields: Map = mapOf( + NAME to FieldType.Text, PARAMS to FieldType.Blob, TIMESTAMP to FieldType.Integer + ) + + companion object { + private const val NAME = "name" + private const val PARAMS = "params" + private const val TIMESTAMP = "timestamp" + } + + override suspend fun insertEvent(event: BlueshiftEvent) { + insert(event) + } + + override suspend fun deleteEvents(events: List) { + if (events.isEmpty()) return + + val placeholder = StringBuilder() + val ids = mutableListOf() + + for (event in events) { + ids.add("${event.id}") + placeholder.append("?, ") + } + + // delete the tailing coma and space + placeholder.delete(placeholder.length - 2, placeholder.length) + + deleteAll(whereClause = "$ID IN ($placeholder)", selectionArgs = ids.toTypedArray()) + } + + override suspend fun readOneBatch(batchCount: Int): List { + return withContext(Dispatchers.IO) { + synchronized(this) { + val events = mutableListOf() + val cursor = readableDatabase.query( + tableName, null, null, null, null, null, "$TIMESTAMP ASC", "$batchCount" + ) + + while (cursor.moveToNext()) { + events.add(getObject(cursor)) + } + + cursor.close() + + events + } + } + } + + override suspend fun clear() { + withContext(Dispatchers.IO) { + deleteAll(whereClause = null, selectionArgs = null) + } + } +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkConfiguration.kt b/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkConfiguration.kt new file mode 100644 index 00000000..6e8ce866 --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkConfiguration.kt @@ -0,0 +1,27 @@ +package com.blueshift.core.network + +import android.util.Base64 +import com.blueshift.BlueshiftRegion +import com.blueshift.core.common.BlueshiftAPI + + +object BlueshiftNetworkConfiguration { + var authorization: String? = null + var requestRetryIntervalInMilliseconds: Long = 5 * (60 * 1000) // 5 Minutes + var isConnected = true + + fun setDatacenter(region: BlueshiftRegion) { + val datacenter = when (region) { + BlueshiftRegion.US -> BlueshiftAPI.Datacenter.US + BlueshiftRegion.EU -> BlueshiftAPI.Datacenter.EU + } + + BlueshiftAPI.setDatacenter(datacenter) + } + + fun configureBasicAuthentication(username: String, password: String) { + val authToken = "$username:$password" + val base64Token = Base64.encodeToString(authToken.toByteArray(), Base64.NO_WRAP) + authorization = "Basic ".plus(base64Token) + } +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkRepository.kt b/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkRepository.kt new file mode 100644 index 00000000..2cafd26f --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkRepository.kt @@ -0,0 +1,5 @@ +package com.blueshift.core.network + +interface BlueshiftNetworkRepository { + suspend fun makeNetworkRequest(networkRequest: BlueshiftNetworkRequest) : BlueshiftNetworkResponse +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkRepositoryImpl.kt b/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkRepositoryImpl.kt new file mode 100644 index 00000000..ee3c4c49 --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkRepositoryImpl.kt @@ -0,0 +1,109 @@ +package com.blueshift.core.network + +import com.blueshift.core.common.BlueshiftLogger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.IOException +import java.net.URL +import javax.net.ssl.HttpsURLConnection + +class BlueshiftNetworkRepositoryImpl : BlueshiftNetworkRepository { + + override suspend fun makeNetworkRequest(networkRequest: BlueshiftNetworkRequest): BlueshiftNetworkResponse { + return withContext(Dispatchers.IO) { + var response: BlueshiftNetworkResponse + var connection: HttpsURLConnection? = null + + try { + val url = URL(networkRequest.url) + connection = url.openConnection() as HttpsURLConnection + + BlueshiftLogger.d("$TAG: $networkRequest") + + if (networkRequest.authorizationRequired) { + val authorization = networkRequest.authorization + authorization?.let { connection.setRequestProperty("Authorization", it) } + } + + networkRequest.header?.let { headers -> + headers.keys().forEach { key -> + connection.setRequestProperty(key, headers.optString(key)) + } + } + + when (networkRequest.method) { + BlueshiftNetworkRequest.Method.GET -> prepareGetRequest(connection) + BlueshiftNetworkRequest.Method.POST -> preparePostRequest( + connection, networkRequest + ) + } + + connection.connect() + response = readResponseFromHttpsConnection(connection) + } catch (e: Exception) { + response = when (e) { + is IOException -> { + BlueshiftNetworkResponse(responseCode = 0, responseBody = "IOException") + } + + else -> { + BlueshiftNetworkResponse(responseCode = -1, responseBody = "${e.message}") + } + } + } finally { + connection?.let { + try { + it.disconnect() + } catch (_: Exception) { + } + } + } + + BlueshiftLogger.d("$TAG: $response") + + response + } + } + + private fun prepareGetRequest(connection: HttpsURLConnection) { + connection.requestMethod = "GET" + } + + private fun preparePostRequest( + connection: HttpsURLConnection, request: BlueshiftNetworkRequest + ) { + connection.doOutput = true + connection.requestMethod = "POST" + + request.body?.let { bodyJson -> + val bodyBytes = bodyJson.toString().toByteArray() + val outputStream = connection.outputStream + outputStream.write(bodyBytes) + outputStream.flush() + } + } + + private fun readResponseFromHttpsConnection(connection: HttpsURLConnection): BlueshiftNetworkResponse { + val responseCode = connection.responseCode + + val responseBody = try { + val inputStream = connection.inputStream + inputStream.bufferedReader().readText() + } catch (e: Exception) { + try { + val errorStream = connection.errorStream + errorStream.bufferedReader().readText() + } catch (ex: Exception) { + BlueshiftLogger.d("$TAG - Error reading error stream: $ex") + "" + } + } + + + return BlueshiftNetworkResponse(responseCode = responseCode, responseBody = responseBody) + } + + companion object { + private const val TAG = "NetworkRepository" + } +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkRequest.kt b/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkRequest.kt new file mode 100644 index 00000000..6dfba1d0 --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkRequest.kt @@ -0,0 +1,31 @@ +package com.blueshift.core.network + +import com.blueshift.core.database.BlueshiftSQLiteModel +import org.json.JSONObject + +data class BlueshiftNetworkRequest( + override val id: Long = -1, + val url: String, + val method: Method, + val header: JSONObject? = null, + val body: JSONObject? = null, + var authorization: String? = null, // should add it from network config when needed + val authorizationRequired: Boolean = false, // for db to store if auth is required + var retryAttemptBalance: Int = 3, + var retryAttemptTimestamp: Long = 0, // epoch timestamp + val timestamp: Long = 0, // epoch timestamp +) : BlueshiftSQLiteModel() { + enum class Method { + GET, POST; + + companion object { + fun fromString(string: String): Method { + return when (string) { + "GET" -> GET + "POST" -> POST + else -> GET + } + } + } + } +} diff --git a/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkRequestRepository.kt b/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkRequestRepository.kt new file mode 100644 index 00000000..0fe4ab5f --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkRequestRepository.kt @@ -0,0 +1,9 @@ +package com.blueshift.core.network + +interface BlueshiftNetworkRequestRepository { + suspend fun insertRequest(networkRequest: BlueshiftNetworkRequest) + suspend fun updateRequest(networkRequest: BlueshiftNetworkRequest) + suspend fun deleteRequest(networkRequest: BlueshiftNetworkRequest) + suspend fun readNextRequest(): BlueshiftNetworkRequest? + suspend fun clear() +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkRequestRepositoryImpl.kt b/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkRequestRepositoryImpl.kt new file mode 100644 index 00000000..40b59224 --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkRequestRepositoryImpl.kt @@ -0,0 +1,144 @@ +package com.blueshift.core.network + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import com.blueshift.core.database.BlueshiftSQLiteOpenHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.json.JSONObject + +class BlueshiftNetworkRequestRepositoryImpl( + context: Context? +) : BlueshiftSQLiteOpenHelper( + context, "com.blueshift.network_request_queue.db", null, 1 +), BlueshiftNetworkRequestRepository { + override fun onCreate(db: SQLiteDatabase?) { + db?.execSQL(buildCreateTableQuery()) + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + + } + + override fun getContentValues(obj: BlueshiftNetworkRequest): ContentValues { + val contentValues = ContentValues() + if (obj.id != ID_DEFAULT) contentValues.put(ID, obj.id) + contentValues.put(URL, obj.url) + contentValues.put(METHOD, obj.method.name) + obj.header?.let { + contentValues.put(HEADER, it.toString().toByteArray(charset = Charsets.UTF_8)) + } + obj.body?.let { + contentValues.put(BODY, it.toString().toByteArray(charset = Charsets.UTF_8)) + } + contentValues.put(AUTH_REQUIRED, if (obj.authorizationRequired) 1 else 0) + contentValues.put(RETRY_BALANCE, obj.retryAttemptBalance) + contentValues.put(RETRY_TIMESTAMP, obj.retryAttemptTimestamp) + contentValues.put(TIMESTAMP, obj.timestamp) + + return contentValues + } + + override fun getObject(cursor: Cursor): BlueshiftNetworkRequest { + val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID)) + val url = cursor.getString(cursor.getColumnIndexOrThrow(URL)) + val method = cursor.getString(cursor.getColumnIndexOrThrow(METHOD)) + var headerJson: JSONObject? = null + val headerBytes = cursor.getBlob(cursor.getColumnIndexOrThrow(HEADER)) + headerBytes?.let { + val headerString = String(it, Charsets.UTF_8) + headerJson = JSONObject(headerString) + } + var bodyJson: JSONObject? = null + val bodyBytes = cursor.getBlob(cursor.getColumnIndexOrThrow(BODY)) + bodyBytes?.let { + val bodyString = String(it, Charsets.UTF_8) + bodyJson = JSONObject(bodyString) + } + val authRequired = cursor.getInt(cursor.getColumnIndexOrThrow(AUTH_REQUIRED)) == 1 + val retryBalance = cursor.getInt(cursor.getColumnIndexOrThrow(RETRY_BALANCE)) + val retryTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(RETRY_TIMESTAMP)) + val timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)) + + return BlueshiftNetworkRequest( + id = id, + url = url, + method = BlueshiftNetworkRequest.Method.fromString(method), + header = headerJson, + body = bodyJson, + authorizationRequired = authRequired, + retryAttemptBalance = retryBalance, + retryAttemptTimestamp = retryTimestamp, + timestamp = timestamp + ) + } + + override val tableName: String = "request_queue_table" + override val fields: Map = mapOf( + URL to FieldType.String, + METHOD to FieldType.String, + HEADER to FieldType.Blob, + BODY to FieldType.Blob, + AUTH_REQUIRED to FieldType.Integer, + RETRY_BALANCE to FieldType.Integer, + RETRY_TIMESTAMP to FieldType.Integer, + TIMESTAMP to FieldType.Integer, + ) + + companion object { + private const val URL = "url" + private const val METHOD = "method" + private const val HEADER = "header" + private const val BODY = "body" + private const val AUTH_REQUIRED = "auth_required" + private const val RETRY_BALANCE = "retry_balance" + private const val RETRY_TIMESTAMP = "retry_timestamp" + private const val TIMESTAMP = "timestamp" + } + + override suspend fun insertRequest(networkRequest: BlueshiftNetworkRequest) { + insert(networkRequest) + } + + override suspend fun updateRequest(networkRequest: BlueshiftNetworkRequest) { + update(networkRequest) + } + + override suspend fun deleteRequest(networkRequest: BlueshiftNetworkRequest) { + delete(networkRequest) + } + + override suspend fun readNextRequest(): BlueshiftNetworkRequest? { + return withContext(Dispatchers.IO) { + synchronized(this) { + var request: BlueshiftNetworkRequest? = null + val cursor = readableDatabase.query( + tableName, + null, + "$RETRY_BALANCE > 0 AND $RETRY_TIMESTAMP < ${System.currentTimeMillis()}", + null, + null, + null, + "$TIMESTAMP ASC", + "1" + ) + + if (cursor.moveToFirst()) { + request = getObject(cursor) + } + + cursor.close() + + request + } + } + } + + override suspend fun clear() { + withContext(Dispatchers.IO) { + deleteAll(whereClause = null, selectionArgs = null) + } + } +} diff --git a/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkResponse.kt b/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkResponse.kt new file mode 100644 index 00000000..900146f6 --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/network/BlueshiftNetworkResponse.kt @@ -0,0 +1,3 @@ +package com.blueshift.core.network + +data class BlueshiftNetworkResponse(val responseCode: Int, val responseBody: String) diff --git a/android-sdk/src/main/java/com/blueshift/core/schedule/network/BlueshiftNetworkChangeJobService.kt b/android-sdk/src/main/java/com/blueshift/core/schedule/network/BlueshiftNetworkChangeJobService.kt new file mode 100644 index 00000000..db033b68 --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/schedule/network/BlueshiftNetworkChangeJobService.kt @@ -0,0 +1,54 @@ +package com.blueshift.core.schedule.network + +import android.app.job.JobParameters +import android.app.job.JobService +import com.blueshift.core.BlueshiftEventManager +import com.blueshift.core.BlueshiftNetworkRequestQueueManager +import com.blueshift.core.common.BlueshiftLogger +import com.blueshift.util.NetworkUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class BlueshiftNetworkChangeJobService : JobService() { + override fun onStartJob(params: JobParameters?): Boolean { + doBackgroundWork(jobParameters = params) + + // The job should continue running as the enqueue operation may take a while + // we have the jobFinished method called once the task is complete. + return true + } + + private fun doBackgroundWork(jobParameters: JobParameters?) { + CoroutineScope(Dispatchers.Default).launch { + try { + BlueshiftLogger.d("$TAG: doBackgroundWork - START") + + // Create batches of events and add it to request queue + BlueshiftEventManager.buildAndEnqueueBatchEvents() + + // If internet is available, sync the queue + if (NetworkUtils.isConnected(applicationContext)) { + BlueshiftNetworkRequestQueueManager.sync() + } + + BlueshiftLogger.d("$TAG: doBackgroundWork - FINISH") + } catch (e: Exception) { + BlueshiftLogger.e("$TAG: doBackgroundWork - ERROR : ${e.stackTraceToString()}") + } + + // this is a periodic job, we don't need the job scheduler to + // reschedule this with available backoff policy. hence passing + // false as 2nd argument. + jobFinished(jobParameters, false) + } + } + + override fun onStopJob(params: JobParameters?): Boolean { + return false + } + + companion object { + const val TAG = "BlueshiftNetworkChangeJobService" + } +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/blueshift/core/schedule/network/BlueshiftNetworkChangeScheduler.kt b/android-sdk/src/main/java/com/blueshift/core/schedule/network/BlueshiftNetworkChangeScheduler.kt new file mode 100644 index 00000000..91370b31 --- /dev/null +++ b/android-sdk/src/main/java/com/blueshift/core/schedule/network/BlueshiftNetworkChangeScheduler.kt @@ -0,0 +1,46 @@ +package com.blueshift.core.schedule.network + +import android.app.job.JobInfo +import android.app.job.JobScheduler +import android.content.ComponentName +import android.content.Context +import android.os.Build +import com.blueshift.core.common.BlueshiftLogger +import com.blueshift.model.Configuration +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +object BlueshiftNetworkChangeScheduler { + fun scheduleWithJobScheduler(context: Context, configuration: Configuration) { + val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler + val jobID = configuration.networkChangeListenerJobId + + // Check if the job exists already, if yes, skip scheduling a new one + val jobExists = jobScheduler.allPendingJobs.any { it.id == jobID } + if (jobExists) return + + val intervalMillis = configuration.batchInterval + val componentName = ComponentName(context, BlueshiftNetworkChangeJobService::class.java) + + val builder = JobInfo.Builder(jobID, componentName) + // Send events on any network type + builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + // Send events on a given interval of time, default being 30 minutes. + builder.setPeriodic(intervalMillis) + // Send events only when battery is not low + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + builder.setRequiresBatteryNotLow(true) + } + val jobInfo = builder.build() + + CoroutineScope(Dispatchers.IO).launch { + try { + val isScheduled = jobScheduler.schedule(jobInfo) + BlueshiftLogger.d("job = BlueshiftNetworkChangeJobService, jobId = $jobID, isScheduled = $isScheduled") + } catch (e: Exception) { + BlueshiftLogger.e(e.stackTraceToString()) + } + } + } +} \ No newline at end of file diff --git a/android-sdk/src/main/java/com/blueshift/fcm/BlueshiftMessagingService.java b/android-sdk/src/main/java/com/blueshift/fcm/BlueshiftMessagingService.java index f5fbc1c6..c60db17c 100644 --- a/android-sdk/src/main/java/com/blueshift/fcm/BlueshiftMessagingService.java +++ b/android-sdk/src/main/java/com/blueshift/fcm/BlueshiftMessagingService.java @@ -395,12 +395,7 @@ protected void onMessageNotFound(Map data) { @Override public void onNewToken(@NonNull String newToken) { BlueshiftLogger.d(LOG_TAG, "onNewToken: " + newToken); - - BlueshiftAttributesApp.getInstance().updateFirebaseToken(newToken); - - // We are calling an identify here to make sure that the change in - // device token is notified to the blueshift servers. - Blueshift.getInstance(this).identifyUser(null, false); + handleNewToken(getApplicationContext(), newToken); } /** @@ -447,11 +442,18 @@ public static void handleMessageReceived(Context context, RemoteMessage remoteMe /** * Helper method for the host app to invoke the onNewToken method of the BlueshiftMessagingService class * + * @param context Valid {@link Context} object * @param newToken Valid new token provided by FCM */ - public static void handleNewToken(String newToken) { - if (newToken != null) { - new BlueshiftMessagingService().onNewToken(newToken); + public static void handleNewToken(Context context, String newToken) { + try { + BlueshiftAttributesApp.getInstance().updateFirebaseToken(context, newToken); + + // We are calling an identify here to make sure that the change in + // device token is notified to the blueshift servers. + Blueshift.getInstance(context).identifyUser(null, false); + } catch (Exception e) { + BlueshiftLogger.e(LOG_TAG, e); } } } diff --git a/android-sdk/src/main/java/com/blueshift/httpmanager/Method.java b/android-sdk/src/main/java/com/blueshift/httpmanager/Method.java index e329e568..1ba663a6 100644 --- a/android-sdk/src/main/java/com/blueshift/httpmanager/Method.java +++ b/android-sdk/src/main/java/com/blueshift/httpmanager/Method.java @@ -1,5 +1,12 @@ package com.blueshift.httpmanager; +/** + * @deprecated + * This class is deprecated and will be removed in a future release. The events module has been + * refactored to improve performance and reliability. This class is now used internally for legacy + * data migration and will not be supported going forward. + */ +@Deprecated public enum Method { GET, POST, diff --git a/android-sdk/src/main/java/com/blueshift/httpmanager/Request.java b/android-sdk/src/main/java/com/blueshift/httpmanager/Request.java index 9545d45f..24bb80e2 100644 --- a/android-sdk/src/main/java/com/blueshift/httpmanager/Request.java +++ b/android-sdk/src/main/java/com/blueshift/httpmanager/Request.java @@ -10,7 +10,13 @@ * @author Rahul Raveendran V P * Created on 25/2/15 @ 3:04 PM * https://github.com/rahulrvp + * + * @deprecated + * This class is deprecated and will be removed in a future release. The events module has been + * refactored to improve performance and reliability. This class is now used internally for legacy + * data migration and will not be supported going forward. */ +@Deprecated public class Request implements Serializable { private long id; private String url; diff --git a/android-sdk/src/main/java/com/blueshift/httpmanager/Response.java b/android-sdk/src/main/java/com/blueshift/httpmanager/Response.java index 8f4bb522..31b86273 100644 --- a/android-sdk/src/main/java/com/blueshift/httpmanager/Response.java +++ b/android-sdk/src/main/java/com/blueshift/httpmanager/Response.java @@ -7,7 +7,13 @@ * @author Rahul Raveendran V P * Created on 05/12/13 @ 3:04 PM * https://github.com/rahulrvp + * + * @deprecated + * This class is deprecated and will be removed in a future release. The events module has been + * refactored to improve performance and reliability. This class is now used internally for legacy + * data migration and will not be supported going forward. */ +@Deprecated public class Response { private int statusCode; // HTTP Status Code private String responseBody; diff --git a/android-sdk/src/main/java/com/blueshift/httpmanager/request_queue/RequestQueueJobService.java b/android-sdk/src/main/java/com/blueshift/httpmanager/request_queue/RequestQueueJobService.java index 8e5d1e3c..a3b0cb92 100644 --- a/android-sdk/src/main/java/com/blueshift/httpmanager/request_queue/RequestQueueJobService.java +++ b/android-sdk/src/main/java/com/blueshift/httpmanager/request_queue/RequestQueueJobService.java @@ -15,9 +15,13 @@ * @author Rahul Raveendran V P * Created on 13/03/18 @ 11:09 AM * https://github.com/rahulrvp + * + * @deprecated + * This class is deprecated and will be removed in a future release. The events module has been + * refactored to improve performance and reliability. This class is now used internally for legacy + * data migration and will not be supported going forward. */ - - +@Deprecated @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public class RequestQueueJobService extends JobService { private static final String TAG = "RequestQueueJobService"; diff --git a/android-sdk/src/main/java/com/blueshift/model/Configuration.java b/android-sdk/src/main/java/com/blueshift/model/Configuration.java index bbf829d2..7c1c2a28 100644 --- a/android-sdk/src/main/java/com/blueshift/model/Configuration.java +++ b/android-sdk/src/main/java/com/blueshift/model/Configuration.java @@ -35,6 +35,13 @@ public class Configuration { // job scheduler private int networkChangeListenerJobId; + // todo: Fix the version in comments + /** + * @deprecated starting v3.5.? we are deprecating this field. The bulk events will be created + * and sent to blueshift when internet connection is available at a periodic interval. This process + * will be done using the network change job scheduler we have created. + */ + @Deprecated private int bulkEventsJobId; // push @@ -213,10 +220,20 @@ public void setNetworkChangeListenerJobId(int networkChangeListenerJobId) { this.networkChangeListenerJobId = networkChangeListenerJobId; } + /** + * @deprecated The field bulkEventsJobId is deprecated. + * @return bulkEventsJobId + */ + @Deprecated public int getBulkEventsJobId() { return bulkEventsJobId; } + /** + * @deprecated The field bulkEventsJobId is deprecated. + * @param bulkEventsJobId bulkEventsJobId + */ + @Deprecated public void setBulkEventsJobId(int bulkEventsJobId) { this.bulkEventsJobId = bulkEventsJobId; } diff --git a/android-sdk/src/main/java/com/blueshift/receiver/AppInstallReceiver.java b/android-sdk/src/main/java/com/blueshift/receiver/AppInstallReceiver.java index 85cfc6c3..f5d2f23f 100644 --- a/android-sdk/src/main/java/com/blueshift/receiver/AppInstallReceiver.java +++ b/android-sdk/src/main/java/com/blueshift/receiver/AppInstallReceiver.java @@ -10,9 +10,13 @@ /** * @author Rahul Raveendran V P - * Created on 17/3/15 @ 3:04 PM - * https://github.com/rahulrvp + * Created on 17/3/15 @ 3:04 PM + * ... + * @deprecated Since version 3.4.6, the Blueshift SDK automatically detects app install and update events. + * Using this class to track app installs can lead to confusion. We recommend using the automatic detection instead. + * This class will be removed in a future release. */ +@Deprecated public class AppInstallReceiver extends BroadcastReceiver { private static final String LOG_TAG = AppInstallReceiver.class.getSimpleName(); diff --git a/android-sdk/src/main/java/com/blueshift/receiver/NetworkChangeListener.java b/android-sdk/src/main/java/com/blueshift/receiver/NetworkChangeListener.java index 8e9e3e1e..ad1b6ecd 100644 --- a/android-sdk/src/main/java/com/blueshift/receiver/NetworkChangeListener.java +++ b/android-sdk/src/main/java/com/blueshift/receiver/NetworkChangeListener.java @@ -11,8 +11,13 @@ * @author Rahul Raveendran V P * Created on 09/03/18 @ 3:16 PM * https://github.com/rahulrvp + * + * @deprecated + * This class is deprecated and will be removed in a future release. The events module has been + * refactored to improve performance and reliability. This class is now used internally for legacy + * data migration and will not be supported going forward. */ - +@Deprecated public class NetworkChangeListener extends BroadcastReceiver { /* diff --git a/android-sdk/src/main/java/com/blueshift/request_queue/RequestDispatcher.java b/android-sdk/src/main/java/com/blueshift/request_queue/RequestDispatcher.java index c8527f8a..ecd24221 100644 --- a/android-sdk/src/main/java/com/blueshift/request_queue/RequestDispatcher.java +++ b/android-sdk/src/main/java/com/blueshift/request_queue/RequestDispatcher.java @@ -36,6 +36,13 @@ import java.lang.reflect.Type; import java.util.HashMap; +/** + * @deprecated + * This class is deprecated and will be removed in a future release. The events module has been + * refactored to improve performance and reliability. This class is now used internally for legacy + * data migration and will not be supported going forward. + */ +@Deprecated class RequestDispatcher { private static final String LOG_TAG = "RequestDispatcher"; private static final long RETRY_INTERVAL = 5 * 60 * 1000; diff --git a/android-sdk/src/main/java/com/blueshift/request_queue/RequestQueue.java b/android-sdk/src/main/java/com/blueshift/request_queue/RequestQueue.java index aa22cf28..64bfa4c9 100644 --- a/android-sdk/src/main/java/com/blueshift/request_queue/RequestQueue.java +++ b/android-sdk/src/main/java/com/blueshift/request_queue/RequestQueue.java @@ -20,7 +20,13 @@ * @author Rahul Raveendran V P * Created on 26/2/15 @ 3:07 PM * https://github.com/rahulrvp + * + * @deprecated + * This class is deprecated and will be removed in a future release. The events module has been + * refactored to improve performance and reliability. This class is now used internally for legacy + * data migration and will not be supported going forward. */ +@Deprecated public class RequestQueue { public static final int DEFAULT_RETRY_COUNT = 3; diff --git a/android-sdk/src/main/java/com/blueshift/request_queue/RequestQueueTable.java b/android-sdk/src/main/java/com/blueshift/request_queue/RequestQueueTable.java index 77c45547..afa53a26 100644 --- a/android-sdk/src/main/java/com/blueshift/request_queue/RequestQueueTable.java +++ b/android-sdk/src/main/java/com/blueshift/request_queue/RequestQueueTable.java @@ -16,7 +16,12 @@ * @author Rahul Raveendran V P * Created on 26/5/15 @ 3:07 PM * https://github.com/rahulrvp + * @deprecated + * This class is deprecated and will be removed in a future release. The events module has been + * refactored to improve performance and reliability. This class is now used internally for legacy + * data migration and will not be supported going forward. */ +@Deprecated public class RequestQueueTable extends BaseSqliteTable { private static final String LOG_TAG = RequestQueueTable.class.getSimpleName(); diff --git a/android-sdk/src/main/java/com/blueshift/rich_push/NotificationFactory.java b/android-sdk/src/main/java/com/blueshift/rich_push/NotificationFactory.java index 3abf6b6d..043d594e 100644 --- a/android-sdk/src/main/java/com/blueshift/rich_push/NotificationFactory.java +++ b/android-sdk/src/main/java/com/blueshift/rich_push/NotificationFactory.java @@ -34,7 +34,7 @@ /** * @author Rahul Raveendran V P * Created on 18/2/15 @ 12:22 PM - * https://github.com/rahulrvp + * ... */ public class NotificationFactory { private final static String LOG_TAG = NotificationFactory.class.getSimpleName(); diff --git a/android-sdk/src/test/java/com/blueshift/BlueshiftTest.java b/android-sdk/src/test/java/com/blueshift/BlueshiftTest.java index b7ca9998..2b909fb3 100644 --- a/android-sdk/src/test/java/com/blueshift/BlueshiftTest.java +++ b/android-sdk/src/test/java/com/blueshift/BlueshiftTest.java @@ -1,95 +1,4 @@ package com.blueshift; -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import android.content.Context; - -import com.blueshift.util.CommonUtils; - -import org.junit.Test; - -import java.io.File; -import java.util.HashMap; -import java.util.List; - public class BlueshiftTest { - @Test - public void testFreshAppInstall() { - Context context = mock(Context.class); - - // stored version is unavailable - when(BlueShiftPreference.getStoredAppVersionString(context)).thenReturn(null); - - // database file is unavailable - File database = mock(File.class); - when(context.getDatabasePath("blueshift_db.sqlite3")).thenReturn(database); - when(database.exists()).thenReturn(false); - - // app version is available - when(CommonUtils.getAppVersion(context)).thenReturn("1.0.0"); - - Blueshift blueshift = Blueshift.getInstance(context); - List result = blueshift.inferAppVersionChangeEvent(context); - - // assert that the item in 0th index of result is "app_install" - assertEquals("app_install", result.get(0)); - - // assert that the hashmap in 1st index of result has key "app_installed_at" - HashMap map = (HashMap) result.get(1); - assertEquals("app_installed_at", map.keySet().iterator().next()); - } - - @Test - public void testAppVersionUpgradeOldSDKToNewSDK() { - Context context = mock(Context.class); - - // stored version is unavailable - when(BlueShiftPreference.getStoredAppVersionString(context)).thenReturn(null); - - // database file is available - File database = mock(File.class); - when(context.getDatabasePath("blueshift_db.sqlite3")).thenReturn(database); - when(database.exists()).thenReturn(true); - - // app version is available - when(CommonUtils.getAppVersion(context)).thenReturn("2.0.0"); - - Blueshift blueshift = Blueshift.getInstance(context); - List result = blueshift.inferAppVersionChangeEvent(context); - - // assert that the item in 0th index of result is "app_update" - assertEquals("app_update", result.get(0)); - - // assert that the hashmap in 1st index of result has key "app_updated_at" - HashMap map = (HashMap) result.get(1); - assertEquals("app_updated_at", map.keySet().iterator().next()); - } - - @Test - public void testAppVersionUpgradeNoSDKToNewSDK() { - Context context = mock(Context.class); - - // stored version is available - when(BlueShiftPreference.getStoredAppVersionString(context)).thenReturn(null); - - // database file is available - File database = mock(File.class); - when(context.getDatabasePath("blueshift_db.sqlite3")).thenReturn(database); - when(database.exists()).thenReturn(false); - - // app version is available - when(CommonUtils.getAppVersion(context)).thenReturn("2.0.0"); - - Blueshift blueshift = Blueshift.getInstance(context); - List result = blueshift.inferAppVersionChangeEvent(context); - - // assert that the item in 0th index of result is "app_install" - assertEquals("app_install", result.get(0)); - - // assert that the hashmap in 1st index of result has key "app_installed_at" - HashMap map = (HashMap) result.get(1); - assertEquals("app_installed_at", map.keySet().iterator().next()); - } } diff --git a/android-sdk/src/test/java/com/blueshift/ExampleUnitTest.java b/android-sdk/src/test/java/com/blueshift/ExampleUnitTest.java deleted file mode 100644 index 3a1ed9d4..00000000 --- a/android-sdk/src/test/java/com/blueshift/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.blueshift; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/android-sdk/src/test/java/com/blueshift/core/BlueshiftEventManagerTest.kt b/android-sdk/src/test/java/com/blueshift/core/BlueshiftEventManagerTest.kt new file mode 100644 index 00000000..efd1fca9 --- /dev/null +++ b/android-sdk/src/test/java/com/blueshift/core/BlueshiftEventManagerTest.kt @@ -0,0 +1,149 @@ +package com.blueshift.core + +import android.util.Log +import com.blueshift.core.events.BlueshiftEvent +import com.blueshift.core.events.FakeEventsRepo +import com.blueshift.core.network.FakeNetworkRequestRepo +import io.mockk.every +import io.mockk.mockkStatic +import kotlinx.coroutines.runBlocking +import org.json.JSONObject +import org.junit.After +import org.junit.Before +import org.junit.Test + +class BlueshiftEventManagerTest { + private lateinit var fakeNetworkRequestRepo: FakeNetworkRequestRepo + private lateinit var fakeEventsRepo: FakeEventsRepo + + @Before + fun setUp() { + mockkStatic(Log::class) + every { Log.d(any(), any()) } returns 0 + + fakeNetworkRequestRepo = FakeNetworkRequestRepo() + fakeEventsRepo = FakeEventsRepo() + + BlueshiftEventManager.initialize( + fakeEventsRepo, fakeNetworkRequestRepo, BlueshiftLambdaQueue + ) + } + + @After + fun tearDown() = runBlocking { + fakeEventsRepo.clear() + fakeNetworkRequestRepo.clear() + } + + @Test + fun trackCampaignEvent_shouldNotInsertAnyNetworkRequestWhenQueryStringIsEmpty() = runBlocking { + // Empty query string + BlueshiftEventManager.trackCampaignEvent(queryString = "") + assert(fakeNetworkRequestRepo.requests.size == 0) + } + + @Test + fun trackCampaignEvent_shouldInsertOneNetworkRequestWhenQueryStringIsNotEmpty() = runBlocking { + // Query string with key and value + BlueshiftEventManager.trackCampaignEvent(queryString = "key=value&number=1") + assert(fakeNetworkRequestRepo.requests.size == 1) + + // The url should contain the query string + assert(fakeNetworkRequestRepo.requests[0].url.contains("key=value&number=1")) + } + + @Test + fun trackEvent_shouldInsertOneNetworkRequestWhenOnline() = runBlocking { + BlueshiftEventManager.trackEvent( + event = BlueshiftEvent( + id = 1, + eventName = "test", + eventParams = JSONObject(), + timestamp = System.currentTimeMillis() + ), isBatchEvent = false // when online, we don't add events as batched events. + ) + + // A new request should be added to network request repo + assert(fakeNetworkRequestRepo.requests.size == 1) + // No request should be added to events repo (offline events) + assert(fakeEventsRepo.blueshiftEvents.size == 0) + } + + @Test + fun trackEvent_shouldInsertOneEventWhenOffline() = runBlocking { + BlueshiftEventManager.trackEvent( + event = BlueshiftEvent( + id = 1, eventName = "test", eventParams = JSONObject(), timestamp = 0L + ), isBatchEvent = true // we add a batched event when we are offline + ) + + // No request should be added to network request repo + assert(fakeNetworkRequestRepo.requests.size == 0) + // A new request should be added to events repo (offline events) + assert(fakeEventsRepo.blueshiftEvents.size == 1) + } + + private suspend fun insertBatchEvents(count: Int) { + for (i in 1..count) { + BlueshiftEventManager.trackEvent( + event = BlueshiftEvent( + id = i.toLong(), eventName = "test", eventParams = JSONObject(), timestamp = 0L + ), isBatchEvent = true + ) + } + } + + @Test + fun buildAndEnqueueBatchEvents_shouldInsertABulkEventRequestPer100OfflineEvents() = + runBlocking { + insertBatchEvents(200) + + BlueshiftEventManager.buildAndEnqueueBatchEvents() + + // Two bulk event requests should be added in the network request repo + assert(fakeNetworkRequestRepo.requests.size == 2) + // After sync, the events repo should be empty + assert(fakeEventsRepo.blueshiftEvents.size == 0) + } + + @Test + fun buildAndEnqueueBatchEvents_shouldInsertOneBulkEventRequestForLessThan100OfflineEvents() = + runBlocking { + insertBatchEvents(50) + + BlueshiftEventManager.buildAndEnqueueBatchEvents() + + // Two bulk event requests should be added in the network request repo + assert(fakeNetworkRequestRepo.requests.size == 1) + // After sync, the events repo should be empty + assert(fakeEventsRepo.blueshiftEvents.size == 0) + } + + @Test + fun clear_shouldDeleteAllEventsAndNetworkRequests() = runBlocking { + BlueshiftEventManager.trackEvent( + event = BlueshiftEvent( + id = 1, + eventName = "offline_event", + eventParams = JSONObject(), + timestamp = System.currentTimeMillis() + ), isBatchEvent = false + ) + BlueshiftEventManager.trackEvent( + event = BlueshiftEvent( + id = 1, + eventName = "realtime_event", + eventParams = JSONObject(), + timestamp = System.currentTimeMillis() + ), isBatchEvent = true + ) + + assert(fakeNetworkRequestRepo.requests.size == 1) + assert(fakeEventsRepo.blueshiftEvents.size == 1) + + BlueshiftEventManager.clear() + + assert(fakeNetworkRequestRepo.requests.size == 0) + assert(fakeEventsRepo.blueshiftEvents.size == 0) + } +} diff --git a/android-sdk/src/test/java/com/blueshift/core/BlueshiftNetworkRequestQueueManagerTest.kt b/android-sdk/src/test/java/com/blueshift/core/BlueshiftNetworkRequestQueueManagerTest.kt new file mode 100644 index 00000000..dcc99fa9 --- /dev/null +++ b/android-sdk/src/test/java/com/blueshift/core/BlueshiftNetworkRequestQueueManagerTest.kt @@ -0,0 +1,158 @@ +package com.blueshift.core + +import android.util.Log +import com.blueshift.core.network.BlueshiftNetworkConfiguration +import com.blueshift.core.network.BlueshiftNetworkRequest +import com.blueshift.core.network.FakeNetworkRepoWithAPIError +import com.blueshift.core.network.FakeNetworkRepoWithAPISuccess +import com.blueshift.core.network.FakeNetworkRequestRepo +import io.mockk.every +import io.mockk.mockkStatic +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Test + +class BlueshiftNetworkRequestQueueManagerTest { + private lateinit var networkRequestRepo: FakeNetworkRequestRepo + + companion object { + const val REQUEST_COUNT = 2 + } + + @Before + fun setUp() = runBlocking { + networkRequestRepo = FakeNetworkRequestRepo() + + (1..REQUEST_COUNT).forEach { + val networkRequest = BlueshiftNetworkRequest( + id = it.toLong(), + url = "https://fakeapi.com", + method = BlueshiftNetworkRequest.Method.GET, + body = null + ) + + networkRequestRepo.insertRequest(networkRequest = networkRequest) + } + + BlueshiftNetworkConfiguration.authorization = "basicAuth" + } + + @After + fun tearDown() { + networkRequestRepo.requests.clear() + } + + @Test + fun insertRequest_TheQueueShouldContainsTheInsertedNumberOfRequests() = runBlocking { + val requestQueueManager = BlueshiftNetworkRequestQueueManager + requestQueueManager.initialize(networkRequestRepo, FakeNetworkRepoWithAPISuccess()) + + val request = BlueshiftNetworkRequest( + id = 100, + url = "https://fakeapi.com", + method = BlueshiftNetworkRequest.Method.GET, + body = null + ) + + requestQueueManager.insertNewRequest(request) + + // The request count should increment by one + assert(networkRequestRepo.requests.size == REQUEST_COUNT + 1) + } + + @Test + fun deleteRequest_TheQueueShouldNotContainTheDeletedRequest() = runBlocking { + val requestQueueManager = BlueshiftNetworkRequestQueueManager + requestQueueManager.initialize(networkRequestRepo, FakeNetworkRepoWithAPISuccess()) + + val request = requestQueueManager.readNextRequest() + request?.let { requestQueueManager.deleteRequest(request) } + + // The deleted request should not be found in the repo + assert(networkRequestRepo.requests.find { it.id == request?.id } == null) + } + + @Test + fun updateRequest_TheQueueShouldContainTheUpdatedRequestWithUpdatedRetryTimestamp() = + runBlocking { + val requestQueueManager = BlueshiftNetworkRequestQueueManager + requestQueueManager.initialize(networkRequestRepo, FakeNetworkRepoWithAPISuccess()) + + val newRetryTimestamp = System.currentTimeMillis() + 100 + val request = requestQueueManager.readNextRequest() + request?.let { + request.retryAttemptTimestamp = newRetryTimestamp + requestQueueManager.updateRequest(request) + } + + assert(networkRequestRepo.requests.find { it.id == request?.id }?.retryAttemptTimestamp == newRetryTimestamp) + } + + @Test + fun sync_ShouldClearTheQueueWhenApiReturnsSuccess() = runBlocking { + mockkStatic(Log::class) + every { Log.d(any(), any()) } returns 0 + + val requestQueueManager = BlueshiftNetworkRequestQueueManager + requestQueueManager.initialize(networkRequestRepo, FakeNetworkRepoWithAPISuccess()) + + requestQueueManager.sync() + + assert(networkRequestRepo.requests.isEmpty()) + } + + @Test + fun sync_ShouldNotClearTheQueueWhenApiReturnsError() = runBlocking { + mockkStatic(Log::class) + every { Log.d(any(), any()) } returns 0 + + val requestQueueManager = BlueshiftNetworkRequestQueueManager + requestQueueManager.initialize(networkRequestRepo, FakeNetworkRepoWithAPIError()) + + requestQueueManager.sync() + + assert(networkRequestRepo.requests.size == REQUEST_COUNT) + } + + @Test + fun sync_ShouldDecrementTheRetryAttemptBalanceByOneWhenApiReturnsError() = runBlocking { + mockkStatic(Log::class) + every { Log.d(any(), any()) } returns 0 + + val requestQueueManager = BlueshiftNetworkRequestQueueManager + requestQueueManager.initialize(networkRequestRepo, FakeNetworkRepoWithAPIError()) + + requestQueueManager.sync() + + assert(networkRequestRepo.requests.filter { it.retryAttemptBalance == 2 }.size == REQUEST_COUNT) + } + + @Test + fun sync_ShouldSetNonZeroValueForRetryAttemptTimestampWhenApiReturnsError() = runBlocking { + mockkStatic(Log::class) + every { Log.d(any(), any()) } returns 0 + + val requestQueueManager = BlueshiftNetworkRequestQueueManager + requestQueueManager.initialize(networkRequestRepo, FakeNetworkRepoWithAPIError()) + + requestQueueManager.sync() + + assert(networkRequestRepo.requests.filter { it.retryAttemptTimestamp != 0L }.size == REQUEST_COUNT) + } + + @Test + fun sync_ShouldNotMakeAnyChangesToTheQueueWhenAuthorizationIsNull() = runBlocking { + mockkStatic(Log::class) + every { Log.d(any(), any()) } returns 0 + + BlueshiftNetworkConfiguration.authorization = null + + val requestQueueManager = BlueshiftNetworkRequestQueueManager + requestQueueManager.initialize(networkRequestRepo, FakeNetworkRepoWithAPISuccess()) + + requestQueueManager.sync() + + assert(networkRequestRepo.requests.size == REQUEST_COUNT) + } +} \ No newline at end of file diff --git a/android-sdk/src/test/java/com/blueshift/core/app/BlueshiftInstallationStatusHelperTest.kt b/android-sdk/src/test/java/com/blueshift/core/app/BlueshiftInstallationStatusHelperTest.kt new file mode 100644 index 00000000..bba33b36 --- /dev/null +++ b/android-sdk/src/test/java/com/blueshift/core/app/BlueshiftInstallationStatusHelperTest.kt @@ -0,0 +1,88 @@ +package com.blueshift.core.app + +import android.util.Log +import com.blueshift.BlueshiftConstants +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import org.junit.Before +import org.junit.Test +import java.io.File + +class BlueshiftInstallationStatusHelperTest { + + @Before + fun setUp() { + mockkStatic(Log::class) + every { Log.d(any(), any()) } returns 0 + } + + @Test + fun getInstallationStatus_returnAppInstallWhenDoingAFreshInstallOrUpdateFromAnAppWithoutBlueshiftSDK() { + // This happens in two scenarios, + // 1. When user installs the app for the first time. + // 2. When user update from an app version that does not have Blueshift SDK integrated to it. + val helper = BlueshiftInstallationStatusHelper() + val database = mockk() + every { database.exists() } returns false + val status = helper.getInstallationStatus("1.0.0", null, database) + assert(status == BlueshiftInstallationStatus.APP_INSTALL) + } + + @Test + fun getInstallationStatus_returnAppUpdateWhenPreviousAppVersionIsNullAndBlueshiftDatabaseExists() { + // This case happens when someone updates from an app that has Blueshift SDK version older + // than 3.4.6. + val helper = BlueshiftInstallationStatusHelper() + val database = mockk() + every { database.exists() } returns true + val status = helper.getInstallationStatus("1.0.0", null, database) + assert(status == BlueshiftInstallationStatus.APP_UPDATE) + } + + @Test + fun getInstallationStatus_returnAppUpdateWhenAppVersionAndPreviousAppVersionDoesNotMatch() { + val helper = BlueshiftInstallationStatusHelper() + val database = mockk() + val status = helper.getInstallationStatus("1.0.0", "2.0.0", database) + assert(status == BlueshiftInstallationStatus.APP_UPDATE) + } + + @Test + fun getInstallationStatus_returnNoChangeWhenAppVersionAndPreviousAppVersionMatches() { + val helper = BlueshiftInstallationStatusHelper() + val database = mockk() + val status = helper.getInstallationStatus("1.0.0", "1.0.0", database) + assert(status == BlueshiftInstallationStatus.NO_CHANGE) + } + + @Test + fun getEventAttributes_returnHashMapWithAppInstalledAtWhenStatusIsAppInstall() { + val helper = BlueshiftInstallationStatusHelper() + val attributes = helper.getEventAttributes(BlueshiftInstallationStatus.APP_INSTALL, null) + assert(attributes[BlueshiftConstants.KEY_APP_INSTALLED_AT] != null) + } + + @Test + fun getEventAttributes_returnHashMapWithAppUpdatedAtAndPreviousAppVersionWhenStatusIsAppUpdate() { + val helper = BlueshiftInstallationStatusHelper() + val attributes = helper.getEventAttributes(BlueshiftInstallationStatus.APP_UPDATE, "1.0.0") + assert(attributes[BlueshiftConstants.KEY_APP_UPDATED_AT] != null) + assert(attributes[BlueshiftConstants.KEY_PREVIOUS_APP_VERSION] == "1.0.0") + } + + @Test + fun getEventAttributes_returnHashMapWithAppUpdatedAtWhenStatusIsAppUpdateAndPreviousAppVersionIsNull() { + val helper = BlueshiftInstallationStatusHelper() + val attributes = helper.getEventAttributes(BlueshiftInstallationStatus.APP_UPDATE, null) + assert(attributes[BlueshiftConstants.KEY_APP_UPDATED_AT] != null) + assert(!attributes.contains(BlueshiftConstants.KEY_PREVIOUS_APP_VERSION)) + } + + @Test + fun getEventAttributes_returnHashMapWithEmptyMapWhenStatusIsNoChange() { + val helper = BlueshiftInstallationStatusHelper() + val attributes = helper.getEventAttributes(BlueshiftInstallationStatus.NO_CHANGE, null) + assert(attributes.isEmpty()) + } +} \ No newline at end of file diff --git a/android-sdk/src/test/java/com/blueshift/core/common/BlueshiftAPITest.kt b/android-sdk/src/test/java/com/blueshift/core/common/BlueshiftAPITest.kt new file mode 100644 index 00000000..a5fdf043 --- /dev/null +++ b/android-sdk/src/test/java/com/blueshift/core/common/BlueshiftAPITest.kt @@ -0,0 +1,34 @@ +package com.blueshift.core.common + +import org.junit.Assert.* +import org.junit.Test + +class BlueshiftAPITest { + @Test + fun setDatacenter_whenNotSet_shouldReturnCorrectURLForAllAPIsWithUSBaseUrl() { + val baseUrlForUS = "https://api.getblueshift.com/" + assertEquals("${baseUrlForUS}api/v1/event", BlueshiftAPI.eventURL()) + assertEquals("${baseUrlForUS}api/v1/bulkevents", BlueshiftAPI.bulkEventsURL()) + assertEquals("${baseUrlForUS}track?key=value", BlueshiftAPI.trackURL("key=value")) + } + + @Test + fun setDatacenter_whenSetToUS_shouldReturnCorrectURLForAllAPIsWithUSBaseUrl() { + BlueshiftAPI.setDatacenter(BlueshiftAPI.Datacenter.US) + + val baseUrlForUS = "https://api.getblueshift.com/" + assertEquals("${baseUrlForUS}api/v1/event", BlueshiftAPI.eventURL()) + assertEquals("${baseUrlForUS}api/v1/bulkevents", BlueshiftAPI.bulkEventsURL()) + assertEquals("${baseUrlForUS}track?key=value", BlueshiftAPI.trackURL("key=value")) + } + + @Test + fun setDatacenter_whenSetToEU_shouldReturnCorrectURLForAllAPIsWithEUBaseUrl() { + BlueshiftAPI.setDatacenter(BlueshiftAPI.Datacenter.EU) + + val baseUrlForEU = "https://api.eu.getblueshift.com/" + assertEquals("${baseUrlForEU}api/v1/event", BlueshiftAPI.eventURL()) + assertEquals("${baseUrlForEU}api/v1/bulkevents", BlueshiftAPI.bulkEventsURL()) + assertEquals("${baseUrlForEU}track?key=value", BlueshiftAPI.trackURL("key=value")) + } +} \ No newline at end of file diff --git a/android-sdk/src/test/java/com/blueshift/core/events/FakeEventsRepo.kt b/android-sdk/src/test/java/com/blueshift/core/events/FakeEventsRepo.kt new file mode 100644 index 00000000..1872da70 --- /dev/null +++ b/android-sdk/src/test/java/com/blueshift/core/events/FakeEventsRepo.kt @@ -0,0 +1,28 @@ +package com.blueshift.core.events + +import kotlin.math.min + +class FakeEventsRepo : BlueshiftEventRepository { + val blueshiftEvents = mutableListOf() + override suspend fun insertEvent(event: BlueshiftEvent) { + blueshiftEvents.add(event) + } + + override suspend fun deleteEvents(events: List) { + blueshiftEvents.removeAll(events) + } + + override suspend fun readOneBatch(batchCount: Int): List { + val result = mutableListOf() + val limit = min(batchCount, blueshiftEvents.size) + for (i in 0 until limit) { + result.add(blueshiftEvents[i]) + } + + return result + } + + override suspend fun clear() { + blueshiftEvents.clear() + } +} \ No newline at end of file diff --git a/android-sdk/src/test/java/com/blueshift/core/network/FakeNetworkRepoWithAPIError.kt b/android-sdk/src/test/java/com/blueshift/core/network/FakeNetworkRepoWithAPIError.kt new file mode 100644 index 00000000..8b4a6bec --- /dev/null +++ b/android-sdk/src/test/java/com/blueshift/core/network/FakeNetworkRepoWithAPIError.kt @@ -0,0 +1,7 @@ +package com.blueshift.core.network + +class FakeNetworkRepoWithAPIError : BlueshiftNetworkRepository { + override suspend fun makeNetworkRequest(networkRequest: BlueshiftNetworkRequest): BlueshiftNetworkResponse { + return BlueshiftNetworkResponse(responseCode = 500, responseBody = "{\"status\" : \"error\"}") + } +} \ No newline at end of file diff --git a/android-sdk/src/test/java/com/blueshift/core/network/FakeNetworkRepoWithAPISuccess.kt b/android-sdk/src/test/java/com/blueshift/core/network/FakeNetworkRepoWithAPISuccess.kt new file mode 100644 index 00000000..0d42b0b8 --- /dev/null +++ b/android-sdk/src/test/java/com/blueshift/core/network/FakeNetworkRepoWithAPISuccess.kt @@ -0,0 +1,7 @@ +package com.blueshift.core.network + +class FakeNetworkRepoWithAPISuccess : BlueshiftNetworkRepository { + override suspend fun makeNetworkRequest(networkRequest: BlueshiftNetworkRequest): BlueshiftNetworkResponse { + return BlueshiftNetworkResponse(responseCode = 200, responseBody = "{\"status\" : \"error\"}") + } +} \ No newline at end of file diff --git a/android-sdk/src/test/java/com/blueshift/core/network/FakeNetworkRequestRepo.kt b/android-sdk/src/test/java/com/blueshift/core/network/FakeNetworkRequestRepo.kt new file mode 100644 index 00000000..3dd60c10 --- /dev/null +++ b/android-sdk/src/test/java/com/blueshift/core/network/FakeNetworkRequestRepo.kt @@ -0,0 +1,33 @@ +package com.blueshift.core.network + +class FakeNetworkRequestRepo : BlueshiftNetworkRequestRepository { + val requests = mutableListOf() + override suspend fun insertRequest(networkRequest: BlueshiftNetworkRequest) { + requests.add(networkRequest) + } + + override suspend fun updateRequest(networkRequest: BlueshiftNetworkRequest) { + for (i in requests.indices) { + if (requests[i].id == networkRequest.id) { + requests[i] = networkRequest + break + } + } + } + + override suspend fun deleteRequest(networkRequest: BlueshiftNetworkRequest) { + requests.remove(networkRequest) + } + + override suspend fun readNextRequest(): BlueshiftNetworkRequest? { + return if (requests.isEmpty()) { + null + } else { + requests.find { it.retryAttemptBalance > 0 && it.retryAttemptTimestamp < System.currentTimeMillis() } + } + } + + override suspend fun clear() { + requests.clear() + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 27f4b9d0..94a71fa3 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,6 @@ allprojects { } } -task clean(type: Delete) { +tasks.register('clean', Delete) { delete rootProject.buildDir }