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();