Skip to content

Commit d922d5b

Browse files
authored
Android 12 trampoline restrictions (#470)
* Bypass LeanplumPushReceiver for API 31 * Add log for 'Too many requests' error in dev mode * Bump compile sdk to 31. Fix tests to runs with Java 11. * Set travis java to 11 * Comment Bouncy Castle library * Fix build
1 parent 04dab56 commit d922d5b

29 files changed

+383
-88
lines changed

.travis.yml

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ language: android
44

55
env:
66
global:
7-
- BUILD_API=30
8-
- BUILD_TOOLS=30.0.3
7+
- JAVA_TOOL_OPTIONS=-Dhttps.protocols=TLSv1.2
8+
- BUILD_API=31
9+
- BUILD_TOOLS=31.0.0
910
- ABI=x86
1011
- EMU_API=22
1112
- EMU_FLAVOR=default
1213
- ANDROID_HOME=/usr/local/android-sdk
13-
- TOOLS=${ANDROID_HOME}/tools
14+
- TOOLS=${ANDROID_HOME}/cmdline-tools/latest
1415
# PATH order is important, the 'emulator' script exists in more than one place
1516
- PATH=${ANDROID_HOME}:${ANDROID_HOME}/emulator:${TOOLS}:${TOOLS}/bin:${ANDROID_HOME}/platform-tools:${PATH}
1617
# If you want to run snapshot tests change value to 'true'. Build time will be increased by 6 minutes.
@@ -20,26 +21,33 @@ android:
2021
components:
2122
- tools
2223

24+
jdk:
25+
- oraclejdk11
26+
2327
licenses:
2428
- 'android-sdk-preview-license-.+'
2529
- 'android-sdk-license-.+'
2630
- 'google-gdk-license-.+'
2731

2832
before_install:
29-
- sudo wget "https://bouncycastle.org/download/bcprov-ext-jdk15on-165.jar" -O "${JAVA_HOME}/jre/lib/ext/bcprov-ext-jdk15on-165.jar"
30-
- sudo echo "security.provider.11=org.bouncycastle.jce.provider.BouncyCastleProvider" | sudo tee -a ${JAVA_HOME}/jre/lib/security/java.security
31-
- if [[ $RUN_SNAPSHOT_TESTS == true ]]; then python -m pip install Tools/Pillow-6.2.2-cp27-cp27mu-manylinux1_x86_64.whl --user; fi; # library for image manipulation in snapshot tests
33+
# Install cmdline-tools (older tools doesn't support Java 11)
34+
- wget -q "https://dl.google.com/android/repository/commandlinetools-linux-7583922_latest.zip" -O android-commandline-tools-linux.zip
35+
- mkdir -p ${ANDROID_HOME}/cmdline-tools
36+
- unzip -q android-commandline-tools-linux.zip -d ${ANDROID_HOME}/cmdline-tools
37+
- mv ${ANDROID_HOME}/cmdline-tools/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest
38+
# Library for image manipulation in snapshot tests
39+
- if [[ $RUN_SNAPSHOT_TESTS == true ]]; then python -m pip install Tools/Pillow-6.2.2-cp27-cp27mu-manylinux1_x86_64.whl --user; fi;
3240

3341
before_script:
3442
# Install Android SDK and run emulator
3543
- echo 'count=0' > /home/travis/.android/repositories.cfg # avoid harmless sdkmanager warning
36-
- echo y | sdkmanager "platform-tools" >/dev/null
37-
- echo y | sdkmanager "tools" >/dev/null
38-
- echo y | sdkmanager "build-tools;$BUILD_TOOLS" >/dev/null
39-
- echo y | sdkmanager "platforms;android-$EMU_API" >/dev/null
40-
- echo y | sdkmanager "platforms;android-$BUILD_API" >/dev/null
41-
- echo y | sdkmanager "extras;android;m2repository" >/dev/null
42-
- if [[ $RUN_SNAPSHOT_TESTS == true ]]; then echo y | sdkmanager "system-images;android-$EMU_API;$EMU_FLAVOR;$ABI"; fi;
44+
- echo y | ${TOOLS}/bin/sdkmanager "platform-tools" >/dev/null
45+
- echo y | ${TOOLS}/bin/sdkmanager "tools" >/dev/null
46+
- echo y | ${TOOLS}/bin/sdkmanager "build-tools;$BUILD_TOOLS" >/dev/null
47+
- echo y | ${TOOLS}/bin/sdkmanager "platforms;android-$EMU_API" >/dev/null
48+
- echo y | ${TOOLS}/bin/sdkmanager "platforms;android-$BUILD_API" >/dev/null
49+
- echo y | ${TOOLS}/bin/sdkmanager "extras;android;m2repository" >/dev/null
50+
- if [[ $RUN_SNAPSHOT_TESTS == true ]]; then echo y | ${TOOLS}/bin/sdkmanager "system-images;android-$EMU_API;$EMU_FLAVOR;$ABI"; fi;
4351
- if [[ $RUN_SNAPSHOT_TESTS == true ]]; then echo no | avdmanager create avd --force -n test -k "system-images;android-$EMU_API;$EMU_FLAVOR;$ABI" -c 100M; fi;
4452
- if [[ $RUN_SNAPSHOT_TESTS == true ]]; then emulator -verbose -avd test -no-accel -no-snapshot -no-window -no-audio -camera-back none -camera-front none -selinux permissive -qemu -m 2048 & fi;
4553

AndroidSDKCore/src/main/java/com/leanplum/LeanplumActivityHelper.java

Lines changed: 83 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,24 @@
2121

2222
package com.leanplum;
2323

24+
import android.annotation.TargetApi;
2425
import android.app.Activity;
2526
import android.app.Application;
2627
import android.app.Application.ActivityLifecycleCallbacks;
2728
import android.content.Context;
29+
import android.content.Intent;
2830
import android.content.res.Resources;
2931
import android.os.Bundle;
3032

3133
import com.leanplum.annotations.Parser;
3234
import com.leanplum.callbacks.PostponableAction;
3335
import com.leanplum.internal.ActionManager;
36+
import com.leanplum.internal.Constants;
3437
import com.leanplum.internal.LeanplumInternal;
3538
import com.leanplum.internal.Log;
36-
import com.leanplum.internal.Util;
3739

40+
import com.leanplum.internal.OperationQueue;
41+
import com.leanplum.utils.BuildUtil;
3842
import java.util.Collections;
3943
import java.util.HashSet;
4044
import java.util.LinkedList;
@@ -106,58 +110,98 @@ public static boolean isActivityPaused() {
106110
}
107111

108112
/**
109-
* Enables lifecycle callbacks for Android devices with Android OS >= 4.0
113+
* Class provides additional functionality to handle payloads of push notifications built to
114+
* comply with new Android 12 restrictions on using notification trampolines.
115+
* The intent contains the message bundle which is used to run the open action and to track
116+
* 'Push Opened' and 'Open' events.
110117
*/
111-
public static void enableLifecycleCallbacks(final Application app) {
112-
Leanplum.setApplicationContext(app.getApplicationContext());
113-
app.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
114-
@Override
115-
public void onActivityStopped(Activity activity) {
116-
try {
117-
onStop(activity);
118-
} catch (Throwable t) {
119-
Log.exception(t);
120-
}
121-
}
118+
@TargetApi(31)
119+
static class NoTrampolinesLifecycleCallbacks extends LeanplumLifecycleCallbacks {
122120

123-
@Override
124-
public void onActivityResumed(final Activity activity) {
125-
try {
126-
onResume(activity);
127-
if (Leanplum.isScreenTrackingEnabled()) {
128-
Leanplum.advanceTo(activity.getLocalClassName());
129-
}
130-
} catch (Throwable t) {
131-
Log.exception(t);
121+
@Override
122+
public void onActivityResumed(Activity activity) {
123+
super.onActivityResumed(activity);
124+
125+
if (activity.getIntent() != null) {
126+
Bundle extras = activity.getIntent().getExtras();
127+
if (extras != null && extras.containsKey(Constants.Keys.PUSH_MESSAGE_TEXT)) {
128+
OperationQueue.sharedInstance().addParallelOperation(
129+
() -> handleNotificationPayload(extras));
132130
}
133131
}
132+
}
134133

135-
@Override
136-
public void onActivityPaused(Activity activity) {
137-
try {
138-
onPause(activity);
139-
} catch (Throwable t) {
140-
Log.exception(t);
141-
}
134+
private void handleNotificationPayload(Bundle message) {
135+
try {
136+
Class.forName("com.leanplum.LeanplumPushService")
137+
.getDeclaredMethod("onActivityNotificationClick", Bundle.class)
138+
.invoke(null, message);
139+
} catch (Throwable t) {
140+
Log.e("Push Notification action not run. Did you forget leanplum-push module?", t);
142141
}
142+
}
143+
}
143144

144-
@Override
145-
public void onActivityStarted(Activity activity) {
145+
static class LeanplumLifecycleCallbacks implements ActivityLifecycleCallbacks {
146+
@Override
147+
public void onActivityStopped(Activity activity) {
148+
try {
149+
onStop(activity);
150+
} catch (Throwable t) {
151+
Log.exception(t);
146152
}
153+
}
147154

148-
@Override
149-
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
155+
@Override
156+
public void onActivityResumed(final Activity activity) {
157+
try {
158+
onResume(activity);
159+
if (Leanplum.isScreenTrackingEnabled()) {
160+
Leanplum.advanceTo(activity.getLocalClassName());
161+
}
162+
} catch (Throwable t) {
163+
Log.exception(t);
150164
}
165+
}
151166

152-
@Override
153-
public void onActivityDestroyed(Activity activity) {
167+
@Override
168+
public void onActivityPaused(Activity activity) {
169+
try {
170+
onPause(activity);
171+
} catch (Throwable t) {
172+
Log.exception(t);
154173
}
174+
}
155175

156-
@Override
157-
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
158-
}
176+
@Override
177+
public void onActivityStarted(Activity activity) {
178+
}
179+
180+
@Override
181+
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
182+
}
183+
184+
@Override
185+
public void onActivityDestroyed(Activity activity) {
186+
}
187+
188+
@Override
189+
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
190+
}
191+
}
192+
193+
/**
194+
* Enables lifecycle callbacks for Android devices with Android OS >= 4.0
195+
*/
196+
public static void enableLifecycleCallbacks(final Application app) {
197+
Leanplum.setApplicationContext(app.getApplicationContext());
198+
199+
if (BuildUtil.shouldDisableTrampolines(app)) {
200+
app.registerActivityLifecycleCallbacks(new NoTrampolinesLifecycleCallbacks());
201+
} else {
202+
app.registerActivityLifecycleCallbacks(new LeanplumLifecycleCallbacks());
203+
}
159204

160-
});
161205
registeredCallbacks = true;
162206
// run pending actions if any upon start
163207
LeanplumInternal.addStartIssuedHandler(runPendingActionsRunnable);

AndroidSDKCore/src/main/java/com/leanplum/internal/ActionManager.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.leanplum.Leanplum;
3232
import com.leanplum.LocationManager;
3333
import com.leanplum.callbacks.ActionCallback;
34+
import com.leanplum.internal.Constants.Defaults;
3435
import com.leanplum.utils.SharedPreferencesUtil;
3536

3637
import java.util.HashMap;
@@ -58,7 +59,6 @@ public class ActionManager {
5859
public static final String HELD_BACK_ACTION_NAME = "__held_back";
5960
private static final String LEANPLUM_LOCAL_PUSH_HELPER =
6061
"com.leanplum.internal.LeanplumLocalPushHelper";
61-
private static final String PREFERENCES_NAME = "__leanplum_messaging__";
6262
private static LocationManager locationManager;
6363
private static boolean loggedLocationManagerFailure = false;
6464

@@ -157,7 +157,7 @@ public boolean onResponse(ActionContext actionContext) {
157157
// Get existing eta and clear notification from preferences.
158158
Context context = Leanplum.getContext();
159159
SharedPreferences preferences = context.getSharedPreferences(
160-
PREFERENCES_NAME, Context.MODE_PRIVATE);
160+
Defaults.MESSAGING_PREF_NAME, Context.MODE_PRIVATE);
161161
String preferencesKey = String.format(Constants.Defaults.LOCAL_NOTIFICATION_KEY,
162162
messageId);
163163
long existingEta = preferences.getLong(preferencesKey, 0L);
@@ -193,7 +193,7 @@ public Map<String, Number> getMessageImpressionOccurrences(String messageId) {
193193
}
194194
Context context = Leanplum.getContext();
195195
SharedPreferences preferences = context.getSharedPreferences(
196-
PREFERENCES_NAME, Context.MODE_PRIVATE);
196+
Defaults.MESSAGING_PREF_NAME, Context.MODE_PRIVATE);
197197
String savedValue = preferences.getString(
198198
String.format(Constants.Defaults.MESSAGE_IMPRESSION_OCCURRENCES_KEY, messageId),
199199
"{}");
@@ -205,7 +205,7 @@ public Map<String, Number> getMessageImpressionOccurrences(String messageId) {
205205
public void saveMessageImpressionOccurrences(Map<String, Number> occurrences, String messageId) {
206206
Context context = Leanplum.getContext();
207207
SharedPreferences preferences = context.getSharedPreferences(
208-
PREFERENCES_NAME, Context.MODE_PRIVATE);
208+
Defaults.MESSAGING_PREF_NAME, Context.MODE_PRIVATE);
209209
SharedPreferences.Editor editor = preferences.edit();
210210
editor.putString(
211211
String.format(Constants.Defaults.MESSAGE_IMPRESSION_OCCURRENCES_KEY, messageId),
@@ -221,7 +221,7 @@ public int getMessageTriggerOccurrences(String messageId) {
221221
}
222222
Context context = Leanplum.getContext();
223223
SharedPreferences preferences = context.getSharedPreferences(
224-
PREFERENCES_NAME, Context.MODE_PRIVATE);
224+
Defaults.MESSAGING_PREF_NAME, Context.MODE_PRIVATE);
225225
int savedValue = preferences.getInt(
226226
String.format(Constants.Defaults.MESSAGE_TRIGGER_OCCURRENCES_KEY, messageId), 0);
227227
messageTriggerOccurrences.put(messageId, savedValue);
@@ -231,7 +231,7 @@ public int getMessageTriggerOccurrences(String messageId) {
231231
public void saveMessageTriggerOccurrences(int occurrences, String messageId) {
232232
Context context = Leanplum.getContext();
233233
SharedPreferences preferences = context.getSharedPreferences(
234-
PREFERENCES_NAME, Context.MODE_PRIVATE);
234+
Defaults.MESSAGING_PREF_NAME, Context.MODE_PRIVATE);
235235
SharedPreferences.Editor editor = preferences.edit();
236236
editor.putInt(
237237
String.format(Constants.Defaults.MESSAGE_TRIGGER_OCCURRENCES_KEY, messageId), occurrences);
@@ -246,7 +246,7 @@ public MessageMatchResult shouldShowMessage(String messageId, Map<String, Object
246246
// 1. Must not be muted.
247247
Context context = Leanplum.getContext();
248248
SharedPreferences preferences = context.getSharedPreferences(
249-
PREFERENCES_NAME, Context.MODE_PRIVATE);
249+
Defaults.MESSAGING_PREF_NAME, Context.MODE_PRIVATE);
250250
if (preferences.getBoolean(
251251
String.format(Constants.Defaults.MESSAGE_MUTED_KEY, messageId), false)) {
252252
return result;
@@ -558,7 +558,7 @@ public void muteFutureMessagesOfKind(String messageId) {
558558
if (messageId != null) {
559559
Context context = Leanplum.getContext();
560560
SharedPreferences preferences = context.getSharedPreferences(
561-
PREFERENCES_NAME, Context.MODE_PRIVATE);
561+
Defaults.MESSAGING_PREF_NAME, Context.MODE_PRIVATE);
562562
SharedPreferences.Editor editor = preferences.edit();
563563
editor.putBoolean(
564564
String.format(Constants.Defaults.MESSAGE_MUTED_KEY, messageId),
@@ -661,7 +661,7 @@ private int countOccurrences(long startTime, long endTime) {
661661

662662
Context context = Leanplum.getContext();
663663
SharedPreferences prefs =
664-
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
664+
context.getSharedPreferences(Defaults.MESSAGING_PREF_NAME, Context.MODE_PRIVATE);
665665
Map<String, ?> all = prefs.getAll();
666666

667667
int occurrenceCount = 0;

AndroidSDKCore/src/main/java/com/leanplum/internal/Clock.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
* mock in unit tests.
3030
*/
3131
public abstract class Clock {
32+
public static final long DAY_MILLIS = 24 * 60 * 60 * 1000;
33+
public static final long MONTH_MILLIS = 30 * DAY_MILLIS;
3234

3335
public static final Clock SYSTEM = new Clock() {
3436
@Override
@@ -68,4 +70,13 @@ static void setInstance(Clock clock) {
6870
*/
6971
@NonNull
7072
abstract Date newDate();
73+
74+
/**
75+
* Checks if time is not from more than 30 days ago.
76+
*
77+
* @return True if elapsed time is less than a month.
78+
*/
79+
public boolean lessThanMonthAgo(long timeInMillis) {
80+
return currentTimeMillis() - timeInMillis < MONTH_MILLIS;
81+
}
7182
}

AndroidSDKCore/src/main/java/com/leanplum/internal/Constants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ public static class Defaults {
7777
public static final String TOKEN_KEY = "__leanplum_token";
7878
public static final String MESSAGES_KEY = "__leanplum_messages";
7979
public static final String REGIONS_KEY = "regions";
80+
public static final String MESSAGING_PREF_NAME = "__leanplum_messaging__";
8081
public static final String MESSAGE_TRIGGER_OCCURRENCES_KEY =
8182
"__leanplum_message_trigger_occurrences_%s";
8283
public static final String MESSAGE_IMPRESSION_OCCURRENCES_KEY =
@@ -191,6 +192,7 @@ public static class Keys {
191192
public static final String PUSH_MESSAGE_SILENT_TRACK = "lp_silent_track";
192193
public static final String PUSH_SENT_TIME = "lp_sent_time";
193194
public static final String PUSH_OCCURRENCE_ID = "lp_occurrence_id";
195+
public static final String LOCAL_PUSH_OCCURRENCE_ID = "internal_occurrence_id";
194196
public static final String REGION = "region";
195197
public static final String REGION_STATE = "regionState";
196198
public static final String REGIONS = "regions";

AndroidSDKCore/src/main/java/com/leanplum/internal/RequestSender.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ public void sendRequests() {
154154
}
155155
} else {
156156
Exception errorException = new Exception("HTTP error " + statusCode);
157+
String errorMessage = errorException.getMessage();
158+
if (responseBody != null) {
159+
errorMessage += " " + responseBody.toString();
160+
}
161+
Log.i(errorMessage);
162+
157163
if (statusCode != -1
158164
&& statusCode != 408
159165
&& statusCode != 429

AndroidSDKCore/src/main/java/com/leanplum/utils/BuildUtil.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017, Leanplum, Inc. All rights reserved.
2+
* Copyright 2021, Leanplum, Inc. All rights reserved.
33
*
44
* Licensed to the Apache Software Foundation (ASF) under one
55
* or more contributor license agreements. See the NOTICE file
@@ -42,6 +42,18 @@ public static boolean isNotificationChannelSupported(Context context) {
4242
return Build.VERSION.SDK_INT >= 26 && getTargetSdkVersion(context) >= 26;
4343
}
4444

45+
/**
46+
* Checks whether notification trampolines are not supported.
47+
* Targeting Android 12 means you cannot use a service or broadcast receiver as a trampoline to
48+
* start an activity. The activity must be started immediately when notification is clicked.
49+
*
50+
* @param context The application context.
51+
* @return True if notification trampolines are not supported.
52+
*/
53+
public static boolean shouldDisableTrampolines(Context context) {
54+
return Build.VERSION.SDK_INT >= 31 && getTargetSdkVersion(context) >= 31;
55+
}
56+
4557
/**
4658
* Returns target SDK version parsed from manifest.
4759
*

0 commit comments

Comments
 (0)