From 9706ded67ac434073c217e0a9dbbddf25dae4075 Mon Sep 17 00:00:00 2001 From: Sam Dozor Date: Fri, 3 Nov 2023 14:05:11 -0400 Subject: [PATCH 1/7] fix: only delete branch if all succeeds --- .github/workflows/daily.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index e8487c01c..860306ae2 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -283,8 +283,7 @@ jobs: delete-regression-branch: name: "Delete regression branch" - if: ${{ always() }} - needs: [semantic-release-dryrun, create-regression-branch] + needs: semantic-release-dryrun runs-on: ubuntu-latest env: GITHUB_TOKEN: ${{ secrets.MP_SEMANTIC_RELEASE_BOT }} From 522d91910480dc2e8d9fe62d89b28dd0b2e15719 Mon Sep 17 00:00:00 2001 From: markvdouw Date: Mon, 13 Nov 2023 12:07:54 -0300 Subject: [PATCH 2/7] Adding JobScheduler utilities with current and legacy code to trigger batch upload Implementing as the legacy impl, a delayed message with type UPLOAD based on dynamic delay Adding feature flag ebb to "enableBackgroundBatching", read from config and default=false Adding Service for uploading Adding class in proguard Business logic changes on AppStateManager to route to different implementations based on feature flag. Business logic change at the upload handler level to dismiss automatic upload interval and upload at session end when batching is enabled. --- android-core/proguard.pro | 1 + .../mparticle/internal/AppStateManager.java | 14 ++++++ .../com/mparticle/internal/ConfigManager.java | 13 +++++- .../mparticle/internal/MessageManager.java | 6 ++- .../com/mparticle/internal/UploadHandler.java | 7 ++- .../kotlin/com.mparticle/JobSchedulerUtils.kt | 46 +++++++++++++++++++ .../uploadbatching/BatchUploadingJob.kt | 27 +++++++++++ 7 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 android-core/src/main/kotlin/com.mparticle/JobSchedulerUtils.kt create mode 100644 android-core/src/main/kotlin/com/mparticle/uploadbatching/BatchUploadingJob.kt diff --git a/android-core/proguard.pro b/android-core/proguard.pro index f55779de1..7c0bb4cf6 100644 --- a/android-core/proguard.pro +++ b/android-core/proguard.pro @@ -94,6 +94,7 @@ -keep class com.mparticle.MParticle$UserAttributes { *; } -keep class com.mparticle.MParticle$ResetListener { *; } -keep class com.mparticle.MParticle$OperatingSystem { *; } +-keep class com.mparticle.uploadbatching.BatchUploadingJob -keep class com.mparticle.Session { *; } diff --git a/android-core/src/main/java/com/mparticle/internal/AppStateManager.java b/android-core/src/main/java/com/mparticle/internal/AppStateManager.java index 9d76d16a8..e51526c49 100644 --- a/android-core/src/main/java/com/mparticle/internal/AppStateManager.java +++ b/android-core/src/main/java/com/mparticle/internal/AppStateManager.java @@ -14,6 +14,7 @@ import androidx.annotation.Nullable; +import com.mparticle.JobSchedulerUtilsKt; import com.mparticle.MPEvent; import com.mparticle.MParticle; import com.mparticle.identity.IdentityApi; @@ -27,6 +28,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import kotlin.Unit; + /** * This class is responsible for maintaining the session state by listening to the Activity lifecycle. @@ -368,10 +371,21 @@ private void logBackgrounded() { instance.Internal().getKitManager().onApplicationBackground(); mCurrentActivityName = null; Logger.debug("App backgrounded."); + scheduleBackgroundJob(); mInterruptionCount.incrementAndGet(); } } + private void scheduleBackgroundJob() { + if (mConfigManager.isBackgroundEventBatchingEnabled()) { + JobSchedulerUtilsKt.scheduleBatchUploading(this.mContext, delay -> { + mMessageManager.mUploadHandler.sendMessageDelayed(mMessageManager.mUploadHandler.obtainMessage(UploadHandler.UPLOAD_TRIGGER_MESSAGES, 1, 0, mConfigManager.getMpid()), delay); + Logger.debug("Legacy action with delay: " + delay); + return Unit.INSTANCE; + }); + } + } + @TargetApi(14) private void setupLifecycleCallbacks() { ((Application) mContext).registerActivityLifecycleCallbacks(new MPLifecycleCallbackDelegate(this)); diff --git a/android-core/src/main/java/com/mparticle/internal/ConfigManager.java b/android-core/src/main/java/com/mparticle/internal/ConfigManager.java index cf225f843..fbc78b87f 100644 --- a/android-core/src/main/java/com/mparticle/internal/ConfigManager.java +++ b/android-core/src/main/java/com/mparticle/internal/ConfigManager.java @@ -58,6 +58,7 @@ public class ConfigManager { public static final String VALUE_CUE_CATCH = "forcecatch"; public static final String PREFERENCES_FILE = "mp_preferences"; public static final String KEY_INCLUDE_SESSION_HISTORY = "inhd"; + public static final String ENABLE_BACKGROUND_BATCHING = "ebb"; private static final String KEY_DEVICE_PERFORMANCE_METRICS_DISABLED = "dpmd"; public static final String WORKSPACE_TOKEN = "wst"; static final String ALIAS_MAX_WINDOW = "alias_max_window"; @@ -90,6 +91,7 @@ public class ConfigManager { private JSONObject mProviderPersistence; private int mRampValue = -1; private int mUserBucket = -1; + private boolean isBackgroundEventBatchingEnabled = false; private int mSessionTimeoutInterval = -1; private int mUploadInterval = -1; @@ -229,6 +231,10 @@ public void deleteUserStorage(long mpId) { deleteUserStorage(mContext, mpId); } + public boolean isBackgroundEventBatchingEnabled() { + return isBackgroundEventBatchingEnabled; + } + static void deleteConfigManager(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { context.deleteSharedPreferences(PREFERENCES_FILE); @@ -407,6 +413,7 @@ private synchronized void updateCoreConfig(JSONObject responseJSON, boolean newC } mRampValue = responseJSON.optInt(KEY_RAMP, -1); + isBackgroundEventBatchingEnabled = responseJSON.optBoolean(ENABLE_BACKGROUND_BATCHING, false); if (responseJSON.has(KEY_OPT_OUT)) { mSendOoEvents = responseJSON.getBoolean(KEY_OPT_OUT); @@ -922,9 +929,11 @@ public boolean shouldTrigger(BaseMPMessage message) { isBackgroundAst = (message.getMessageType().equals(Constants.MessageType.APP_STATE_TRANSITION) && message.get(Constants.MessageKey.STATE_TRANSITION_TYPE).equals(Constants.StateTransitionType.STATE_TRANS_BG)); } catch (JSONException ex) { } + if(isBackgroundEventBatchingEnabled && isBackgroundAst){ + return false; + } boolean shouldTrigger = message.getMessageType().equals(Constants.MessageType.PUSH_RECEIVED) - || message.getMessageType().equals(Constants.MessageType.COMMERCE_EVENT) - || isBackgroundAst; + || message.getMessageType().equals(Constants.MessageType.COMMERCE_EVENT) || isBackgroundAst; if (!shouldTrigger && messageMatches != null && messageMatches.length() > 0) { shouldTrigger = true; diff --git a/android-core/src/main/java/com/mparticle/internal/MessageManager.java b/android-core/src/main/java/com/mparticle/internal/MessageManager.java index 8824a7e2e..f71d514bc 100644 --- a/android-core/src/main/java/com/mparticle/internal/MessageManager.java +++ b/android-core/src/main/java/com/mparticle/internal/MessageManager.java @@ -815,8 +815,10 @@ public void onFailed() { @Override public void endUploadLoop() { - mUploadHandler.removeMessages(UploadHandler.UPLOAD_MESSAGES); - MParticle.getInstance().upload(); + if (!mConfigManager.isBackgroundEventBatchingEnabled()) { + mUploadHandler.removeMessages(UploadHandler.UPLOAD_MESSAGES); + MParticle.getInstance().upload(); + } } @Override diff --git a/android-core/src/main/java/com/mparticle/internal/UploadHandler.java b/android-core/src/main/java/com/mparticle/internal/UploadHandler.java index 790c8def4..a7e6aa217 100644 --- a/android-core/src/main/java/com/mparticle/internal/UploadHandler.java +++ b/android-core/src/main/java/com/mparticle/internal/UploadHandler.java @@ -145,8 +145,11 @@ public void handleMessageImpl(Message msg) { } } } - if (mAppStateManager.getSession().isActive() && uploadInterval > 0 && msg.arg1 == 0) { - this.sendEmptyDelayed(UPLOAD_MESSAGES, uploadInterval); + + if (!mConfigManager.isBackgroundEventBatchingEnabled()) { + if (mAppStateManager.getSession().isActive() && uploadInterval > 0 && msg.arg1 == 0) { + this.sendEmptyDelayed(UPLOAD_MESSAGES, uploadInterval); + } } break; case UPLOAD_HISTORY: diff --git a/android-core/src/main/kotlin/com.mparticle/JobSchedulerUtils.kt b/android-core/src/main/kotlin/com.mparticle/JobSchedulerUtils.kt new file mode 100644 index 000000000..cd8b2d7c1 --- /dev/null +++ b/android-core/src/main/kotlin/com.mparticle/JobSchedulerUtils.kt @@ -0,0 +1,46 @@ +package com.mparticle + +import android.app.job.JobInfo +import android.app.job.JobScheduler +import android.content.ComponentName +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import com.mparticle.internal.Logger +import com.mparticle.uploadbatching.BatchUploadingJob + +const val UPLOAD_BATCH_JOB = 123 + +private fun Int.minutesToMillis(): Long = this * 60000L + +fun scheduleBatchUploading(context: Context, legacyAction: (delay: Long) -> Unit) { + val delay = 15.minutesToMillis() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val job = + JobInfo.Builder( + UPLOAD_BATCH_JOB, + ComponentName(context, BatchUploadingJob::class.java.name) + ) + .setMinimumLatency(delay) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + .build() + context.scheduleJob(job) + } else { + legacyAction.invoke(delay) + } +} + + +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +private fun Context.scheduleJob(job: JobInfo) { + val jobScheduler = this.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler? + val jobRunning = jobScheduler?.allPendingJobs?.firstOrNull { it.id == job.id } != null + if (!jobRunning) { + //Schedule / Re-schedule the job if its not running/scheduled to run at the system service + jobScheduler?.schedule(job) + } else { + Logger.debug("Trying to schedule job in uploadBatch service. Service ALREADY RUNNING") + } +} + + diff --git a/android-core/src/main/kotlin/com/mparticle/uploadbatching/BatchUploadingJob.kt b/android-core/src/main/kotlin/com/mparticle/uploadbatching/BatchUploadingJob.kt new file mode 100644 index 000000000..a4be13e05 --- /dev/null +++ b/android-core/src/main/kotlin/com/mparticle/uploadbatching/BatchUploadingJob.kt @@ -0,0 +1,27 @@ +package com.mparticle.uploadbatching + +import android.app.job.JobParameters +import android.app.job.JobService +import android.os.Build +import androidx.annotation.RequiresApi +import com.mparticle.MParticle +import com.mparticle.internal.Logger + +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +class BatchUploadingJob : JobService() { + + // Whenever the contraints are satisfied this will get fired. + override fun onStartJob(params: JobParameters?): Boolean { + Logger.debug("uploadBatching onStart service ") + MParticle.getInstance()?.let { + //Do if there is a non-null mParticle instance, force upload messages + it.upload() + Logger.debug("Triggering event upload on uploadBatching service") + } ?: run { + Logger.debug("MParticle instance null while trying to call uploadBatching:upload") + } + return false + } + + override fun onStopJob(params: JobParameters?): Boolean = false +} \ No newline at end of file From d9733422b830908a4c0edc42a1588d31258393f1 Mon Sep 17 00:00:00 2001 From: markvdouw Date: Thu, 16 Nov 2023 08:53:25 -0300 Subject: [PATCH 3/7] Changes for one shot and periodic update base on Job scheduler. Deprecating function in Media API Minor business logic changes in AppStatemanager Removing featrue flag on ConfigManager for platform consistency --- android-core/src/main/AndroidManifest.xml | 7 +++ .../mparticle/internal/AppStateManager.java | 13 ++-- .../com/mparticle/internal/ConfigManager.java | 11 +--- .../mparticle/internal/MessageManager.java | 6 +- .../com/mparticle/internal/UploadHandler.java | 24 ++++++-- .../java/com/mparticle/media/MPMediaAPI.java | 2 + .../kotlin/com.mparticle/JobSchedulerUtils.kt | 60 +++++++++++++++---- .../uploadbatching/BatchUploadingJob.kt | 2 +- 8 files changed, 86 insertions(+), 39 deletions(-) diff --git a/android-core/src/main/AndroidManifest.xml b/android-core/src/main/AndroidManifest.xml index 9733c3881..ef6f7114e 100644 --- a/android-core/src/main/AndroidManifest.xml +++ b/android-core/src/main/AndroidManifest.xml @@ -1,4 +1,11 @@ + + + + diff --git a/android-core/src/main/java/com/mparticle/internal/AppStateManager.java b/android-core/src/main/java/com/mparticle/internal/AppStateManager.java index e51526c49..296907f4d 100644 --- a/android-core/src/main/java/com/mparticle/internal/AppStateManager.java +++ b/android-core/src/main/java/com/mparticle/internal/AppStateManager.java @@ -17,6 +17,7 @@ import com.mparticle.JobSchedulerUtilsKt; import com.mparticle.MPEvent; import com.mparticle.MParticle; +import com.mparticle.SchedulingBatchingType; import com.mparticle.identity.IdentityApi; import com.mparticle.identity.IdentityApiRequest; import com.mparticle.identity.MParticleUser; @@ -377,13 +378,11 @@ private void logBackgrounded() { } private void scheduleBackgroundJob() { - if (mConfigManager.isBackgroundEventBatchingEnabled()) { - JobSchedulerUtilsKt.scheduleBatchUploading(this.mContext, delay -> { - mMessageManager.mUploadHandler.sendMessageDelayed(mMessageManager.mUploadHandler.obtainMessage(UploadHandler.UPLOAD_TRIGGER_MESSAGES, 1, 0, mConfigManager.getMpid()), delay); - Logger.debug("Legacy action with delay: " + delay); - return Unit.INSTANCE; - }); - } + JobSchedulerUtilsKt.scheduleBatchUploading(this.mContext, mConfigManager.getUploadInterval(), SchedulingBatchingType.ONE_SHOT, true, delay -> { + mMessageManager.mUploadHandler.sendMessageDelayed(mMessageManager.mUploadHandler.obtainMessage(UploadHandler.UPLOAD_TRIGGER_MESSAGES, 1, 0, mConfigManager.getMpid()), delay); + Logger.debug("Legacy action with delay: " + delay); + return Unit.INSTANCE; + }); } @TargetApi(14) diff --git a/android-core/src/main/java/com/mparticle/internal/ConfigManager.java b/android-core/src/main/java/com/mparticle/internal/ConfigManager.java index fbc78b87f..74864db12 100644 --- a/android-core/src/main/java/com/mparticle/internal/ConfigManager.java +++ b/android-core/src/main/java/com/mparticle/internal/ConfigManager.java @@ -91,7 +91,6 @@ public class ConfigManager { private JSONObject mProviderPersistence; private int mRampValue = -1; private int mUserBucket = -1; - private boolean isBackgroundEventBatchingEnabled = false; private int mSessionTimeoutInterval = -1; private int mUploadInterval = -1; @@ -231,10 +230,6 @@ public void deleteUserStorage(long mpId) { deleteUserStorage(mContext, mpId); } - public boolean isBackgroundEventBatchingEnabled() { - return isBackgroundEventBatchingEnabled; - } - static void deleteConfigManager(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { context.deleteSharedPreferences(PREFERENCES_FILE); @@ -413,8 +408,6 @@ private synchronized void updateCoreConfig(JSONObject responseJSON, boolean newC } mRampValue = responseJSON.optInt(KEY_RAMP, -1); - isBackgroundEventBatchingEnabled = responseJSON.optBoolean(ENABLE_BACKGROUND_BATCHING, false); - if (responseJSON.has(KEY_OPT_OUT)) { mSendOoEvents = responseJSON.getBoolean(KEY_OPT_OUT); } else { @@ -929,11 +922,11 @@ public boolean shouldTrigger(BaseMPMessage message) { isBackgroundAst = (message.getMessageType().equals(Constants.MessageType.APP_STATE_TRANSITION) && message.get(Constants.MessageKey.STATE_TRANSITION_TYPE).equals(Constants.StateTransitionType.STATE_TRANS_BG)); } catch (JSONException ex) { } - if(isBackgroundEventBatchingEnabled && isBackgroundAst){ + if (isBackgroundAst) { return false; } boolean shouldTrigger = message.getMessageType().equals(Constants.MessageType.PUSH_RECEIVED) - || message.getMessageType().equals(Constants.MessageType.COMMERCE_EVENT) || isBackgroundAst; + || message.getMessageType().equals(Constants.MessageType.COMMERCE_EVENT); if (!shouldTrigger && messageMatches != null && messageMatches.length() > 0) { shouldTrigger = true; diff --git a/android-core/src/main/java/com/mparticle/internal/MessageManager.java b/android-core/src/main/java/com/mparticle/internal/MessageManager.java index f71d514bc..8824a7e2e 100644 --- a/android-core/src/main/java/com/mparticle/internal/MessageManager.java +++ b/android-core/src/main/java/com/mparticle/internal/MessageManager.java @@ -815,10 +815,8 @@ public void onFailed() { @Override public void endUploadLoop() { - if (!mConfigManager.isBackgroundEventBatchingEnabled()) { - mUploadHandler.removeMessages(UploadHandler.UPLOAD_MESSAGES); - MParticle.getInstance().upload(); - } + mUploadHandler.removeMessages(UploadHandler.UPLOAD_MESSAGES); + MParticle.getInstance().upload(); } @Override diff --git a/android-core/src/main/java/com/mparticle/internal/UploadHandler.java b/android-core/src/main/java/com/mparticle/internal/UploadHandler.java index a7e6aa217..e539dda49 100644 --- a/android-core/src/main/java/com/mparticle/internal/UploadHandler.java +++ b/android-core/src/main/java/com/mparticle/internal/UploadHandler.java @@ -9,7 +9,9 @@ import androidx.annotation.Nullable; +import com.mparticle.JobSchedulerUtilsKt; import com.mparticle.MParticle; +import com.mparticle.SchedulingBatchingType; import com.mparticle.identity.AliasRequest; import com.mparticle.identity.AliasResponse; import com.mparticle.internal.database.services.MParticleDBManager; @@ -25,6 +27,8 @@ import javax.net.ssl.SSLHandshakeException; +import kotlin.Unit; + /** * Primary queue handler which is responsible for querying, packaging, and uploading data. */ @@ -133,10 +137,12 @@ public void handleMessageImpl(Message msg) { break; case UPLOAD_MESSAGES: case UPLOAD_TRIGGER_MESSAGES: + boolean anythingToUpload = false; long uploadInterval = mConfigManager.getUploadInterval(); if (isNetworkConnected) { if (uploadInterval > 0 || msg.arg1 == 1) { while (mParticleDBManager.hasMessagesForUpload()) { + anythingToUpload = true; prepareMessageUploads(false); } boolean needsHistory = upload(false); @@ -145,11 +151,19 @@ public void handleMessageImpl(Message msg) { } } } - - if (!mConfigManager.isBackgroundEventBatchingEnabled()) { - if (mAppStateManager.getSession().isActive() && uploadInterval > 0 && msg.arg1 == 0) { - this.sendEmptyDelayed(UPLOAD_MESSAGES, uploadInterval); - } + if (!anythingToUpload && (!mAppStateManager.getSession().isActive() || mAppStateManager.isBackgrounded())) { + //If there isn't anything to upload and is in background or inactive, cancel schedule job until a new message is stored + JobSchedulerUtilsKt.cancelScheduledUploadBatchJob(mContext); + } + if (mAppStateManager.getSession().isActive() && uploadInterval > 0 && msg.arg1 == 0) { + JobSchedulerUtilsKt.scheduleBatchUploading(mContext, + uploadInterval, + SchedulingBatchingType.PERIODIC, + true, + delay -> { + sendEmptyDelayed(UPLOAD_MESSAGES, uploadInterval); + return Unit.INSTANCE; + }); } break; case UPLOAD_HISTORY: diff --git a/android-core/src/main/java/com/mparticle/media/MPMediaAPI.java b/android-core/src/main/java/com/mparticle/media/MPMediaAPI.java index fdbb0617c..cef2b98bf 100644 --- a/android-core/src/main/java/com/mparticle/media/MPMediaAPI.java +++ b/android-core/src/main/java/com/mparticle/media/MPMediaAPI.java @@ -36,6 +36,7 @@ public MPMediaAPI(@Nullable Context context, @NonNull MediaCallbacks callbacks) * * @param playing Is your app currently playing music for the user. */ + @Deprecated public void setAudioPlaying(boolean playing) { mAudioPlaying.set(playing); if (playing) { @@ -45,6 +46,7 @@ public void setAudioPlaying(boolean playing) { } } + @Deprecated public boolean getAudioPlaying() { return mAudioPlaying.get(); } diff --git a/android-core/src/main/kotlin/com.mparticle/JobSchedulerUtils.kt b/android-core/src/main/kotlin/com.mparticle/JobSchedulerUtils.kt index cd8b2d7c1..499b406e9 100644 --- a/android-core/src/main/kotlin/com.mparticle/JobSchedulerUtils.kt +++ b/android-core/src/main/kotlin/com.mparticle/JobSchedulerUtils.kt @@ -11,33 +11,67 @@ import com.mparticle.uploadbatching.BatchUploadingJob const val UPLOAD_BATCH_JOB = 123 -private fun Int.minutesToMillis(): Long = this * 60000L +enum class SchedulingBatchingType { PERIODIC, ONE_SHOT; } -fun scheduleBatchUploading(context: Context, legacyAction: (delay: Long) -> Unit) { - val delay = 15.minutesToMillis() +fun cancelScheduledUploadBatchJob(context: Context) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val jobScheduler = context.getJobScheduler() + val jobRunning = context.getScheduledJob(UPLOAD_BATCH_JOB) + jobRunning?.let { jobScheduler?.cancel(UPLOAD_BATCH_JOB) } + } + } catch (e: Exception) { + } +} + +fun scheduleBatchUploading( + context: Context, + delayInMillis: Long, + type: SchedulingBatchingType, + cancelPrevious: Boolean = true, + legacyAction: (delay: Long) -> Unit +) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - val job = - JobInfo.Builder( + try { + if (cancelPrevious) { + context.getJobScheduler()?.cancel(UPLOAD_BATCH_JOB) + } + val builder = JobInfo.Builder( UPLOAD_BATCH_JOB, ComponentName(context, BatchUploadingJob::class.java.name) ) - .setMinimumLatency(delay) - .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) - .build() - context.scheduleJob(job) + if (type == SchedulingBatchingType.PERIODIC) { + builder.setPeriodic(delayInMillis) + } else if (type == SchedulingBatchingType.ONE_SHOT) { + builder.setMinimumLatency(delayInMillis) + } + builder.apply { + setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + } + context.scheduleJob(builder.build()) + } catch (e: Exception) { + Logger.warning("Service ${BatchUploadingJob::class.java.name} should be added to the manifest") + legacyAction.invoke(delayInMillis) + } } else { - legacyAction.invoke(delay) + legacyAction.invoke(delayInMillis) } } +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +private fun Context.getJobScheduler(): JobScheduler? = + this.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler? + +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +private fun Context.getScheduledJob(jobId: Int): JobInfo? = + this.getJobScheduler()?.allPendingJobs?.firstOrNull { it.id == jobId } @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun Context.scheduleJob(job: JobInfo) { - val jobScheduler = this.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler? - val jobRunning = jobScheduler?.allPendingJobs?.firstOrNull { it.id == job.id } != null + val jobRunning = this.getScheduledJob(job.id) != null if (!jobRunning) { //Schedule / Re-schedule the job if its not running/scheduled to run at the system service - jobScheduler?.schedule(job) + this.getJobScheduler()?.schedule(job) } else { Logger.debug("Trying to schedule job in uploadBatch service. Service ALREADY RUNNING") } diff --git a/android-core/src/main/kotlin/com/mparticle/uploadbatching/BatchUploadingJob.kt b/android-core/src/main/kotlin/com/mparticle/uploadbatching/BatchUploadingJob.kt index a4be13e05..3f729b1b7 100644 --- a/android-core/src/main/kotlin/com/mparticle/uploadbatching/BatchUploadingJob.kt +++ b/android-core/src/main/kotlin/com/mparticle/uploadbatching/BatchUploadingJob.kt @@ -20,7 +20,7 @@ class BatchUploadingJob : JobService() { } ?: run { Logger.debug("MParticle instance null while trying to call uploadBatching:upload") } - return false + return true } override fun onStopJob(params: JobParameters?): Boolean = false From 426b9179747d86de7f2000eaae42a739992ffc8c Mon Sep 17 00:00:00 2001 From: markvdouw Date: Tue, 5 Dec 2023 20:26:29 -0300 Subject: [PATCH 4/7] Scheduling job onActivityResumed and only stop uploading if: sdk is backgrounded and there are no messages stored pending and the upload table is empty. --- .../mparticle/internal/AppStateManager.java | 4 +- .../com/mparticle/internal/UploadHandler.java | 29 ++++-------- .../kotlin/com.mparticle/JobSchedulerUtils.kt | 46 +++++++++---------- .../uploadbatching/BatchUploadingJob.kt | 16 ++++--- 4 files changed, 45 insertions(+), 50 deletions(-) diff --git a/android-core/src/main/java/com/mparticle/internal/AppStateManager.java b/android-core/src/main/java/com/mparticle/internal/AppStateManager.java index 296907f4d..4602dc2e9 100644 --- a/android-core/src/main/java/com/mparticle/internal/AppStateManager.java +++ b/android-core/src/main/java/com/mparticle/internal/AppStateManager.java @@ -136,6 +136,7 @@ private long getTime() { public void onActivityResumed(Activity activity) { try { + scheduleBackgroundJob(); mCurrentActivityName = AppStateManager.getActivityName(activity); int interruptions = mInterruptionCount.get(); @@ -372,13 +373,12 @@ private void logBackgrounded() { instance.Internal().getKitManager().onApplicationBackground(); mCurrentActivityName = null; Logger.debug("App backgrounded."); - scheduleBackgroundJob(); mInterruptionCount.incrementAndGet(); } } private void scheduleBackgroundJob() { - JobSchedulerUtilsKt.scheduleBatchUploading(this.mContext, mConfigManager.getUploadInterval(), SchedulingBatchingType.ONE_SHOT, true, delay -> { + JobSchedulerUtilsKt.scheduleBatchUploading(this.mContext, mConfigManager.getUploadInterval(), SchedulingBatchingType.ONE_SHOT, delay -> { mMessageManager.mUploadHandler.sendMessageDelayed(mMessageManager.mUploadHandler.obtainMessage(UploadHandler.UPLOAD_TRIGGER_MESSAGES, 1, 0, mConfigManager.getMpid()), delay); Logger.debug("Legacy action with delay: " + delay); return Unit.INSTANCE; diff --git a/android-core/src/main/java/com/mparticle/internal/UploadHandler.java b/android-core/src/main/java/com/mparticle/internal/UploadHandler.java index e539dda49..6789a8e2d 100644 --- a/android-core/src/main/java/com/mparticle/internal/UploadHandler.java +++ b/android-core/src/main/java/com/mparticle/internal/UploadHandler.java @@ -11,7 +11,6 @@ import com.mparticle.JobSchedulerUtilsKt; import com.mparticle.MParticle; -import com.mparticle.SchedulingBatchingType; import com.mparticle.identity.AliasRequest; import com.mparticle.identity.AliasResponse; import com.mparticle.internal.database.services.MParticleDBManager; @@ -27,8 +26,6 @@ import javax.net.ssl.SSLHandshakeException; -import kotlin.Unit; - /** * Primary queue handler which is responsible for querying, packaging, and uploading data. */ @@ -137,12 +134,10 @@ public void handleMessageImpl(Message msg) { break; case UPLOAD_MESSAGES: case UPLOAD_TRIGGER_MESSAGES: - boolean anythingToUpload = false; long uploadInterval = mConfigManager.getUploadInterval(); if (isNetworkConnected) { if (uploadInterval > 0 || msg.arg1 == 1) { while (mParticleDBManager.hasMessagesForUpload()) { - anythingToUpload = true; prepareMessageUploads(false); } boolean needsHistory = upload(false); @@ -151,20 +146,6 @@ public void handleMessageImpl(Message msg) { } } } - if (!anythingToUpload && (!mAppStateManager.getSession().isActive() || mAppStateManager.isBackgrounded())) { - //If there isn't anything to upload and is in background or inactive, cancel schedule job until a new message is stored - JobSchedulerUtilsKt.cancelScheduledUploadBatchJob(mContext); - } - if (mAppStateManager.getSession().isActive() && uploadInterval > 0 && msg.arg1 == 0) { - JobSchedulerUtilsKt.scheduleBatchUploading(mContext, - uploadInterval, - SchedulingBatchingType.PERIODIC, - true, - delay -> { - sendEmptyDelayed(UPLOAD_MESSAGES, uploadInterval); - return Unit.INSTANCE; - }); - } break; case UPLOAD_HISTORY: removeMessage(UPLOAD_HISTORY); @@ -227,6 +208,7 @@ protected void prepareMessageUploads(boolean history) throws Exception { */ protected boolean upload(boolean history) { mParticleDBManager.cleanupUploadMessages(); + boolean uploadFailed = false; boolean processingSessionEnd = false; try { List readyUploads = mParticleDBManager.getReadyUploads(); @@ -256,12 +238,21 @@ protected boolean upload(boolean history) { } } } catch (MParticleApiClientImpl.MPThrottleException e) { + uploadFailed = true; } catch (SSLHandshakeException ssle) { Logger.debug("SSL handshake failed while preparing uploads - possible MITM attack detected."); + uploadFailed = true; } catch (MParticleApiClientImpl.MPConfigException e) { Logger.error("Bad API request - is the correct API key and secret configured?"); + uploadFailed = true; } catch (Exception e) { Logger.error(e, "Error processing batch uploads in mParticle DB."); + uploadFailed = true; + } + if (!uploadFailed && !mParticleDBManager.hasMessagesForUpload() && mAppStateManager.isBackgrounded()) { + //Cancel job scheduler for batch upload if there are no messages stored to create batches and all uploads were successful and the app is backgrounded. + Logger.debug("Cancel batch uploading job"); + JobSchedulerUtilsKt.cancelScheduledUploadBatchJob(mContext); } return processingSessionEnd; } diff --git a/android-core/src/main/kotlin/com.mparticle/JobSchedulerUtils.kt b/android-core/src/main/kotlin/com.mparticle/JobSchedulerUtils.kt index 499b406e9..835a021b6 100644 --- a/android-core/src/main/kotlin/com.mparticle/JobSchedulerUtils.kt +++ b/android-core/src/main/kotlin/com.mparticle/JobSchedulerUtils.kt @@ -28,32 +28,37 @@ fun scheduleBatchUploading( context: Context, delayInMillis: Long, type: SchedulingBatchingType, - cancelPrevious: Boolean = true, legacyAction: (delay: Long) -> Unit ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { try { - if (cancelPrevious) { - context.getJobScheduler()?.cancel(UPLOAD_BATCH_JOB) - } - val builder = JobInfo.Builder( - UPLOAD_BATCH_JOB, - ComponentName(context, BatchUploadingJob::class.java.name) - ) - if (type == SchedulingBatchingType.PERIODIC) { - builder.setPeriodic(delayInMillis) - } else if (type == SchedulingBatchingType.ONE_SHOT) { - builder.setMinimumLatency(delayInMillis) - } - builder.apply { - setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + val jobId = UPLOAD_BATCH_JOB + val jobRunning = context.getScheduledJob(jobId) != null + + if (!jobRunning) { + val builder = JobInfo.Builder( + jobId, + ComponentName(context, BatchUploadingJob::class.java.name) + ) + if (type == SchedulingBatchingType.PERIODIC) { + builder.setPeriodic(delayInMillis) + } else if (type == SchedulingBatchingType.ONE_SHOT) { + builder.setMinimumLatency(delayInMillis) + } + builder.apply { + setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) + } + context.scheduleJob(builder.build()) + Logger.debug("Scheduling batch upload with interval ${delayInMillis/1000} sec") + } else { + Logger.debug("Trying to schedule job in uploadBatch service. Service ALREADY RUNNING") } - context.scheduleJob(builder.build()) } catch (e: Exception) { Logger.warning("Service ${BatchUploadingJob::class.java.name} should be added to the manifest") legacyAction.invoke(delayInMillis) } } else { + Logger.debug("Sending post delayed message for batch uploading") legacyAction.invoke(delayInMillis) } } @@ -68,13 +73,8 @@ private fun Context.getScheduledJob(jobId: Int): JobInfo? = @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun Context.scheduleJob(job: JobInfo) { - val jobRunning = this.getScheduledJob(job.id) != null - if (!jobRunning) { - //Schedule / Re-schedule the job if its not running/scheduled to run at the system service - this.getJobScheduler()?.schedule(job) - } else { - Logger.debug("Trying to schedule job in uploadBatch service. Service ALREADY RUNNING") - } + //Schedule / Re-schedule the job if its not running/scheduled to run at the system service + this.getJobScheduler()?.schedule(job) } diff --git a/android-core/src/main/kotlin/com/mparticle/uploadbatching/BatchUploadingJob.kt b/android-core/src/main/kotlin/com/mparticle/uploadbatching/BatchUploadingJob.kt index 3f729b1b7..356d5f290 100644 --- a/android-core/src/main/kotlin/com/mparticle/uploadbatching/BatchUploadingJob.kt +++ b/android-core/src/main/kotlin/com/mparticle/uploadbatching/BatchUploadingJob.kt @@ -13,12 +13,16 @@ class BatchUploadingJob : JobService() { // Whenever the contraints are satisfied this will get fired. override fun onStartJob(params: JobParameters?): Boolean { Logger.debug("uploadBatching onStart service ") - MParticle.getInstance()?.let { - //Do if there is a non-null mParticle instance, force upload messages - it.upload() - Logger.debug("Triggering event upload on uploadBatching service") - } ?: run { - Logger.debug("MParticle instance null while trying to call uploadBatching:upload") + try { + MParticle.getInstance()?.let { + //Do if there is a non-null mParticle instance, force upload messages + it.upload() + Logger.debug("Triggering event upload on uploadBatching service") + } ?: run { + Logger.debug("MParticle instance null while trying to call uploadBatching:upload") + } + } catch (e: Exception) { + Logger.error("Error while uploading batches with BatchUploadingJob service") } return true } From 1548520f27f9f5333620442354ac54555c533848 Mon Sep 17 00:00:00 2001 From: markvdouw Date: Mon, 11 Dec 2023 07:38:12 -0300 Subject: [PATCH 5/7] Removing JobScheduler in favor of one shot alarme scheduled to send all events after app goes bachgrounded --- android-core/proguard.pro | 2 +- android-core/src/main/AndroidManifest.xml | 12 ++- .../main/java/com/mparticle/MParticle.java | 8 ++ .../mparticle/internal/AppStateManager.java | 15 +--- .../com/mparticle/internal/UploadHandler.java | 14 ++-- .../com.mparticle/AlarmSchedulingUtils.kt | 42 ++++++++++ .../kotlin/com.mparticle/JobSchedulerUtils.kt | 80 ------------------- .../main/kotlin/com.mparticle/TimeUtils.kt | 8 ++ .../com.mparticle/UploadBatchReceiver.kt | 36 +++++++++ 9 files changed, 113 insertions(+), 104 deletions(-) create mode 100644 android-core/src/main/kotlin/com.mparticle/AlarmSchedulingUtils.kt delete mode 100644 android-core/src/main/kotlin/com.mparticle/JobSchedulerUtils.kt create mode 100644 android-core/src/main/kotlin/com.mparticle/TimeUtils.kt create mode 100644 android-core/src/main/kotlin/com.mparticle/UploadBatchReceiver.kt diff --git a/android-core/proguard.pro b/android-core/proguard.pro index 7c0bb4cf6..f2c8940ea 100644 --- a/android-core/proguard.pro +++ b/android-core/proguard.pro @@ -94,7 +94,7 @@ -keep class com.mparticle.MParticle$UserAttributes { *; } -keep class com.mparticle.MParticle$ResetListener { *; } -keep class com.mparticle.MParticle$OperatingSystem { *; } --keep class com.mparticle.uploadbatching.BatchUploadingJob +-keep class com.mparticle.UploadBatchReceiver -keep class com.mparticle.Session { *; } diff --git a/android-core/src/main/AndroidManifest.xml b/android-core/src/main/AndroidManifest.xml index ef6f7114e..d1c57b1ba 100644 --- a/android-core/src/main/AndroidManifest.xml +++ b/android-core/src/main/AndroidManifest.xml @@ -3,9 +3,13 @@ - + + + + + diff --git a/android-core/src/main/java/com/mparticle/MParticle.java b/android-core/src/main/java/com/mparticle/MParticle.java index 726c852d4..bcef69c96 100644 --- a/android-core/src/main/java/com/mparticle/MParticle.java +++ b/android-core/src/main/java/com/mparticle/MParticle.java @@ -5,6 +5,7 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.SharedPreferences; import android.location.Location; import android.location.LocationManager; @@ -136,6 +137,13 @@ private MParticle(MParticleOptions options) { mMessageManager = new MessageManager(configManager, appStateManager, mKitManager, sDevicePerformanceMetricsDisabled, mDatabaseManager, options); mConfigManager.setNetworkOptions(options.getNetworkOptions()); mPreferences = options.getContext().getSharedPreferences(Constants.PREFS_FILE, Context.MODE_PRIVATE); + UploadBatchReceiver receiver = new UploadBatchReceiver(); + IntentFilter intentFilter = UploadBatchReceiver.Companion.getIntentFilter(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + mAppContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED); + } else { + mAppContext.registerReceiver(receiver, intentFilter); + } } /** diff --git a/android-core/src/main/java/com/mparticle/internal/AppStateManager.java b/android-core/src/main/java/com/mparticle/internal/AppStateManager.java index 4602dc2e9..6c4b74db9 100644 --- a/android-core/src/main/java/com/mparticle/internal/AppStateManager.java +++ b/android-core/src/main/java/com/mparticle/internal/AppStateManager.java @@ -14,10 +14,9 @@ import androidx.annotation.Nullable; -import com.mparticle.JobSchedulerUtilsKt; +import com.mparticle.AlarmSchedulingUtilsKt; import com.mparticle.MPEvent; import com.mparticle.MParticle; -import com.mparticle.SchedulingBatchingType; import com.mparticle.identity.IdentityApi; import com.mparticle.identity.IdentityApiRequest; import com.mparticle.identity.MParticleUser; @@ -29,8 +28,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; -import kotlin.Unit; - /** * This class is responsible for maintaining the session state by listening to the Activity lifecycle. @@ -136,7 +133,6 @@ private long getTime() { public void onActivityResumed(Activity activity) { try { - scheduleBackgroundJob(); mCurrentActivityName = AppStateManager.getActivityName(activity); int interruptions = mInterruptionCount.get(); @@ -369,6 +365,7 @@ public void onActivityStopped(Activity activity) { private void logBackgrounded() { MParticle instance = MParticle.getInstance(); if (instance != null) { + AlarmSchedulingUtilsKt.scheduleUploadBatchAlarm(mContext, mConfigManager.getUploadInterval()); logStateTransition(Constants.StateTransitionType.STATE_TRANS_BG, mCurrentActivityName); instance.Internal().getKitManager().onApplicationBackground(); mCurrentActivityName = null; @@ -377,14 +374,6 @@ private void logBackgrounded() { } } - private void scheduleBackgroundJob() { - JobSchedulerUtilsKt.scheduleBatchUploading(this.mContext, mConfigManager.getUploadInterval(), SchedulingBatchingType.ONE_SHOT, delay -> { - mMessageManager.mUploadHandler.sendMessageDelayed(mMessageManager.mUploadHandler.obtainMessage(UploadHandler.UPLOAD_TRIGGER_MESSAGES, 1, 0, mConfigManager.getMpid()), delay); - Logger.debug("Legacy action with delay: " + delay); - return Unit.INSTANCE; - }); - } - @TargetApi(14) private void setupLifecycleCallbacks() { ((Application) mContext).registerActivityLifecycleCallbacks(new MPLifecycleCallbackDelegate(this)); diff --git a/android-core/src/main/java/com/mparticle/internal/UploadHandler.java b/android-core/src/main/java/com/mparticle/internal/UploadHandler.java index 6789a8e2d..23a88d9ee 100644 --- a/android-core/src/main/java/com/mparticle/internal/UploadHandler.java +++ b/android-core/src/main/java/com/mparticle/internal/UploadHandler.java @@ -9,7 +9,6 @@ import androidx.annotation.Nullable; -import com.mparticle.JobSchedulerUtilsKt; import com.mparticle.MParticle; import com.mparticle.identity.AliasRequest; import com.mparticle.identity.AliasResponse; @@ -208,8 +207,8 @@ protected void prepareMessageUploads(boolean history) throws Exception { */ protected boolean upload(boolean history) { mParticleDBManager.cleanupUploadMessages(); - boolean uploadFailed = false; boolean processingSessionEnd = false; + boolean uploadFailed = false; try { List readyUploads = mParticleDBManager.getReadyUploads(); if (readyUploads.size() > 0) { @@ -249,10 +248,13 @@ protected boolean upload(boolean history) { Logger.error(e, "Error processing batch uploads in mParticle DB."); uploadFailed = true; } - if (!uploadFailed && !mParticleDBManager.hasMessagesForUpload() && mAppStateManager.isBackgrounded()) { - //Cancel job scheduler for batch upload if there are no messages stored to create batches and all uploads were successful and the app is backgrounded. - Logger.debug("Cancel batch uploading job"); - JobSchedulerUtilsKt.cancelScheduledUploadBatchJob(mContext); + boolean activeNotBackgrounded = !mAppStateManager.isBackgrounded() && mAppStateManager.getSession().isActive(); + boolean backgroundedWithPendingMessages = mAppStateManager.isBackgrounded() && (uploadFailed || mParticleDBManager.hasMessagesForUpload()); + if (mConfigManager.getUploadInterval() > 0 && (activeNotBackgrounded || backgroundedWithPendingMessages)) { + Logger.debug("Upload scheduled with message in interval due to activeNotBackgrounded: " + activeNotBackgrounded + " and/or backgroundedWithPendingMessages: " + backgroundedWithPendingMessages); + this.sendEmptyDelayed(UPLOAD_MESSAGES, mConfigManager.getUploadInterval()); + } else { + Logger.debug("No uploads are being schedules do to activeNotBackgrounded: " + activeNotBackgrounded + " and/or backgroundedWithPendingMessages: " + backgroundedWithPendingMessages); } return processingSessionEnd; } diff --git a/android-core/src/main/kotlin/com.mparticle/AlarmSchedulingUtils.kt b/android-core/src/main/kotlin/com.mparticle/AlarmSchedulingUtils.kt new file mode 100644 index 000000000..5988d9bf3 --- /dev/null +++ b/android-core/src/main/kotlin/com.mparticle/AlarmSchedulingUtils.kt @@ -0,0 +1,42 @@ +package com.mparticle + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import com.mparticle.internal.Logger + +fun scheduleUploadBatchAlarm(context: Context, delay: Long) { + val intent = Intent(context, UploadBatchReceiver::class.java).apply { + action = UploadBatchReceiver.ACTION_UPLOAD_BATCH + } + val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + + val time = System.currentTimeMillis() + delay + val alarmType = AlarmManager.RTC_WAKEUP + (context.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { manager -> + + PendingIntent.getService( + context, + 0, + intent, + PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE + )?.let { + manager.cancel(it) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + manager.setAndAllowWhileIdle(alarmType, time, pendingIntent) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + manager.setAndAllowWhileIdle(alarmType, time, pendingIntent) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + manager.set(alarmType, time, pendingIntent) + } else { + manager.set(alarmType, time, pendingIntent) + } + } + Logger.debug("Upload batch alarm set at ${millisToLoggingDate(time)}") +} + + + diff --git a/android-core/src/main/kotlin/com.mparticle/JobSchedulerUtils.kt b/android-core/src/main/kotlin/com.mparticle/JobSchedulerUtils.kt deleted file mode 100644 index 835a021b6..000000000 --- a/android-core/src/main/kotlin/com.mparticle/JobSchedulerUtils.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.mparticle - -import android.app.job.JobInfo -import android.app.job.JobScheduler -import android.content.ComponentName -import android.content.Context -import android.os.Build -import androidx.annotation.RequiresApi -import com.mparticle.internal.Logger -import com.mparticle.uploadbatching.BatchUploadingJob - -const val UPLOAD_BATCH_JOB = 123 - -enum class SchedulingBatchingType { PERIODIC, ONE_SHOT; } - -fun cancelScheduledUploadBatchJob(context: Context) { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - val jobScheduler = context.getJobScheduler() - val jobRunning = context.getScheduledJob(UPLOAD_BATCH_JOB) - jobRunning?.let { jobScheduler?.cancel(UPLOAD_BATCH_JOB) } - } - } catch (e: Exception) { - } -} - -fun scheduleBatchUploading( - context: Context, - delayInMillis: Long, - type: SchedulingBatchingType, - legacyAction: (delay: Long) -> Unit -) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - try { - val jobId = UPLOAD_BATCH_JOB - val jobRunning = context.getScheduledJob(jobId) != null - - if (!jobRunning) { - val builder = JobInfo.Builder( - jobId, - ComponentName(context, BatchUploadingJob::class.java.name) - ) - if (type == SchedulingBatchingType.PERIODIC) { - builder.setPeriodic(delayInMillis) - } else if (type == SchedulingBatchingType.ONE_SHOT) { - builder.setMinimumLatency(delayInMillis) - } - builder.apply { - setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) - } - context.scheduleJob(builder.build()) - Logger.debug("Scheduling batch upload with interval ${delayInMillis/1000} sec") - } else { - Logger.debug("Trying to schedule job in uploadBatch service. Service ALREADY RUNNING") - } - } catch (e: Exception) { - Logger.warning("Service ${BatchUploadingJob::class.java.name} should be added to the manifest") - legacyAction.invoke(delayInMillis) - } - } else { - Logger.debug("Sending post delayed message for batch uploading") - legacyAction.invoke(delayInMillis) - } -} - -@RequiresApi(Build.VERSION_CODES.LOLLIPOP) -private fun Context.getJobScheduler(): JobScheduler? = - this.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler? - -@RequiresApi(Build.VERSION_CODES.LOLLIPOP) -private fun Context.getScheduledJob(jobId: Int): JobInfo? = - this.getJobScheduler()?.allPendingJobs?.firstOrNull { it.id == jobId } - -@RequiresApi(Build.VERSION_CODES.LOLLIPOP) -private fun Context.scheduleJob(job: JobInfo) { - //Schedule / Re-schedule the job if its not running/scheduled to run at the system service - this.getJobScheduler()?.schedule(job) -} - - diff --git a/android-core/src/main/kotlin/com.mparticle/TimeUtils.kt b/android-core/src/main/kotlin/com.mparticle/TimeUtils.kt new file mode 100644 index 000000000..c45933612 --- /dev/null +++ b/android-core/src/main/kotlin/com.mparticle/TimeUtils.kt @@ -0,0 +1,8 @@ +package com.mparticle + +import java.text.SimpleDateFormat +import java.util.Date + +fun millisToLoggingDate(millis: Long): String { + return SimpleDateFormat("MM/dd/yyyy HH:mm:ss").format(Date(millis)) +} \ No newline at end of file diff --git a/android-core/src/main/kotlin/com.mparticle/UploadBatchReceiver.kt b/android-core/src/main/kotlin/com.mparticle/UploadBatchReceiver.kt new file mode 100644 index 000000000..2dc12fd92 --- /dev/null +++ b/android-core/src/main/kotlin/com.mparticle/UploadBatchReceiver.kt @@ -0,0 +1,36 @@ +package com.mparticle + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import com.mparticle.internal.Logger + +internal class UploadBatchReceiver : BroadcastReceiver() { + + companion object { + const val ACTION_UPLOAD_BATCH = "ACTION_UPLOAD_BATCH" + fun getIntentFilter(): IntentFilter { + return IntentFilter().apply { addAction(ACTION_UPLOAD_BATCH) } + } + } + + override fun onReceive(context: Context?, intent: Intent?) { + Logger.debug("Received broadcast ${this::class.java.name}") + intent?.let { + if (it.action == ACTION_UPLOAD_BATCH) { + try { + MParticle.getInstance()?.let { + //Do if there is a non-null mParticle instance, force upload messages + it.upload() + Logger.debug("Uploading events in upload batch receiver") + } ?: run { + Logger.debug("Batches cant be uploaded in receiver because MParticle instance is null") + } + } catch (e: Exception) { + Logger.error("Error while uploading batches in upload batch reveiver") + } + } + } + } +} \ No newline at end of file From eb40eaecd9bf9e31fdb5f9388ef3e07225036619 Mon Sep 17 00:00:00 2001 From: markvdouw Date: Thu, 21 Dec 2023 11:31:40 -0300 Subject: [PATCH 6/7] Adding feature flag with default value false Setting min alarm delay to 2mins Adding logic to handle both background event batching and current behavior Changing package receiver package name Removing comments --- android-core/proguard.pro | 2 +- android-core/src/main/AndroidManifest.xml | 2 +- .../main/java/com/mparticle/MParticle.java | 1 + .../mparticle/internal/AppStateManager.java | 6 ++-- .../com/mparticle/internal/ConfigManager.java | 14 +++++++-- .../com/mparticle/internal/UploadHandler.java | 17 +++------- .../uploadbatching}/AlarmSchedulingUtils.kt | 11 +++++-- .../uploadbatching/BatchUploadingJob.kt | 31 ------------------- .../uploadbatching}/UploadBatchReceiver.kt | 6 ++-- 9 files changed, 33 insertions(+), 57 deletions(-) rename android-core/src/main/kotlin/{com.mparticle => com/mparticle/uploadbatching}/AlarmSchedulingUtils.kt (81%) delete mode 100644 android-core/src/main/kotlin/com/mparticle/uploadbatching/BatchUploadingJob.kt rename android-core/src/main/kotlin/{com.mparticle => com/mparticle/uploadbatching}/UploadBatchReceiver.kt (80%) diff --git a/android-core/proguard.pro b/android-core/proguard.pro index f2c8940ea..6651d38c2 100644 --- a/android-core/proguard.pro +++ b/android-core/proguard.pro @@ -94,7 +94,7 @@ -keep class com.mparticle.MParticle$UserAttributes { *; } -keep class com.mparticle.MParticle$ResetListener { *; } -keep class com.mparticle.MParticle$OperatingSystem { *; } --keep class com.mparticle.UploadBatchReceiver +-keep class com.mparticle.uploadbatching.UploadBatchReceiver -keep class com.mparticle.Session { *; } diff --git a/android-core/src/main/AndroidManifest.xml b/android-core/src/main/AndroidManifest.xml index d1c57b1ba..8c3b9358b 100644 --- a/android-core/src/main/AndroidManifest.xml +++ b/android-core/src/main/AndroidManifest.xml @@ -4,7 +4,7 @@ diff --git a/android-core/src/main/java/com/mparticle/MParticle.java b/android-core/src/main/java/com/mparticle/MParticle.java index bcef69c96..53ab9ce23 100644 --- a/android-core/src/main/java/com/mparticle/MParticle.java +++ b/android-core/src/main/java/com/mparticle/MParticle.java @@ -52,6 +52,7 @@ import com.mparticle.messaging.MPMessagingAPI; import com.mparticle.messaging.ProviderCloudMessage; import com.mparticle.segmentation.SegmentListener; +import com.mparticle.uploadbatching.UploadBatchReceiver; import org.jetbrains.annotations.NotNull; import org.json.JSONObject; diff --git a/android-core/src/main/java/com/mparticle/internal/AppStateManager.java b/android-core/src/main/java/com/mparticle/internal/AppStateManager.java index 6c4b74db9..c565f1b0d 100644 --- a/android-core/src/main/java/com/mparticle/internal/AppStateManager.java +++ b/android-core/src/main/java/com/mparticle/internal/AppStateManager.java @@ -14,13 +14,13 @@ import androidx.annotation.Nullable; -import com.mparticle.AlarmSchedulingUtilsKt; import com.mparticle.MPEvent; import com.mparticle.MParticle; import com.mparticle.identity.IdentityApi; import com.mparticle.identity.IdentityApiRequest; import com.mparticle.identity.MParticleUser; import com.mparticle.internal.listeners.InternalListenerManager; +import com.mparticle.uploadbatching.AlarmSchedulingUtilsKt; import org.json.JSONObject; @@ -365,7 +365,9 @@ public void onActivityStopped(Activity activity) { private void logBackgrounded() { MParticle instance = MParticle.getInstance(); if (instance != null) { - AlarmSchedulingUtilsKt.scheduleUploadBatchAlarm(mContext, mConfigManager.getUploadInterval()); + if (mConfigManager.isBackgroundBatchUploadingEnabled()) { + AlarmSchedulingUtilsKt.scheduleUploadBatchAlarm(mContext, mConfigManager.getUploadInterval()); + } logStateTransition(Constants.StateTransitionType.STATE_TRANS_BG, mCurrentActivityName); instance.Internal().getKitManager().onApplicationBackground(); mCurrentActivityName = null; diff --git a/android-core/src/main/java/com/mparticle/internal/ConfigManager.java b/android-core/src/main/java/com/mparticle/internal/ConfigManager.java index 74864db12..a7283cbb0 100644 --- a/android-core/src/main/java/com/mparticle/internal/ConfigManager.java +++ b/android-core/src/main/java/com/mparticle/internal/ConfigManager.java @@ -108,6 +108,7 @@ public class ConfigManager { public static final int DEFAULT_UPLOAD_INTERVAL = 600; private List configUpdatedListeners = new ArrayList<>(); private List sideloadedKits = new ArrayList<>(); + private boolean enableBackgroundBatchingUpload = false; private ConfigManager() { super(); @@ -402,6 +403,11 @@ private synchronized void updateCoreConfig(JSONObject responseJSON, boolean newC mLogUnhandledExceptions = responseJSON.getString(KEY_UNHANDLED_EXCEPTIONS); } + if (responseJSON.has(ENABLE_BACKGROUND_BATCHING)) { + enableBackgroundBatchingUpload = responseJSON.getBoolean(ENABLE_BACKGROUND_BATCHING); + } + editor.putBoolean(ENABLE_BACKGROUND_BATCHING, enableBackgroundBatchingUpload); + if (responseJSON.has(KEY_PUSH_MESSAGES) && newConfig) { sPushKeys = responseJSON.getJSONArray(KEY_PUSH_MESSAGES); editor.putString(KEY_PUSH_MESSAGES, sPushKeys.toString()); @@ -913,6 +919,10 @@ public JSONArray getTriggerMessageHashes() { return mTriggerMessageHashes; } + public boolean isBackgroundBatchUploadingEnabled() { + return enableBackgroundBatchingUpload; + } + public boolean shouldTrigger(BaseMPMessage message) { JSONArray messageMatches = getTriggerMessageMatches(); JSONArray triggerHashes = getTriggerMessageHashes(); @@ -922,11 +932,11 @@ public boolean shouldTrigger(BaseMPMessage message) { isBackgroundAst = (message.getMessageType().equals(Constants.MessageType.APP_STATE_TRANSITION) && message.get(Constants.MessageKey.STATE_TRANSITION_TYPE).equals(Constants.StateTransitionType.STATE_TRANS_BG)); } catch (JSONException ex) { } - if (isBackgroundAst) { + if (enableBackgroundBatchingUpload && isBackgroundAst) { return false; } boolean shouldTrigger = message.getMessageType().equals(Constants.MessageType.PUSH_RECEIVED) - || message.getMessageType().equals(Constants.MessageType.COMMERCE_EVENT); + || message.getMessageType().equals(Constants.MessageType.COMMERCE_EVENT) || isBackgroundAst; if (!shouldTrigger && messageMatches != null && messageMatches.length() > 0) { shouldTrigger = true; diff --git a/android-core/src/main/java/com/mparticle/internal/UploadHandler.java b/android-core/src/main/java/com/mparticle/internal/UploadHandler.java index 23a88d9ee..6f3798de2 100644 --- a/android-core/src/main/java/com/mparticle/internal/UploadHandler.java +++ b/android-core/src/main/java/com/mparticle/internal/UploadHandler.java @@ -145,6 +145,10 @@ public void handleMessageImpl(Message msg) { } } } + if ((mAppStateManager.getSession().isActive() && uploadInterval > 0 && msg.arg1 == 0) || + (mParticleDBManager.hasMessagesForUpload() && mAppStateManager.isBackgrounded())) { + this.sendEmptyDelayed(UPLOAD_MESSAGES, uploadInterval); + } break; case UPLOAD_HISTORY: removeMessage(UPLOAD_HISTORY); @@ -208,7 +212,6 @@ protected void prepareMessageUploads(boolean history) throws Exception { protected boolean upload(boolean history) { mParticleDBManager.cleanupUploadMessages(); boolean processingSessionEnd = false; - boolean uploadFailed = false; try { List readyUploads = mParticleDBManager.getReadyUploads(); if (readyUploads.size() > 0) { @@ -237,24 +240,12 @@ protected boolean upload(boolean history) { } } } catch (MParticleApiClientImpl.MPThrottleException e) { - uploadFailed = true; } catch (SSLHandshakeException ssle) { Logger.debug("SSL handshake failed while preparing uploads - possible MITM attack detected."); - uploadFailed = true; } catch (MParticleApiClientImpl.MPConfigException e) { Logger.error("Bad API request - is the correct API key and secret configured?"); - uploadFailed = true; } catch (Exception e) { Logger.error(e, "Error processing batch uploads in mParticle DB."); - uploadFailed = true; - } - boolean activeNotBackgrounded = !mAppStateManager.isBackgrounded() && mAppStateManager.getSession().isActive(); - boolean backgroundedWithPendingMessages = mAppStateManager.isBackgrounded() && (uploadFailed || mParticleDBManager.hasMessagesForUpload()); - if (mConfigManager.getUploadInterval() > 0 && (activeNotBackgrounded || backgroundedWithPendingMessages)) { - Logger.debug("Upload scheduled with message in interval due to activeNotBackgrounded: " + activeNotBackgrounded + " and/or backgroundedWithPendingMessages: " + backgroundedWithPendingMessages); - this.sendEmptyDelayed(UPLOAD_MESSAGES, mConfigManager.getUploadInterval()); - } else { - Logger.debug("No uploads are being schedules do to activeNotBackgrounded: " + activeNotBackgrounded + " and/or backgroundedWithPendingMessages: " + backgroundedWithPendingMessages); } return processingSessionEnd; } diff --git a/android-core/src/main/kotlin/com.mparticle/AlarmSchedulingUtils.kt b/android-core/src/main/kotlin/com/mparticle/uploadbatching/AlarmSchedulingUtils.kt similarity index 81% rename from android-core/src/main/kotlin/com.mparticle/AlarmSchedulingUtils.kt rename to android-core/src/main/kotlin/com/mparticle/uploadbatching/AlarmSchedulingUtils.kt index 5988d9bf3..537c35ef5 100644 --- a/android-core/src/main/kotlin/com.mparticle/AlarmSchedulingUtils.kt +++ b/android-core/src/main/kotlin/com/mparticle/uploadbatching/AlarmSchedulingUtils.kt @@ -1,4 +1,4 @@ -package com.mparticle +package com.mparticle.uploadbatching import android.app.AlarmManager import android.app.PendingIntent @@ -6,14 +6,19 @@ import android.content.Context import android.content.Intent import android.os.Build import com.mparticle.internal.Logger +import com.mparticle.millisToLoggingDate fun scheduleUploadBatchAlarm(context: Context, delay: Long) { val intent = Intent(context, UploadBatchReceiver::class.java).apply { action = UploadBatchReceiver.ACTION_UPLOAD_BATCH } val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) - - val time = System.currentTimeMillis() + delay + var alarmDelay = delay + //Setting alarm delay to 2min MINIMUM to prevent collision with end session message triggers + if (delay < 120000) { + alarmDelay = 120000L + } + val time = System.currentTimeMillis() + alarmDelay val alarmType = AlarmManager.RTC_WAKEUP (context.getSystemService(Context.ALARM_SERVICE) as AlarmManager?)?.let { manager -> diff --git a/android-core/src/main/kotlin/com/mparticle/uploadbatching/BatchUploadingJob.kt b/android-core/src/main/kotlin/com/mparticle/uploadbatching/BatchUploadingJob.kt deleted file mode 100644 index 356d5f290..000000000 --- a/android-core/src/main/kotlin/com/mparticle/uploadbatching/BatchUploadingJob.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.mparticle.uploadbatching - -import android.app.job.JobParameters -import android.app.job.JobService -import android.os.Build -import androidx.annotation.RequiresApi -import com.mparticle.MParticle -import com.mparticle.internal.Logger - -@RequiresApi(Build.VERSION_CODES.LOLLIPOP) -class BatchUploadingJob : JobService() { - - // Whenever the contraints are satisfied this will get fired. - override fun onStartJob(params: JobParameters?): Boolean { - Logger.debug("uploadBatching onStart service ") - try { - MParticle.getInstance()?.let { - //Do if there is a non-null mParticle instance, force upload messages - it.upload() - Logger.debug("Triggering event upload on uploadBatching service") - } ?: run { - Logger.debug("MParticle instance null while trying to call uploadBatching:upload") - } - } catch (e: Exception) { - Logger.error("Error while uploading batches with BatchUploadingJob service") - } - return true - } - - override fun onStopJob(params: JobParameters?): Boolean = false -} \ No newline at end of file diff --git a/android-core/src/main/kotlin/com.mparticle/UploadBatchReceiver.kt b/android-core/src/main/kotlin/com/mparticle/uploadbatching/UploadBatchReceiver.kt similarity index 80% rename from android-core/src/main/kotlin/com.mparticle/UploadBatchReceiver.kt rename to android-core/src/main/kotlin/com/mparticle/uploadbatching/UploadBatchReceiver.kt index 2dc12fd92..972a08691 100644 --- a/android-core/src/main/kotlin/com.mparticle/UploadBatchReceiver.kt +++ b/android-core/src/main/kotlin/com/mparticle/uploadbatching/UploadBatchReceiver.kt @@ -1,9 +1,10 @@ -package com.mparticle +package com.mparticle.uploadbatching import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import com.mparticle.MParticle import com.mparticle.internal.Logger internal class UploadBatchReceiver : BroadcastReceiver() { @@ -24,11 +25,8 @@ internal class UploadBatchReceiver : BroadcastReceiver() { //Do if there is a non-null mParticle instance, force upload messages it.upload() Logger.debug("Uploading events in upload batch receiver") - } ?: run { - Logger.debug("Batches cant be uploaded in receiver because MParticle instance is null") } } catch (e: Exception) { - Logger.error("Error while uploading batches in upload batch reveiver") } } } From f52c441f880d4a1420f7f76aa68f2c1fe6bcbf2a Mon Sep 17 00:00:00 2001 From: markvdouw Date: Thu, 18 Jan 2024 10:04:28 -0300 Subject: [PATCH 7/7] Removing the place where reading the feature flag from --- .../src/main/java/com/mparticle/internal/ConfigManager.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/android-core/src/main/java/com/mparticle/internal/ConfigManager.java b/android-core/src/main/java/com/mparticle/internal/ConfigManager.java index a7283cbb0..b2b7c0f7a 100644 --- a/android-core/src/main/java/com/mparticle/internal/ConfigManager.java +++ b/android-core/src/main/java/com/mparticle/internal/ConfigManager.java @@ -403,9 +403,7 @@ private synchronized void updateCoreConfig(JSONObject responseJSON, boolean newC mLogUnhandledExceptions = responseJSON.getString(KEY_UNHANDLED_EXCEPTIONS); } - if (responseJSON.has(ENABLE_BACKGROUND_BATCHING)) { - enableBackgroundBatchingUpload = responseJSON.getBoolean(ENABLE_BACKGROUND_BATCHING); - } + //TODO Read from backgroundEventBatching feature flag editor.putBoolean(ENABLE_BACKGROUND_BATCHING, enableBackgroundBatchingUpload); if (responseJSON.has(KEY_PUSH_MESSAGES) && newConfig) {