diff --git a/android-core/proguard.pro b/android-core/proguard.pro index f55779de1..6651d38c2 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.UploadBatchReceiver -keep class com.mparticle.Session { *; } diff --git a/android-core/src/main/AndroidManifest.xml b/android-core/src/main/AndroidManifest.xml index 9733c3881..8c3b9358b 100644 --- a/android-core/src/main/AndroidManifest.xml +++ b/android-core/src/main/AndroidManifest.xml @@ -1,4 +1,15 @@ + + + + + + + + diff --git a/android-core/src/main/java/com/mparticle/MParticle.java b/android-core/src/main/java/com/mparticle/MParticle.java index 726c852d4..53ab9ce23 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; @@ -51,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; @@ -136,6 +138,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 9d76d16a8..c565f1b0d 100644 --- a/android-core/src/main/java/com/mparticle/internal/AppStateManager.java +++ b/android-core/src/main/java/com/mparticle/internal/AppStateManager.java @@ -20,6 +20,7 @@ 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; @@ -364,6 +365,9 @@ public void onActivityStopped(Activity activity) { private void logBackgrounded() { MParticle instance = MParticle.getInstance(); if (instance != null) { + 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 cf225f843..b2b7c0f7a 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"; @@ -107,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(); @@ -401,13 +403,15 @@ private synchronized void updateCoreConfig(JSONObject responseJSON, boolean newC mLogUnhandledExceptions = responseJSON.getString(KEY_UNHANDLED_EXCEPTIONS); } + //TODO Read from backgroundEventBatching feature flag + 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()); } mRampValue = responseJSON.optInt(KEY_RAMP, -1); - if (responseJSON.has(KEY_OPT_OUT)) { mSendOoEvents = responseJSON.getBoolean(KEY_OPT_OUT); } else { @@ -913,6 +917,10 @@ public JSONArray getTriggerMessageHashes() { return mTriggerMessageHashes; } + public boolean isBackgroundBatchUploadingEnabled() { + return enableBackgroundBatchingUpload; + } + public boolean shouldTrigger(BaseMPMessage message) { JSONArray messageMatches = getTriggerMessageMatches(); JSONArray triggerHashes = getTriggerMessageHashes(); @@ -922,9 +930,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 (enableBackgroundBatchingUpload && 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/UploadHandler.java b/android-core/src/main/java/com/mparticle/internal/UploadHandler.java index 790c8def4..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,7 +145,8 @@ public void handleMessageImpl(Message msg) { } } } - if (mAppStateManager.getSession().isActive() && uploadInterval > 0 && msg.arg1 == 0) { + if ((mAppStateManager.getSession().isActive() && uploadInterval > 0 && msg.arg1 == 0) || + (mParticleDBManager.hasMessagesForUpload() && mAppStateManager.isBackgrounded())) { this.sendEmptyDelayed(UPLOAD_MESSAGES, uploadInterval); } break; 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/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/uploadbatching/AlarmSchedulingUtils.kt b/android-core/src/main/kotlin/com/mparticle/uploadbatching/AlarmSchedulingUtils.kt new file mode 100644 index 000000000..537c35ef5 --- /dev/null +++ b/android-core/src/main/kotlin/com/mparticle/uploadbatching/AlarmSchedulingUtils.kt @@ -0,0 +1,47 @@ +package com.mparticle.uploadbatching + +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 +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) + 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 -> + + 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/uploadbatching/UploadBatchReceiver.kt b/android-core/src/main/kotlin/com/mparticle/uploadbatching/UploadBatchReceiver.kt new file mode 100644 index 000000000..972a08691 --- /dev/null +++ b/android-core/src/main/kotlin/com/mparticle/uploadbatching/UploadBatchReceiver.kt @@ -0,0 +1,34 @@ +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() { + + 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") + } + } catch (e: Exception) { + } + } + } + } +} \ No newline at end of file