diff --git a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.java b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.java
index 7c895aebf1..bd77833395 100644
--- a/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.java
+++ b/Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.java
@@ -41,7 +41,7 @@ public void onCreate() {
OSNotification notification = notificationReceivedEvent.getNotification();
JSONObject data = notification.getAdditionalData();
- notificationReceivedEvent.complete(null);
+ notificationReceivedEvent.complete(notification);
});
OneSignal.unsubscribeWhenNotificationsAreDisabled(true);
diff --git a/OneSignalSDK/onesignal/src/main/AndroidManifest.xml b/OneSignalSDK/onesignal/src/main/AndroidManifest.xml
index ed445b01d1..8cdd87ecc0 100644
--- a/OneSignalSDK/onesignal/src/main/AndroidManifest.xml
+++ b/OneSignalSDK/onesignal/src/main/AndroidManifest.xml
@@ -135,6 +135,7 @@
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotification.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotification.java
index bc11f1e4ea..aa7ea676a8 100644
--- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotification.java
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotification.java
@@ -127,25 +127,10 @@ private static CharSequence getTitle(JSONObject fcmJson) {
return currentContext.getPackageManager().getApplicationLabel(currentContext.getApplicationInfo());
}
-
- /**
- * Notification delete is processed by Broadcast Receiver to avoid creation of activities that can end
- * on weird UI interaction
- */
- private static PendingIntent getNewActionPendingIntent(int requestCode, Intent intent) {
- return PendingIntent.getActivity(currentContext, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- }
-
private static PendingIntent getNewDismissActionPendingIntent(int requestCode, Intent intent) {
return PendingIntent.getBroadcast(currentContext, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
- private static Intent getNewBaseIntent(int notificationId) {
- return new Intent(currentContext, notificationOpenedClass)
- .putExtra(BUNDLE_KEY_ANDROID_NOTIFICATION_ID, notificationId)
- .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
- }
-
private static Intent getNewBaseDismissIntent(int notificationId) {
return new Intent(currentContext, notificationDismissedClass)
.putExtra(BUNDLE_KEY_ANDROID_NOTIFICATION_ID, notificationId)
@@ -274,6 +259,11 @@ private static boolean showNotification(OSNotificationGenerationJob notification
JSONObject fcmJson = notificationJob.getJsonPayload();
String group = fcmJson.optString("grp", null);
+ GenerateNotificationOpenIntent intentGenerator = GenerateNotificationOpenIntentFromPushPayload.INSTANCE.create(
+ currentContext,
+ fcmJson
+ );
+
ArrayList grouplessNotifs = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
/* Android 7.0 auto groups 4 or more notifications so we find these groupless active
@@ -289,7 +279,13 @@ private static boolean showNotification(OSNotificationGenerationJob notification
OneSignalNotificationBuilder oneSignalNotificationBuilder = getBaseOneSignalNotificationBuilder(notificationJob);
NotificationCompat.Builder notifBuilder = oneSignalNotificationBuilder.compatBuilder;
- addNotificationActionButtons(fcmJson, notifBuilder, notificationId, null);
+ addNotificationActionButtons(
+ fcmJson,
+ intentGenerator,
+ notifBuilder,
+ notificationId,
+ null
+ );
try {
addBackgroundImage(fcmJson, notifBuilder);
@@ -310,17 +306,33 @@ private static boolean showNotification(OSNotificationGenerationJob notification
Notification notification;
if (group != null) {
- createGenericPendingIntentsForGroup(notifBuilder, fcmJson, group, notificationId);
+ createGenericPendingIntentsForGroup(
+ notifBuilder,
+ intentGenerator,
+ fcmJson,
+ group,
+ notificationId
+ );
notification = createSingleNotificationBeforeSummaryBuilder(notificationJob, notifBuilder);
// Create PendingIntents for notifications in a groupless or defined summary
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
- group.equals(OneSignalNotificationManager.getGrouplessSummaryKey()))
- createGrouplessSummaryNotification(notificationJob, grouplessNotifs.size() + 1);
+ group.equals(OneSignalNotificationManager.getGrouplessSummaryKey())) {
+ createGrouplessSummaryNotification(
+ notificationJob,
+ intentGenerator,
+ grouplessNotifs.size() + 1
+ );
+ }
else
createSummaryNotification(notificationJob, oneSignalNotificationBuilder);
} else {
- notification = createGenericPendingIntentsForNotif(notifBuilder, fcmJson, notificationId);
+ notification = createGenericPendingIntentsForNotif(
+ notifBuilder,
+ intentGenerator,
+ fcmJson,
+ notificationId
+ );
}
// NotificationManagerCompat does not auto omit the individual notification on the device when using
// stacked notifications on Android 4.2 and older
@@ -338,18 +350,35 @@ private static boolean showNotification(OSNotificationGenerationJob notification
return true;
}
- private static Notification createGenericPendingIntentsForNotif(NotificationCompat.Builder notifBuilder, JSONObject gcmBundle, int notificationId) {
+ private static Notification createGenericPendingIntentsForNotif(
+ NotificationCompat.Builder notifBuilder,
+ GenerateNotificationOpenIntent intentGenerator,
+ JSONObject gcmBundle,
+ int notificationId
+ ) {
Random random = new SecureRandom();
- PendingIntent contentIntent = getNewActionPendingIntent(random.nextInt(), getNewBaseIntent(notificationId).putExtra(BUNDLE_KEY_ONESIGNAL_DATA, gcmBundle.toString()));
+ PendingIntent contentIntent = intentGenerator.getNewActionPendingIntent(
+ random.nextInt(),
+ intentGenerator.getNewBaseIntent(notificationId).putExtra(BUNDLE_KEY_ONESIGNAL_DATA, gcmBundle.toString())
+ );
notifBuilder.setContentIntent(contentIntent);
PendingIntent deleteIntent = getNewDismissActionPendingIntent(random.nextInt(), getNewBaseDismissIntent(notificationId));
notifBuilder.setDeleteIntent(deleteIntent);
return notifBuilder.build();
}
- private static void createGenericPendingIntentsForGroup(NotificationCompat.Builder notifBuilder, JSONObject gcmBundle, String group, int notificationId) {
+ private static void createGenericPendingIntentsForGroup(
+ NotificationCompat.Builder notifBuilder,
+ GenerateNotificationOpenIntent intentGenerator,
+ JSONObject gcmBundle,
+ String group,
+ int notificationId
+ ) {
Random random = new SecureRandom();
- PendingIntent contentIntent = getNewActionPendingIntent(random.nextInt(), getNewBaseIntent(notificationId).putExtra(BUNDLE_KEY_ONESIGNAL_DATA, gcmBundle.toString()).putExtra("grp", group));
+ PendingIntent contentIntent = intentGenerator.getNewActionPendingIntent(
+ random.nextInt(),
+ intentGenerator.getNewBaseIntent(notificationId).putExtra(BUNDLE_KEY_ONESIGNAL_DATA, gcmBundle.toString()).putExtra("grp", group)
+ );
notifBuilder.setContentIntent(contentIntent);
PendingIntent deleteIntent = getNewDismissActionPendingIntent(random.nextInt(), getNewBaseDismissIntent(notificationId).putExtra("grp", group));
notifBuilder.setDeleteIntent(deleteIntent);
@@ -450,6 +479,10 @@ static void updateSummaryNotification(OSNotificationGenerationJob notificationJo
private static void createSummaryNotification(OSNotificationGenerationJob notificationJob, OneSignalNotificationBuilder notifBuilder) {
boolean updateSummary = notificationJob.isRestoring();
JSONObject fcmJson = notificationJob.getJsonPayload();
+ GenerateNotificationOpenIntent intentGenerator = GenerateNotificationOpenIntentFromPushPayload.INSTANCE.create(
+ currentContext,
+ fcmJson
+ );
String group = fcmJson.optString("grp", null);
@@ -536,7 +569,10 @@ private static void createSummaryNotification(OSNotificationGenerationJob notifi
createSummaryIdDatabaseEntry(dbHelper, group, summaryNotificationId);
}
- PendingIntent summaryContentIntent = getNewActionPendingIntent(random.nextInt(), createBaseSummaryIntent(summaryNotificationId, fcmJson, group));
+ PendingIntent summaryContentIntent = intentGenerator.getNewActionPendingIntent(
+ random.nextInt(),
+ createBaseSummaryIntent(summaryNotificationId, intentGenerator, fcmJson, group)
+ );
// 2 or more notifications with a group received, group them together as a single notification.
if (summaryList != null &&
@@ -622,7 +658,13 @@ private static void createSummaryNotification(OSNotificationGenerationJob notifi
// extender setup all the settings will carry over.
// Note: However their buttons will not carry over as we need to be setup with this new summaryNotificationId.
summaryBuilder.mActions.clear();
- addNotificationActionButtons(fcmJson, summaryBuilder, summaryNotificationId, group);
+ addNotificationActionButtons(
+ fcmJson,
+ intentGenerator,
+ summaryBuilder,
+ summaryNotificationId,
+ group
+ );
summaryBuilder.setContentIntent(summaryContentIntent)
.setDeleteIntent(summaryDeleteIntent)
@@ -646,7 +688,11 @@ private static void createSummaryNotification(OSNotificationGenerationJob notifi
}
@RequiresApi(api = Build.VERSION_CODES.M)
- private static void createGrouplessSummaryNotification(OSNotificationGenerationJob notificationJob, int grouplessNotifCount) {
+ private static void createGrouplessSummaryNotification(
+ OSNotificationGenerationJob notificationJob,
+ GenerateNotificationOpenIntent intentGenerator,
+ int grouplessNotifCount
+ ) {
JSONObject fcmJson = notificationJob.getJsonPayload();
Notification summaryNotification;
@@ -656,7 +702,10 @@ private static void createGrouplessSummaryNotification(OSNotificationGenerationJ
String summaryMessage = grouplessNotifCount + " new messages";
int summaryNotificationId = OneSignalNotificationManager.getGrouplessSummaryId();
- PendingIntent summaryContentIntent = getNewActionPendingIntent(random.nextInt(), createBaseSummaryIntent(summaryNotificationId, fcmJson, group));
+ PendingIntent summaryContentIntent = intentGenerator.getNewActionPendingIntent(
+ random.nextInt(),
+ createBaseSummaryIntent(summaryNotificationId,intentGenerator, fcmJson, group)
+ );
PendingIntent summaryDeleteIntent = getNewDismissActionPendingIntent(random.nextInt(), getNewBaseDismissIntent(0).putExtra("summary", group));
NotificationCompat.Builder summaryBuilder = getBaseOneSignalNotificationBuilder(notificationJob).compatBuilder;
@@ -696,8 +745,13 @@ private static void createGrouplessSummaryNotification(OSNotificationGenerationJ
NotificationManagerCompat.from(currentContext).notify(summaryNotificationId, summaryNotification);
}
- private static Intent createBaseSummaryIntent(int summaryNotificationId, JSONObject fcmJson, String group) {
- return getNewBaseIntent(summaryNotificationId).putExtra(BUNDLE_KEY_ONESIGNAL_DATA, fcmJson.toString()).putExtra("summary", group);
+ private static Intent createBaseSummaryIntent(
+ int summaryNotificationId,
+ GenerateNotificationOpenIntent intentGenerator,
+ JSONObject fcmJson,
+ String group
+ ) {
+ return intentGenerator.getNewBaseIntent(summaryNotificationId).putExtra(BUNDLE_KEY_ONESIGNAL_DATA, fcmJson.toString()).putExtra("summary", group);
}
private static void createSummaryIdDatabaseEntry(OneSignalDbHelper dbHelper, String group, int id) {
@@ -958,7 +1012,13 @@ static BigInteger getAccentColor(JSONObject fcmJson) {
return null;
}
- private static void addNotificationActionButtons(JSONObject fcmJson, NotificationCompat.Builder mBuilder, int notificationId, String groupSummary) {
+ private static void addNotificationActionButtons(
+ JSONObject fcmJson,
+ GenerateNotificationOpenIntent intentGenerator,
+ NotificationCompat.Builder mBuilder,
+ int notificationId,
+ String groupSummary
+ ) {
try {
JSONObject customJson = new JSONObject(fcmJson.optString("custom"));
@@ -975,7 +1035,7 @@ private static void addNotificationActionButtons(JSONObject fcmJson, Notificatio
JSONObject button = buttons.optJSONObject(i);
JSONObject bundle = new JSONObject(fcmJson.toString());
- Intent buttonIntent = getNewBaseIntent(notificationId);
+ Intent buttonIntent = intentGenerator.getNewBaseIntent(notificationId);
buttonIntent.setAction("" + i); // Required to keep each action button from replacing extras of each other
buttonIntent.putExtra("action_button", true);
bundle.put(BUNDLE_KEY_ACTION_ID, button.optString("id"));
@@ -985,7 +1045,7 @@ private static void addNotificationActionButtons(JSONObject fcmJson, Notificatio
else if (fcmJson.has("grp"))
buttonIntent.putExtra("grp", fcmJson.optString("grp"));
- PendingIntent buttonPIntent = getNewActionPendingIntent(notificationId, buttonIntent);
+ PendingIntent buttonPIntent = intentGenerator.getNewActionPendingIntent(notificationId, buttonIntent);
int buttonIcon = 0;
if (button.has("icon"))
@@ -1043,4 +1103,4 @@ private static int convertOSToAndroidPriority(int priority) {
return NotificationCompat.PRIORITY_MIN;
}
-}
\ No newline at end of file
+}
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotificationOpenIntent.kt b/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotificationOpenIntent.kt
new file mode 100644
index 0000000000..caf6e7b71a
--- /dev/null
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotificationOpenIntent.kt
@@ -0,0 +1,119 @@
+package com.onesignal
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+
+class GenerateNotificationOpenIntent(
+ private val context: Context,
+ private val intent: Intent?,
+ private val startApp: Boolean
+) {
+
+ private val notificationOpenedClass: Class<*> = NotificationOpenedReceiver::class.java
+
+ fun getNewBaseIntent(
+ notificationId: Int,
+ ): Intent {
+ // We use SINGLE_TOP and CLEAR_TOP as we don't want more than one OneSignal invisible click
+ // tracking Activity instance around.
+ var intentFlags =
+ Intent.FLAG_ACTIVITY_SINGLE_TOP or
+ Intent.FLAG_ACTIVITY_CLEAR_TOP
+ if (!startApp) {
+ // If we don't want the app to launch we put OneSignal's invisible click tracking Activity on it's own task
+ // so it doesn't resume an existing one once it closes.
+ intentFlags =
+ intentFlags or (
+ Intent.FLAG_ACTIVITY_NEW_TASK or
+ Intent.FLAG_ACTIVITY_MULTIPLE_TASK or
+ Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
+ )
+ }
+
+ return Intent(
+ context,
+ notificationOpenedClass
+ )
+ .putExtra(
+ GenerateNotification.BUNDLE_KEY_ANDROID_NOTIFICATION_ID,
+ notificationId
+ )
+ .addFlags(intentFlags)
+ }
+
+ /**
+ * Creates a PendingIntent to attach to the notification click and it's action button(s).
+ * If the user interacts with the notification this normally starts the app or resumes it
+ * unless the app developer disables this via a OneSignal meta-data AndroidManifest.xml setting
+ *
+ * The default behavior is to open the app in the same way an Android homescreen launcher does.
+ * This means we expect the following behavior:
+ * 1. Starts the Activity defined in the app's AndroidManifest.xml as "android.intent.action.MAIN"
+ * 2. If the app is already running, instead the last activity will be resumed
+ * 3. If the app is not running (due to being push out of memory), the last activity will be resumed
+ * 4. If the app is no longer in the recent apps list, it is not resumed, same as #1 above.
+ * - App is removed from the recent app's list if it is swiped away or "clear all" is pressed.
+ */
+ fun getNewActionPendingIntent(
+ requestCode: Int,
+ oneSignalIntent: Intent,
+ ): PendingIntent? {
+ val launchIntent = getIntentVisible()
+ ?:
+ // Even though the default app open action is disabled we still need to attach OneSignal's
+ // invisible Activity to capture click event to report click counts and etc.
+ // You may be thinking why not use a BroadcastReceiver instead of an invisible
+ // Activity? This could be done in a 5.0.0 release but can't be changed now as it is
+ // unknown if the app developer will be starting there own Activity from their
+ // OSNotificationOpenedHandler and that would have side-effects.
+ return PendingIntent.getActivity(
+ context,
+ requestCode,
+ oneSignalIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+
+ // This setups up a "Reverse Activity Trampoline"
+ // The first Activity to launch will be oneSignalIntent, which is an invisible
+ // Activity to track the click, fire OSNotificationOpenedHandler, etc. This Activity
+ // will finish quickly and the destination Activity, launchIntent, will be shown to the user
+ // since it is the next in the back stack.
+ return PendingIntent.getActivities(
+ context,
+ requestCode,
+ arrayOf(launchIntent, oneSignalIntent),
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ }
+
+ // Return the provide intent if one was set, otherwise default to opening the app.
+ private fun getIntentVisible(): Intent? {
+ if (intent != null) return intent
+ return getIntentAppOpen()
+ }
+
+ // Provides the default launcher Activity, if the app has one.
+ // - This is almost always true, one of the few exceptions being an app that is only a widget.
+ private fun getIntentAppOpen(): Intent? {
+ if (!startApp) return null
+
+ val launchIntent =
+ context.packageManager.getLaunchIntentForPackage(
+ context.packageName
+ )
+ ?: return null
+
+ // Removing "package" from the intent treats the app as if it was started externally.
+ // - This is exactly what an Android Launcher does.
+ // This prevents another instance of the Activity from being created.
+ // Android 11 no longer requires nulling this out to get this behavior.
+ launchIntent.setPackage(null)
+ launchIntent.flags =
+ Intent.FLAG_ACTIVITY_NEW_TASK or
+ Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+
+ return launchIntent
+ }
+}
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotificationOpenIntentFromPushPayload.kt b/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotificationOpenIntentFromPushPayload.kt
new file mode 100644
index 0000000000..54209f9c18
--- /dev/null
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotificationOpenIntentFromPushPayload.kt
@@ -0,0 +1,42 @@
+package com.onesignal
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import org.json.JSONObject
+
+/**
+ * Create a GenerateNotificationOpenIntent instance based on:
+ * * OSNotificationOpenBehaviorFromPushPayload
+ * * Payload
+ */
+object GenerateNotificationOpenIntentFromPushPayload {
+ fun create(
+ context: Context,
+ fcmPayload: JSONObject
+ ): GenerateNotificationOpenIntent {
+ val behavior = OSNotificationOpenBehaviorFromPushPayload(
+ context,
+ fcmPayload,
+ )
+
+ return GenerateNotificationOpenIntent(
+ context,
+ openBrowserIntent(behavior.uri),
+ shouldOpenApp(behavior.shouldOpenApp, fcmPayload)
+ )
+ }
+
+ private fun shouldOpenApp(shouldOpenApp: Boolean, fcmPayload: JSONObject): Boolean {
+ val isIAMPreviewNotification = OSInAppMessagePreviewHandler.inAppPreviewPushUUID(fcmPayload) != null
+ return isIAMPreviewNotification or
+ shouldOpenApp
+ }
+
+ private fun openBrowserIntent(
+ uri: Uri?,
+ ): Intent? {
+ if (uri == null) return null
+ return OSUtils.openURLInBrowserIntent(uri)
+ }
+}
\ No newline at end of file
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationOpenedProcessor.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationOpenedProcessor.java
index 3f879ba8aa..f0d2bedf8f 100644
--- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationOpenedProcessor.java
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationOpenedProcessor.java
@@ -143,7 +143,6 @@ static boolean handleIAMPreviewOpen(@NonNull Activity context, @NonNull JSONObje
if (previewUUID == null)
return false;
- OneSignal.startOrResumeApp(context);
OneSignal.getInAppMessageController().displayPreviewMessage(previewUUID);
return true;
}
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationSummaryManager.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationSummaryManager.java
index 5ac83bce9d..fa19caea75 100644
--- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationSummaryManager.java
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationSummaryManager.java
@@ -43,12 +43,15 @@ static void updateSummaryNotificationAfterChildRemoved(Context context, OneSigna
cursor.close();
}
}
-
+
private static Cursor internalUpdateSummaryNotificationAfterChildRemoved(Context context, OneSignalDb db, String group, boolean dismissed) {
Cursor cursor = db.query(
NotificationTable.TABLE_NAME,
- new String[] { NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID, // return columns
- NotificationTable.COLUMN_NAME_CREATED_TIME },
+ new String[] {
+ NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID,
+ NotificationTable.COLUMN_NAME_CREATED_TIME,
+ NotificationTable.COLUMN_NAME_FULL_DATA,
+ },
NotificationTable.COLUMN_NAME_GROUP_ID + " = ? AND " + // Where String
NotificationTable.COLUMN_NAME_DISMISSED + " = 0 AND " +
NotificationTable.COLUMN_NAME_OPENED + " = 0 AND " +
@@ -102,22 +105,22 @@ private static Cursor internalUpdateSummaryNotificationAfterChildRemoved(Context
try {
cursor.moveToFirst();
Long datetime = cursor.getLong(cursor.getColumnIndex(NotificationTable.COLUMN_NAME_CREATED_TIME));
+ String jsonStr = cursor.getString(cursor.getColumnIndex(NotificationTable.COLUMN_NAME_FULL_DATA));
cursor.close();
-
+
Integer androidNotifId = getSummaryNotificationId(db, group);
if (androidNotifId == null)
return cursor;
-
+
OSNotificationGenerationJob notificationJob = new OSNotificationGenerationJob(context);
notificationJob.setRestoring(true);
notificationJob.setShownTimeStamp(datetime);
-
- JSONObject payload = new JSONObject();
- payload.put("grp", group);
- notificationJob.setJsonPayload(payload);
-
+ notificationJob.setJsonPayload(new JSONObject(jsonStr));
+
GenerateNotification.updateSummaryNotification(notificationJob);
- } catch (JSONException e) {}
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
return cursor;
}
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSNotificationOpenAppSettings.kt b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSNotificationOpenAppSettings.kt
new file mode 100644
index 0000000000..a6bfa8792e
--- /dev/null
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSNotificationOpenAppSettings.kt
@@ -0,0 +1,30 @@
+package com.onesignal
+
+import android.content.Context
+
+/***
+ * Settings that effect the OneSignal notification open behavior at the app level.
+ */
+object OSNotificationOpenAppSettings {
+
+ /***
+ * When the notification is tapped on should it show an Activity?
+ * This could be resuming / opening the app or opening the URL on the notification.
+ */
+ fun getShouldOpenActivity(context: Context): Boolean {
+ return "DISABLE" != OSUtils.getManifestMeta(
+ context,
+ "com.onesignal.NotificationOpened.DEFAULT"
+ )
+ }
+
+ /***
+ * Should the default behavior of OneSignal be to always open URLs be disabled?
+ */
+ fun getSuppressLaunchURL(context: Context): Boolean {
+ return OSUtils.getManifestMetaBoolean(
+ context,
+ "com.onesignal.suppressLaunchURLs"
+ )
+ }
+}
\ No newline at end of file
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSNotificationOpenBehaviorFromPushPayload.kt b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSNotificationOpenBehaviorFromPushPayload.kt
new file mode 100644
index 0000000000..193405cfd1
--- /dev/null
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSNotificationOpenBehaviorFromPushPayload.kt
@@ -0,0 +1,35 @@
+package com.onesignal
+
+import android.content.Context
+import android.net.Uri
+import org.json.JSONObject
+
+class OSNotificationOpenBehaviorFromPushPayload(
+ private val context: Context,
+ private val fcmPayload: JSONObject,
+) {
+
+ val shouldOpenApp: Boolean
+ get() {
+ return OSNotificationOpenAppSettings.getShouldOpenActivity(context)
+ && uri == null
+ }
+
+ val uri: Uri?
+ get() {
+ if (!OSNotificationOpenAppSettings.getShouldOpenActivity(context)) return null
+ if (OSNotificationOpenAppSettings.getSuppressLaunchURL(context)) return null
+
+ val customJSON = JSONObject(fcmPayload.optString("custom"))
+
+ if (customJSON.has("u")) {
+ val url = customJSON.optString("u")
+ if (url != "") {
+ return Uri.parse(url.trim { it <= ' ' })
+ }
+ }
+
+ return null
+ }
+
+}
\ No newline at end of file
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSUtils.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSUtils.java
index af7c7ade5d..0c7bf75adc 100644
--- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSUtils.java
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSUtils.java
@@ -548,10 +548,16 @@ static void openURLInBrowser(@NonNull String url) {
}
private static void openURLInBrowser(@NonNull Uri uri) {
+ Intent intent = openURLInBrowserIntent(uri);
+ OneSignal.appContext.startActivity(intent);
+ }
+
+ @NonNull
+ static Intent openURLInBrowserIntent(@NonNull Uri uri) {
SchemaType type = uri.getScheme() != null ? SchemaType.fromString(uri.getScheme()) : null;
if (type == null) {
- type = SchemaType.HTTP;
- if (!uri.toString().contains("://")) {
+ type = SchemaType.HTTP;
+ if (!uri.toString().contains("://")) {
uri = Uri.parse("http://" + uri.toString());
}
}
@@ -568,11 +574,12 @@ private static void openURLInBrowser(@NonNull Uri uri) {
break;
}
intent.addFlags(
- Intent.FLAG_ACTIVITY_NO_HISTORY |
- Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET |
- Intent.FLAG_ACTIVITY_MULTIPLE_TASK |
- Intent.FLAG_ACTIVITY_NEW_TASK);
- OneSignal.appContext.startActivity(intent);
+ Intent.FLAG_ACTIVITY_NO_HISTORY |
+ Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET |
+ Intent.FLAG_ACTIVITY_MULTIPLE_TASK |
+ Intent.FLAG_ACTIVITY_NEW_TASK
+ );
+ return intent;
}
// Creates a new Set that supports reads and writes from more than one thread at a time
diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignal.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignal.java
index 9bf020dcaa..94ca9e1a1d 100644
--- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignal.java
+++ b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignal.java
@@ -2180,40 +2180,6 @@ static void sendPurchases(JSONArray purchases, boolean newAsExisting, OneSignalR
}
}
- private static boolean openURLFromNotification(Context context, JSONArray dataArray) {
-
- //if applicable, check if the user provided privacy consent
- if (shouldLogUserPrivacyConsentErrorMessageForMethodName(null))
- return false;
-
- int jsonArraySize = dataArray.length();
-
- boolean urlOpened = false;
- boolean launchUrlSuppress = OSUtils.getManifestMetaBoolean(context, "com.onesignal.suppressLaunchURLs");
-
- for (int i = 0; i < jsonArraySize; i++) {
- try {
- JSONObject data = dataArray.getJSONObject(i);
- if (!data.has("custom"))
- continue;
-
- JSONObject customJSON = new JSONObject(data.optString("custom"));
-
- if (customJSON.has("u")) {
- String url = customJSON.optString("u", null);
- if (url != null && !launchUrlSuppress) {
- OSUtils.openURLInBrowser(url);
- urlOpened = true;
- }
- }
- } catch (Throwable t) {
- Log(LOG_LEVEL.ERROR, "Error parsing JSON item " + i + "/" + jsonArraySize + " for launching a web URL.", t);
- }
- }
-
- return urlOpened;
- }
-
private static void runNotificationOpenedCallback(final JSONArray dataArray) {
if (notificationOpenedHandler == null) {
unprocessedOpenedNotifs.add(dataArray);
@@ -2366,66 +2332,34 @@ public void run() {
if (trackFirebaseAnalytics != null && getFirebaseAnalyticsEnabled())
trackFirebaseAnalytics.trackOpenedEvent(generateNotificationOpenedResult(data));
- boolean urlOpened = false;
- boolean defaultOpenActionDisabled = "DISABLE".equals(OSUtils.getManifestMeta(context, "com.onesignal.NotificationOpened.DEFAULT"));
-
- if (!defaultOpenActionDisabled)
- urlOpened = openURLFromNotification(context, data);
-
- logger.debug("handleNotificationOpen from context: " + context + " with fromAlert: " + fromAlert + " urlOpened: " + urlOpened + " defaultOpenActionDisabled: " + defaultOpenActionDisabled);
- // Check if the notification click should lead to a DIRECT session
- if (shouldInitDirectSessionFromNotificationOpen(context, fromAlert, urlOpened, defaultOpenActionDisabled)) {
+ if (shouldInitDirectSessionFromNotificationOpen(context, data)) {
applicationOpenedByNotification(notificationId);
}
runNotificationOpenedCallback(data);
}
- static void applicationOpenedByNotification(@Nullable final String notificationId) {
- // We want to set the app entry state to NOTIFICATION_CLICK when coming from background
- appEntryState = AppEntryAction.NOTIFICATION_CLICK;
- sessionManager.onDirectInfluenceFromNotificationOpen(appEntryState, notificationId);
- }
-
- // This opens the app in the same way an Android homescreen launcher does.
- // This means we expect the following behavior:
- // 1. Starts the Activity defined in the app's AndroidManifest.xml as "android.intent.action.MAIN"
- // 2. If the app is already running, instead the last activity will be resumed
- // 3. If the app is not running (due to being push out of memory), the last activity will be resumed
- // 4. If the app is no longer in the recent apps list, it is not resumed, same as #1 above.
- // - App is removed from the recent app's list if it is swiped away or "clear all" is pressed.
- static boolean startOrResumeApp(@NonNull Activity activity) {
- Intent launchIntent = activity.getPackageManager().getLaunchIntentForPackage(activity.getPackageName());
- logger.debug("startOrResumeApp from context: " + activity + " isRoot: " + activity.isTaskRoot() + " with launchIntent: " + launchIntent);
-
- // Not all apps have a launcher intent, such as one that only provides a homescreen widget
- if (launchIntent == null)
+ private static boolean shouldInitDirectSessionFromNotificationOpen(Activity context, final JSONArray data) {
+ if (inForeground) {
return false;
+ }
- // Removing package from the intent, this treats the app as if it was started externally.
- // This gives us the resume app behavior noted above.
- // Android 11 no longer requires nulling this out to get this behavior.
- launchIntent.setPackage(null);
-
- activity.startActivity(launchIntent);
-
+ try {
+ JSONObject interactedNotificationData = data.getJSONObject(0);
+ return new OSNotificationOpenBehaviorFromPushPayload(
+ context,
+ interactedNotificationData
+ ).getShouldOpenApp();
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
return true;
}
- /**
- * 1. App is not an alert
- * 2. Not a URL open
- * 3. Manifest setting for com.onesignal.NotificationOpened.DEFAULT is not disabled
- * 4. Manifest setting for com.onesignal.suppressLaunchURLs is not true
- * 5. App is coming from the background
- * 6. App open/resume intent exists
- */
- private static boolean shouldInitDirectSessionFromNotificationOpen(Activity context, boolean fromAlert, boolean urlOpened, boolean defaultOpenActionDisabled) {
- return !fromAlert
- && !urlOpened
- && !defaultOpenActionDisabled
- && !inForeground
- && startOrResumeApp(context);
+ static void applicationOpenedByNotification(@Nullable final String notificationId) {
+ // We want to set the app entry state to NOTIFICATION_CLICK when coming from background
+ appEntryState = AppEntryAction.NOTIFICATION_CLICK;
+ sessionManager.onDirectInfluenceFromNotificationOpen(appEntryState, notificationId);
}
private static void notificationOpenedRESTCall(Context inContext, JSONArray dataArray) {
diff --git a/OneSignalSDK/unittest/src/test/java/com/test/onesignal/GenerateNotificationRunner.java b/OneSignalSDK/unittest/src/test/java/com/test/onesignal/GenerateNotificationRunner.java
index 637ee38cbd..a639bbddce 100644
--- a/OneSignalSDK/unittest/src/test/java/com/test/onesignal/GenerateNotificationRunner.java
+++ b/OneSignalSDK/unittest/src/test/java/com/test/onesignal/GenerateNotificationRunner.java
@@ -30,6 +30,7 @@
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
+import android.app.PendingIntent;
import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
@@ -1129,6 +1130,64 @@ public void shouldSetButtonsCorrectly() throws Exception {
assertEquals("id1", new JSONObject(json_data).optString(BUNDLE_KEY_ACTION_ID));
}
+ @Test
+ @Config(shadows = { ShadowGenerateNotification.class })
+ public void shouldSetContentIntentForLaunchURL() throws Exception {
+ generateNotificationWithLaunchURL();
+
+ Intent[] intents = lastNotificationIntents();
+ assertEquals(2, intents.length);
+ Intent intentLaunchURL = intents[0];
+ assertEquals("android.intent.action.VIEW", intentLaunchURL.getAction());
+ assertEquals("https://google.com", intentLaunchURL.getData().toString());
+
+ assertNotificationOpenedReceiver(intents[1]);
+ }
+
+ @Test
+ @Config(shadows = { ShadowGenerateNotification.class })
+ public void shouldNotSetContentIntentForLaunchURLIfDefaultNotificationOpenIsDisabled() throws Exception {
+ OneSignalShadowPackageManager.addManifestMetaData("com.onesignal.NotificationOpened.DEFAULT", "DISABLE");
+ generateNotificationWithLaunchURL();
+
+ Intent[] intents = lastNotificationIntents();
+ assertEquals(1, intents.length);
+ assertNotificationOpenedReceiver(intents[0]);
+ }
+
+ @Test
+ @Config(shadows = { ShadowGenerateNotification.class })
+ public void shouldNotSetContentIntentForLaunchURLIfSuppress() throws Exception {
+ OneSignalShadowPackageManager.addManifestMetaData("com.onesignal.suppressLaunchURLs", true);
+ generateNotificationWithLaunchURL();
+
+ Intent[] intents = lastNotificationIntents();
+ assertEquals(2, intents.length);
+ assertOpenMainActivityIntent(intents[0]);
+ assertNotificationOpenedReceiver(intents[1]);
+ }
+
+ private Intent[] lastNotificationIntents() {
+ PendingIntent pendingIntent = ShadowRoboNotificationManager.getLastNotif().contentIntent;
+ // NOTE: This is fragile until this robolectric issue is fixed: https://github.com/robolectric/robolectric/issues/6660
+ return shadowOf(pendingIntent).getSavedIntents();
+ }
+
+ private void generateNotificationWithLaunchURL() throws Exception {
+ Bundle bundle = launchURLMockPayloadBundle();
+ NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle);
+ threadAndTaskWait();
+ }
+
+ private void assertNotificationOpenedReceiver(@NonNull Intent intent) {
+ assertEquals("com.onesignal.NotificationOpenedReceiver", intent.getComponent().getClassName());
+ }
+
+ private void assertOpenMainActivityIntent(@NonNull Intent intent) {
+ assertEquals(Intent.ACTION_MAIN, intent.getAction());
+ assertTrue(intent.getCategories().contains(Intent.CATEGORY_LAUNCHER));
+ }
+
@Test
@Config(shadows = { ShadowGenerateNotification.class })
public void shouldSetAlertnessFieldsOnNormalPriority() {
@@ -1211,6 +1270,17 @@ public void shouldSetExpireTimeCorrectlyWhenMissingFromPayload() throws Exceptio
return bundle;
}
+ @NonNull
+ private static Bundle launchURLMockPayloadBundle() throws JSONException {
+ Bundle bundle = new Bundle();
+ bundle.putString("alert", "test");
+ bundle.putString("custom", new JSONObject() {{
+ put("i", "UUID");
+ put("u", "https://google.com");
+ }}.toString());
+ return bundle;
+ }
+
@Test
@Config(shadows = { ShadowOneSignalRestClient.class, ShadowOSWebView.class })
public void shouldShowInAppPreviewWhenInFocus() throws Exception {
diff --git a/OneSignalSDK/unittest/src/test/java/com/test/onesignal/MainOneSignalClassRunner.java b/OneSignalSDK/unittest/src/test/java/com/test/onesignal/MainOneSignalClassRunner.java
index 81222a7d21..90c3e10a82 100644
--- a/OneSignalSDK/unittest/src/test/java/com/test/onesignal/MainOneSignalClassRunner.java
+++ b/OneSignalSDK/unittest/src/test/java/com/test/onesignal/MainOneSignalClassRunner.java
@@ -1042,127 +1042,6 @@ public void shouldNotFireNotificationOpenAgainAfterAppRestart() throws Exception
assertEquals(null, lastNotificationOpenedBody);
}
- @Test
- public void testOpeningLauncherActivity() throws Exception {
- // First init run for appId to be saved
- // At least OneSignal was init once for user to be subscribed
- // If this doesn't' happen, notifications will not arrive
- OneSignalInit();
- fastColdRestartApp();
-
- AddLauncherIntentFilter();
- // From app launching normally
- assertNotNull(shadowOf(blankActivity).getNextStartedActivity());
- // Will get appId saved
- OneSignal.initWithContext(blankActivity.getApplicationContext());
- OneSignal_handleNotificationOpen(blankActivity, new JSONArray("[{ \"alert\": \"Test Msg\", \"custom\": { \"i\": \"UUID\" } }]"), false, ONESIGNAL_NOTIFICATION_ID);
-
- assertNotNull(shadowOf(blankActivity).getNextStartedActivity());
- assertNull(shadowOf(blankActivity).getNextStartedActivity());
- }
-
- @Test
- public void testOpeningLaunchUrl() throws Exception {
- // First init run for appId to be saved
- // At least OneSignal was init once for user to be subscribed
- // If this doesn't' happen, notifications will not arrive
- OneSignalInit();
- fastColdRestartApp();
- OneSignal.initWithContext(blankActivity);
- // Removes app launch
- shadowOf(blankActivity).getNextStartedActivity();
-
- // No OneSignal init here to test case where it is located in an Activity.
- OneSignal_handleNotificationOpen(blankActivity, new JSONArray("[{ \"alert\": \"Test Msg\", \"custom\": { \"i\": \"UUID\", \"u\": \"http://google.com\" } }]"), false, ONESIGNAL_NOTIFICATION_ID);
- Intent intent = shadowOf(blankActivity).getNextStartedActivity();
- assertEquals("android.intent.action.VIEW", intent.getAction());
- assertEquals("http://google.com", intent.getData().toString());
- assertNull(shadowOf(blankActivity).getNextStartedActivity());
- }
-
- @Test
- public void testOpeningLaunchUrlWithDisableDefault() throws Exception {
- // Add the 'com.onesignal.NotificationOpened.DEFAULT' as 'DISABLE' meta-data tag
- OneSignalShadowPackageManager.addManifestMetaData("com.onesignal.NotificationOpened.DEFAULT", "DISABLE");
-
- // Removes app launch
- shadowOf(blankActivity).getNextStartedActivity();
-
- // No OneSignal init here to test case where it is located in an Activity.
-
- OneSignal_handleNotificationOpen(blankActivity, new JSONArray("[{ \"alert\": \"Test Msg\", \"custom\": { \"i\": \"UUID\", \"u\": \"http://google.com\" } }]"), false, ONESIGNAL_NOTIFICATION_ID);
- assertNull(shadowOf(blankActivity).getNextStartedActivity());
- }
-
- @Test
- public void testDisableOpeningLauncherActivityOnNotificationOpen() throws Exception {
- // Add the 'com.onesignal.NotificationOpened.DEFAULT' as 'DISABLE' meta-data tag
- OneSignalShadowPackageManager.addManifestMetaData("com.onesignal.NotificationOpened.DEFAULT", "DISABLE");
-
- // From app launching normally
- assertNotNull(shadowOf(blankActivity).getNextStartedActivity());
- OneSignal.setAppId(ONESIGNAL_APP_ID);
- OneSignal.initWithContext(blankActivity);
- OneSignal.setNotificationOpenedHandler(getNotificationOpenedHandler());
- assertNull(lastNotificationOpenedBody);
-
- OneSignal_handleNotificationOpen(blankActivity, new JSONArray("[{ \"alert\": \"Test Msg\", \"custom\": { \"i\": \"UUID\" } }]"), false, ONESIGNAL_NOTIFICATION_ID);
-
- assertNull(shadowOf(blankActivity).getNextStartedActivity());
- assertEquals("Test Msg", lastNotificationOpenedBody);
- }
-
- @Test
- public void testLaunchUrlSuppressTrue() throws Exception {
- // Add the 'com.onesignal.suppressLaunchURLs' as 'true' meta-data tag
- // First init run for appId to be saved
- // At least OneSignal was init once for user to be subscribed
- // If this doesn't' happen, notifications will not arrive
- OneSignalInit();
- fastColdRestartApp();
-
- // Add the 'com.onesignal.suppressLaunchURLs' as 'true' meta-data tag
- OneSignalShadowPackageManager.addManifestMetaData("com.onesignal.suppressLaunchURLs", true);
-
- // Removes app launch
- shadowOf(blankActivity).getNextStartedActivity();
-
- // Init with context since this is call before calling OneSignal_handleNotificationOpen internally
- OneSignal.initWithContext(blankActivity);
-
- OneSignal_handleNotificationOpen(blankActivity, new JSONArray("[{ \"alert\": \"Test Msg\", \"custom\": { \"i\": \"UUID\", \"u\": \"http://google.com\" } }]"), false, ONESIGNAL_NOTIFICATION_ID);
- threadAndTaskWait();
-
- assertNull(shadowOf(blankActivity).getNextStartedActivity());
- }
-
- @Test
- public void testLaunchUrlSuppressFalse() throws Exception {
- // Add the 'com.onesignal.suppressLaunchURLs' as 'true' meta-data tag
- // First init run for appId to be saved
- // At least OneSignal was init once for user to be subscribed
- // If this doesn't' happen, notifications will not arrive
- OneSignalInit();
- fastColdRestartApp();
-
- OneSignalShadowPackageManager.addManifestMetaData("com.onesignal.suppressLaunchURLs", false);
- OneSignal.initWithContext(blankActivity);
-
- // Removes app launch
- shadowOf(blankActivity).getNextStartedActivity();
-
- // Init with context since this is call before calling OneSignal_handleNotificationOpen internally
- OneSignal.initWithContext(blankActivity);
-
- OneSignal_handleNotificationOpen(blankActivity, new JSONArray("[{ \"alert\": \"Test Msg\", \"custom\": { \"i\": \"UUID\", \"u\": \"http://google.com\" } }]"), false, ONESIGNAL_NOTIFICATION_ID);
- threadAndTaskWait();
-
- Intent intent = shadowOf(blankActivity).getNextStartedActivity();
- assertEquals("android.intent.action.VIEW", intent.getAction());
- assertEquals("http://google.com", intent.getData().toString());
- assertNull(shadowOf(blankActivity).getNextStartedActivity());
- }
-
private static String notificationReceivedBody;
private static int androidNotificationId;
diff --git a/OneSignalSDK/unittest/src/test/java/com/test/onesignal/NotificationOpenedActivityHMSIntegrationTestsRunner.java b/OneSignalSDK/unittest/src/test/java/com/test/onesignal/NotificationOpenedActivityHMSIntegrationTestsRunner.java
index 0263c49dbb..6f2941e0da 100644
--- a/OneSignalSDK/unittest/src/test/java/com/test/onesignal/NotificationOpenedActivityHMSIntegrationTestsRunner.java
+++ b/OneSignalSDK/unittest/src/test/java/com/test/onesignal/NotificationOpenedActivityHMSIntegrationTestsRunner.java
@@ -154,15 +154,6 @@ public void emptyIntent_doesNotThrow() {
helper_startHMSOpenActivity(helper_baseHMSOpenIntent());
}
- @Test
- public void barebonesOSPayload_startsMainActivity() throws Exception {
- helper_initSDKAndFireHMSNotificationBarebonesOSOpenIntent();
-
- Intent startedActivity = shadowOf((Application) ApplicationProvider.getApplicationContext()).getNextStartedActivity();
- assertNotNull(startedActivity);
- assertEquals(startedActivity.getComponent().getClassName(), BlankActivity.class.getName());
- }
-
@Test
public void barebonesOSPayload_makesNotificationOpenRequest() throws Exception {
helper_initSDKAndFireHMSNotificationBarebonesOSOpenIntent();