Skip to content

Commit d61f3c9

Browse files
authored
Merge pull request #1377 from OneSignal/feat/reverse_activity_trampolining
Reverse Activity Trampolining Implementation
2 parents 09b8d91 + 07462cc commit d61f3c9

14 files changed

+437
-267
lines changed

Examples/OneSignalDemo/app/src/main/java/com/onesignal/sdktest/application/MainApplication.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public void onCreate() {
4141
OSNotification notification = notificationReceivedEvent.getNotification();
4242
JSONObject data = notification.getAdditionalData();
4343

44-
notificationReceivedEvent.complete(null);
44+
notificationReceivedEvent.complete(notification);
4545
});
4646

4747
OneSignal.unsubscribeWhenNotificationsAreDisabled(true);

OneSignalSDK/onesignal/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
<activity
136136
android:name="com.onesignal.NotificationOpenedReceiver"
137137
android:noHistory="true"
138+
android:excludeFromRecents="true"
138139
android:theme="@android:style/Theme.Translucent.NoTitleBar"
139140
android:exported="true" />
140141

OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotification.java

Lines changed: 94 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -127,25 +127,10 @@ private static CharSequence getTitle(JSONObject fcmJson) {
127127
return currentContext.getPackageManager().getApplicationLabel(currentContext.getApplicationInfo());
128128
}
129129

130-
131-
/**
132-
* Notification delete is processed by Broadcast Receiver to avoid creation of activities that can end
133-
* on weird UI interaction
134-
*/
135-
private static PendingIntent getNewActionPendingIntent(int requestCode, Intent intent) {
136-
return PendingIntent.getActivity(currentContext, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
137-
}
138-
139130
private static PendingIntent getNewDismissActionPendingIntent(int requestCode, Intent intent) {
140131
return PendingIntent.getBroadcast(currentContext, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
141132
}
142133

143-
private static Intent getNewBaseIntent(int notificationId) {
144-
return new Intent(currentContext, notificationOpenedClass)
145-
.putExtra(BUNDLE_KEY_ANDROID_NOTIFICATION_ID, notificationId)
146-
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
147-
}
148-
149134
private static Intent getNewBaseDismissIntent(int notificationId) {
150135
return new Intent(currentContext, notificationDismissedClass)
151136
.putExtra(BUNDLE_KEY_ANDROID_NOTIFICATION_ID, notificationId)
@@ -274,6 +259,11 @@ private static boolean showNotification(OSNotificationGenerationJob notification
274259
JSONObject fcmJson = notificationJob.getJsonPayload();
275260
String group = fcmJson.optString("grp", null);
276261

262+
GenerateNotificationOpenIntent intentGenerator = GenerateNotificationOpenIntentFromPushPayload.INSTANCE.create(
263+
currentContext,
264+
fcmJson
265+
);
266+
277267
ArrayList<StatusBarNotification> grouplessNotifs = new ArrayList<>();
278268
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
279269
/* 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
289279
OneSignalNotificationBuilder oneSignalNotificationBuilder = getBaseOneSignalNotificationBuilder(notificationJob);
290280
NotificationCompat.Builder notifBuilder = oneSignalNotificationBuilder.compatBuilder;
291281

292-
addNotificationActionButtons(fcmJson, notifBuilder, notificationId, null);
282+
addNotificationActionButtons(
283+
fcmJson,
284+
intentGenerator,
285+
notifBuilder,
286+
notificationId,
287+
null
288+
);
293289

294290
try {
295291
addBackgroundImage(fcmJson, notifBuilder);
@@ -310,17 +306,33 @@ private static boolean showNotification(OSNotificationGenerationJob notification
310306

311307
Notification notification;
312308
if (group != null) {
313-
createGenericPendingIntentsForGroup(notifBuilder, fcmJson, group, notificationId);
309+
createGenericPendingIntentsForGroup(
310+
notifBuilder,
311+
intentGenerator,
312+
fcmJson,
313+
group,
314+
notificationId
315+
);
314316
notification = createSingleNotificationBeforeSummaryBuilder(notificationJob, notifBuilder);
315317

316318
// Create PendingIntents for notifications in a groupless or defined summary
317319
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
318-
group.equals(OneSignalNotificationManager.getGrouplessSummaryKey()))
319-
createGrouplessSummaryNotification(notificationJob, grouplessNotifs.size() + 1);
320+
group.equals(OneSignalNotificationManager.getGrouplessSummaryKey())) {
321+
createGrouplessSummaryNotification(
322+
notificationJob,
323+
intentGenerator,
324+
grouplessNotifs.size() + 1
325+
);
326+
}
320327
else
321328
createSummaryNotification(notificationJob, oneSignalNotificationBuilder);
322329
} else {
323-
notification = createGenericPendingIntentsForNotif(notifBuilder, fcmJson, notificationId);
330+
notification = createGenericPendingIntentsForNotif(
331+
notifBuilder,
332+
intentGenerator,
333+
fcmJson,
334+
notificationId
335+
);
324336
}
325337
// NotificationManagerCompat does not auto omit the individual notification on the device when using
326338
// stacked notifications on Android 4.2 and older
@@ -338,18 +350,35 @@ private static boolean showNotification(OSNotificationGenerationJob notification
338350
return true;
339351
}
340352

341-
private static Notification createGenericPendingIntentsForNotif(NotificationCompat.Builder notifBuilder, JSONObject gcmBundle, int notificationId) {
353+
private static Notification createGenericPendingIntentsForNotif(
354+
NotificationCompat.Builder notifBuilder,
355+
GenerateNotificationOpenIntent intentGenerator,
356+
JSONObject gcmBundle,
357+
int notificationId
358+
) {
342359
Random random = new SecureRandom();
343-
PendingIntent contentIntent = getNewActionPendingIntent(random.nextInt(), getNewBaseIntent(notificationId).putExtra(BUNDLE_KEY_ONESIGNAL_DATA, gcmBundle.toString()));
360+
PendingIntent contentIntent = intentGenerator.getNewActionPendingIntent(
361+
random.nextInt(),
362+
intentGenerator.getNewBaseIntent(notificationId).putExtra(BUNDLE_KEY_ONESIGNAL_DATA, gcmBundle.toString())
363+
);
344364
notifBuilder.setContentIntent(contentIntent);
345365
PendingIntent deleteIntent = getNewDismissActionPendingIntent(random.nextInt(), getNewBaseDismissIntent(notificationId));
346366
notifBuilder.setDeleteIntent(deleteIntent);
347367
return notifBuilder.build();
348368
}
349369

350-
private static void createGenericPendingIntentsForGroup(NotificationCompat.Builder notifBuilder, JSONObject gcmBundle, String group, int notificationId) {
370+
private static void createGenericPendingIntentsForGroup(
371+
NotificationCompat.Builder notifBuilder,
372+
GenerateNotificationOpenIntent intentGenerator,
373+
JSONObject gcmBundle,
374+
String group,
375+
int notificationId
376+
) {
351377
Random random = new SecureRandom();
352-
PendingIntent contentIntent = getNewActionPendingIntent(random.nextInt(), getNewBaseIntent(notificationId).putExtra(BUNDLE_KEY_ONESIGNAL_DATA, gcmBundle.toString()).putExtra("grp", group));
378+
PendingIntent contentIntent = intentGenerator.getNewActionPendingIntent(
379+
random.nextInt(),
380+
intentGenerator.getNewBaseIntent(notificationId).putExtra(BUNDLE_KEY_ONESIGNAL_DATA, gcmBundle.toString()).putExtra("grp", group)
381+
);
353382
notifBuilder.setContentIntent(contentIntent);
354383
PendingIntent deleteIntent = getNewDismissActionPendingIntent(random.nextInt(), getNewBaseDismissIntent(notificationId).putExtra("grp", group));
355384
notifBuilder.setDeleteIntent(deleteIntent);
@@ -450,6 +479,10 @@ static void updateSummaryNotification(OSNotificationGenerationJob notificationJo
450479
private static void createSummaryNotification(OSNotificationGenerationJob notificationJob, OneSignalNotificationBuilder notifBuilder) {
451480
boolean updateSummary = notificationJob.isRestoring();
452481
JSONObject fcmJson = notificationJob.getJsonPayload();
482+
GenerateNotificationOpenIntent intentGenerator = GenerateNotificationOpenIntentFromPushPayload.INSTANCE.create(
483+
currentContext,
484+
fcmJson
485+
);
453486

454487
String group = fcmJson.optString("grp", null);
455488

@@ -536,7 +569,10 @@ private static void createSummaryNotification(OSNotificationGenerationJob notifi
536569
createSummaryIdDatabaseEntry(dbHelper, group, summaryNotificationId);
537570
}
538571

539-
PendingIntent summaryContentIntent = getNewActionPendingIntent(random.nextInt(), createBaseSummaryIntent(summaryNotificationId, fcmJson, group));
572+
PendingIntent summaryContentIntent = intentGenerator.getNewActionPendingIntent(
573+
random.nextInt(),
574+
createBaseSummaryIntent(summaryNotificationId, intentGenerator, fcmJson, group)
575+
);
540576

541577
// 2 or more notifications with a group received, group them together as a single notification.
542578
if (summaryList != null &&
@@ -622,7 +658,13 @@ private static void createSummaryNotification(OSNotificationGenerationJob notifi
622658
// extender setup all the settings will carry over.
623659
// Note: However their buttons will not carry over as we need to be setup with this new summaryNotificationId.
624660
summaryBuilder.mActions.clear();
625-
addNotificationActionButtons(fcmJson, summaryBuilder, summaryNotificationId, group);
661+
addNotificationActionButtons(
662+
fcmJson,
663+
intentGenerator,
664+
summaryBuilder,
665+
summaryNotificationId,
666+
group
667+
);
626668

627669
summaryBuilder.setContentIntent(summaryContentIntent)
628670
.setDeleteIntent(summaryDeleteIntent)
@@ -646,7 +688,11 @@ private static void createSummaryNotification(OSNotificationGenerationJob notifi
646688
}
647689

648690
@RequiresApi(api = Build.VERSION_CODES.M)
649-
private static void createGrouplessSummaryNotification(OSNotificationGenerationJob notificationJob, int grouplessNotifCount) {
691+
private static void createGrouplessSummaryNotification(
692+
OSNotificationGenerationJob notificationJob,
693+
GenerateNotificationOpenIntent intentGenerator,
694+
int grouplessNotifCount
695+
) {
650696
JSONObject fcmJson = notificationJob.getJsonPayload();
651697

652698
Notification summaryNotification;
@@ -656,7 +702,10 @@ private static void createGrouplessSummaryNotification(OSNotificationGenerationJ
656702
String summaryMessage = grouplessNotifCount + " new messages";
657703
int summaryNotificationId = OneSignalNotificationManager.getGrouplessSummaryId();
658704

659-
PendingIntent summaryContentIntent = getNewActionPendingIntent(random.nextInt(), createBaseSummaryIntent(summaryNotificationId, fcmJson, group));
705+
PendingIntent summaryContentIntent = intentGenerator.getNewActionPendingIntent(
706+
random.nextInt(),
707+
createBaseSummaryIntent(summaryNotificationId,intentGenerator, fcmJson, group)
708+
);
660709
PendingIntent summaryDeleteIntent = getNewDismissActionPendingIntent(random.nextInt(), getNewBaseDismissIntent(0).putExtra("summary", group));
661710

662711
NotificationCompat.Builder summaryBuilder = getBaseOneSignalNotificationBuilder(notificationJob).compatBuilder;
@@ -696,8 +745,13 @@ private static void createGrouplessSummaryNotification(OSNotificationGenerationJ
696745
NotificationManagerCompat.from(currentContext).notify(summaryNotificationId, summaryNotification);
697746
}
698747

699-
private static Intent createBaseSummaryIntent(int summaryNotificationId, JSONObject fcmJson, String group) {
700-
return getNewBaseIntent(summaryNotificationId).putExtra(BUNDLE_KEY_ONESIGNAL_DATA, fcmJson.toString()).putExtra("summary", group);
748+
private static Intent createBaseSummaryIntent(
749+
int summaryNotificationId,
750+
GenerateNotificationOpenIntent intentGenerator,
751+
JSONObject fcmJson,
752+
String group
753+
) {
754+
return intentGenerator.getNewBaseIntent(summaryNotificationId).putExtra(BUNDLE_KEY_ONESIGNAL_DATA, fcmJson.toString()).putExtra("summary", group);
701755
}
702756

703757
private static void createSummaryIdDatabaseEntry(OneSignalDbHelper dbHelper, String group, int id) {
@@ -958,7 +1012,13 @@ static BigInteger getAccentColor(JSONObject fcmJson) {
9581012
return null;
9591013
}
9601014

961-
private static void addNotificationActionButtons(JSONObject fcmJson, NotificationCompat.Builder mBuilder, int notificationId, String groupSummary) {
1015+
private static void addNotificationActionButtons(
1016+
JSONObject fcmJson,
1017+
GenerateNotificationOpenIntent intentGenerator,
1018+
NotificationCompat.Builder mBuilder,
1019+
int notificationId,
1020+
String groupSummary
1021+
) {
9621022
try {
9631023
JSONObject customJson = new JSONObject(fcmJson.optString("custom"));
9641024

@@ -975,7 +1035,7 @@ private static void addNotificationActionButtons(JSONObject fcmJson, Notificatio
9751035
JSONObject button = buttons.optJSONObject(i);
9761036
JSONObject bundle = new JSONObject(fcmJson.toString());
9771037

978-
Intent buttonIntent = getNewBaseIntent(notificationId);
1038+
Intent buttonIntent = intentGenerator.getNewBaseIntent(notificationId);
9791039
buttonIntent.setAction("" + i); // Required to keep each action button from replacing extras of each other
9801040
buttonIntent.putExtra("action_button", true);
9811041
bundle.put(BUNDLE_KEY_ACTION_ID, button.optString("id"));
@@ -985,7 +1045,7 @@ private static void addNotificationActionButtons(JSONObject fcmJson, Notificatio
9851045
else if (fcmJson.has("grp"))
9861046
buttonIntent.putExtra("grp", fcmJson.optString("grp"));
9871047

988-
PendingIntent buttonPIntent = getNewActionPendingIntent(notificationId, buttonIntent);
1048+
PendingIntent buttonPIntent = intentGenerator.getNewActionPendingIntent(notificationId, buttonIntent);
9891049

9901050
int buttonIcon = 0;
9911051
if (button.has("icon"))
@@ -1043,4 +1103,4 @@ private static int convertOSToAndroidPriority(int priority) {
10431103

10441104
return NotificationCompat.PRIORITY_MIN;
10451105
}
1046-
}
1106+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.onesignal
2+
3+
import android.app.PendingIntent
4+
import android.content.Context
5+
import android.content.Intent
6+
7+
class GenerateNotificationOpenIntent(
8+
private val context: Context,
9+
private val intent: Intent?,
10+
private val startApp: Boolean
11+
) {
12+
13+
private val notificationOpenedClass: Class<*> = NotificationOpenedReceiver::class.java
14+
15+
fun getNewBaseIntent(
16+
notificationId: Int,
17+
): Intent {
18+
// We use SINGLE_TOP and CLEAR_TOP as we don't want more than one OneSignal invisible click
19+
// tracking Activity instance around.
20+
var intentFlags =
21+
Intent.FLAG_ACTIVITY_SINGLE_TOP or
22+
Intent.FLAG_ACTIVITY_CLEAR_TOP
23+
if (!startApp) {
24+
// If we don't want the app to launch we put OneSignal's invisible click tracking Activity on it's own task
25+
// so it doesn't resume an existing one once it closes.
26+
intentFlags =
27+
intentFlags or (
28+
Intent.FLAG_ACTIVITY_NEW_TASK or
29+
Intent.FLAG_ACTIVITY_MULTIPLE_TASK or
30+
Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
31+
)
32+
}
33+
34+
return Intent(
35+
context,
36+
notificationOpenedClass
37+
)
38+
.putExtra(
39+
GenerateNotification.BUNDLE_KEY_ANDROID_NOTIFICATION_ID,
40+
notificationId
41+
)
42+
.addFlags(intentFlags)
43+
}
44+
45+
/**
46+
* Creates a PendingIntent to attach to the notification click and it's action button(s).
47+
* If the user interacts with the notification this normally starts the app or resumes it
48+
* unless the app developer disables this via a OneSignal meta-data AndroidManifest.xml setting
49+
*
50+
* The default behavior is to open the app in the same way an Android homescreen launcher does.
51+
* This means we expect the following behavior:
52+
* 1. Starts the Activity defined in the app's AndroidManifest.xml as "android.intent.action.MAIN"
53+
* 2. If the app is already running, instead the last activity will be resumed
54+
* 3. If the app is not running (due to being push out of memory), the last activity will be resumed
55+
* 4. If the app is no longer in the recent apps list, it is not resumed, same as #1 above.
56+
* - App is removed from the recent app's list if it is swiped away or "clear all" is pressed.
57+
*/
58+
fun getNewActionPendingIntent(
59+
requestCode: Int,
60+
oneSignalIntent: Intent,
61+
): PendingIntent? {
62+
val launchIntent = getIntentVisible()
63+
?:
64+
// Even though the default app open action is disabled we still need to attach OneSignal's
65+
// invisible Activity to capture click event to report click counts and etc.
66+
// You may be thinking why not use a BroadcastReceiver instead of an invisible
67+
// Activity? This could be done in a 5.0.0 release but can't be changed now as it is
68+
// unknown if the app developer will be starting there own Activity from their
69+
// OSNotificationOpenedHandler and that would have side-effects.
70+
return PendingIntent.getActivity(
71+
context,
72+
requestCode,
73+
oneSignalIntent,
74+
PendingIntent.FLAG_UPDATE_CURRENT
75+
)
76+
77+
78+
// This setups up a "Reverse Activity Trampoline"
79+
// The first Activity to launch will be oneSignalIntent, which is an invisible
80+
// Activity to track the click, fire OSNotificationOpenedHandler, etc. This Activity
81+
// will finish quickly and the destination Activity, launchIntent, will be shown to the user
82+
// since it is the next in the back stack.
83+
return PendingIntent.getActivities(
84+
context,
85+
requestCode,
86+
arrayOf(launchIntent, oneSignalIntent),
87+
PendingIntent.FLAG_UPDATE_CURRENT
88+
)
89+
}
90+
91+
// Return the provide intent if one was set, otherwise default to opening the app.
92+
private fun getIntentVisible(): Intent? {
93+
if (intent != null) return intent
94+
return getIntentAppOpen()
95+
}
96+
97+
// Provides the default launcher Activity, if the app has one.
98+
// - This is almost always true, one of the few exceptions being an app that is only a widget.
99+
private fun getIntentAppOpen(): Intent? {
100+
if (!startApp) return null
101+
102+
val launchIntent =
103+
context.packageManager.getLaunchIntentForPackage(
104+
context.packageName
105+
)
106+
?: return null
107+
108+
// Removing "package" from the intent treats the app as if it was started externally.
109+
// - This is exactly what an Android Launcher does.
110+
// This prevents another instance of the Activity from being created.
111+
// Android 11 no longer requires nulling this out to get this behavior.
112+
launchIntent.setPackage(null)
113+
launchIntent.flags =
114+
Intent.FLAG_ACTIVITY_NEW_TASK or
115+
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
116+
117+
return launchIntent
118+
}
119+
}

0 commit comments

Comments
 (0)