Bug 1566406 Upgrade Leanplum SDK from 3.0.2 to 4.2.7 r=petru

Differential Revision: https://phabricator.services.mozilla.com/D38675

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Andrei Lazar 2019-07-24 14:27:19 +00:00
parent 5638261f4a
commit 95c2abace2
68 changed files with 4094 additions and 4193 deletions

View File

@ -1,30 +1,34 @@
<receiver android:name="com.leanplum.LeanplumPushReceiver" android:exported="false"
<receiver android:name="com.leanplum.LeanplumPushReceiver"
android:exported="false"
android:enabled="true">
<intent-filter>
<action android:name="com.leanplum.LeanplumPushListenerService" />
</intent-filter>
</receiver>
<!-- Leanplum Local Push Notification Service-->
<service android:name="com.leanplum.LeanplumLocalPushListenerService" android:exported="false"
android:enabled="true" />
<service android:name="com.leanplum.LeanplumPushListenerService" android:exported="false">
<!-- Leanplum GCM Message Handling Service. -->
<service
android:name="com.leanplum.LeanplumPushListenerService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="@ANDROID_PACKAGE_NAME@" />
<action android:name="com.google.android.c2dm.intent.RECEIVE"/>
<category android:name="@ANDROID_PACKAGE_NAME@" />
</intent-filter>
</service>
<!-- Leanplum GCM Registration Job Service. -->
<service android:name="com.leanplum.LeanplumGcmRegistrationJobService"
<!-- Leanplum GCM Registration Job Service. -->
<service
android:name="com.leanplum.LeanplumGcmRegistrationJobService"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<!-- Leanplum GCM Instance ID Service -->
<service android:name="com.leanplum.LeanplumPushInstanceIDService" android:exported="false"
android:enabled="true">
<!-- Leanplum GCM Instance ID Service. -->
<service
android:name="com.leanplum.LeanplumPushInstanceIDService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.google.android.gms.iid.InstanceID" />
<action android:name="com.google.android.gms.iid.InstanceID"/>
</intent-filter>
</service>

View File

@ -13,6 +13,7 @@ import android.content.Context;
import android.os.Bundle;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import com.leanplum.Leanplum;
@ -80,12 +81,7 @@ public class MmaLeanplumImp implements MmaInterface {
// SDK initialization and Activity lifecycle in the future.
//
// I put it under runOnUiThread because in current Leanplum's SDK design, this should be run in main thread.
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
LeanplumActivityHelper.onResume(activity);
}
});
activity.runOnUiThread(() -> LeanplumActivityHelper.onResume(activity));
}
@SuppressLint("NewApi")
@ -101,6 +97,11 @@ public class MmaLeanplumImp implements MmaInterface {
.getNotificationChannel(NotificationHelper.Channel.DEFAULT).getId());
}
}
@Override
public void customize(Notification.Builder builder, Bundle notificationPayload, @Nullable Notification.Style notificationStyle) {
}
});
}
@ -147,7 +148,7 @@ public class MmaLeanplumImp implements MmaInterface {
@Override
public boolean handleGcmMessage(Context context, String from, Bundle bundle) {
if (from != null && from.equals(MmaConstants.MOZ_MMA_SENDER_ID) && bundle.containsKey(Constants.Keys.PUSH_MESSAGE_TEXT)) {
if (from != null && bundle.containsKey(Constants.Keys.PUSH_MESSAGE_TEXT)) {
LeanplumPushService.handleNotification(context, bundle);
return true;
}

View File

@ -4,6 +4,7 @@ apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
useLibrary 'org.apache.http.legacy'
defaultConfig {
targetSdkVersion project.ext.targetSdkVersion

View File

@ -369,6 +369,7 @@ public class ActionContext extends BaseActionContext implements Comparable<Actio
createActionContextForMessageId(messageAction.toString(), args, messageId, name, false);
}
}
Leanplum.countAggregator().incrementCount("run_action_named");
}
/**
@ -388,7 +389,7 @@ public class ActionContext extends BaseActionContext implements Comparable<Actio
@Override
public void variablesChanged() {
try {
ActionManager.getInstance().recordMessageImpression(actionContext.getMessageId());
Leanplum.triggerMessageDisplayed(actionContext);
} catch (Throwable t) {
Util.handleException(t);
}
@ -526,6 +527,7 @@ public class ActionContext extends BaseActionContext implements Comparable<Actio
} catch (Throwable t) {
Util.handleException(t);
}
Leanplum.countAggregator().incrementCount("run_tracked_action_named");
}
/**

View File

@ -22,24 +22,26 @@
package com.leanplum;
import android.app.Activity;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.os.AsyncTask;
import android.support.v4.app.NotificationCompat;
import android.os.Message;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import com.leanplum.ActionContext.ContextualValues;
import com.leanplum.callbacks.ActionCallback;
import com.leanplum.callbacks.MessageDisplayedCallback;
import com.leanplum.callbacks.RegisterDeviceCallback;
import com.leanplum.callbacks.RegisterDeviceFinishedCallback;
import com.leanplum.callbacks.StartCallback;
import com.leanplum.callbacks.VariablesChangedCallback;
import com.leanplum.internal.ActionManager;
import com.leanplum.internal.Constants;
import com.leanplum.internal.FileManager;
import com.leanplum.internal.JsonConverter;
import com.leanplum.internal.CountAggregator;
import com.leanplum.internal.FeatureFlagManager;
import com.leanplum.internal.LeanplumEventDataManager;
import com.leanplum.internal.LeanplumInternal;
import com.leanplum.internal.LeanplumMessageMatchFilter;
@ -47,11 +49,12 @@ import com.leanplum.internal.LeanplumUIEditorWrapper;
import com.leanplum.internal.Log;
import com.leanplum.internal.OsHandler;
import com.leanplum.internal.Registration;
import com.leanplum.internal.Request;
import com.leanplum.internal.RequestOld;
import com.leanplum.internal.Util;
import com.leanplum.internal.Util.DeviceIdInfo;
import com.leanplum.internal.VarCache;
import com.leanplum.messagetemplates.MessageTemplates;
import com.leanplum.models.MessageArchiveData;
import com.leanplum.utils.BuildUtil;
import com.leanplum.utils.SharedPreferencesUtil;
@ -61,8 +64,10 @@ import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@ -81,6 +86,7 @@ public class Leanplum {
* Default event name to use for Purchase events.
*/
public static final String PURCHASE_EVENT_NAME = "Purchase";
private static final String LEANPLUM_PUSH_SERVICE = "com.leanplum.LeanplumPushService";
private static final ArrayList<StartCallback> startHandlers = new ArrayList<>();
private static final ArrayList<VariablesChangedCallback> variablesChangedHandlers =
@ -89,7 +95,11 @@ public class Leanplum {
new ArrayList<>();
private static final ArrayList<VariablesChangedCallback> onceNoDownloadsHandlers =
new ArrayList<>();
private static final ArrayList<MessageDisplayedCallback> messageDisplayedHandlers =
new ArrayList<>();
private static final Object heartbeatLock = new Object();
private static final String LEANPLUM_NOTIFICATION_CHANNEL =
"com.leanplum.LeanplumNotificationChannel";
private static RegisterDeviceCallback registerDeviceHandler;
private static RegisterDeviceFinishedCallback registerDeviceFinishedHandler;
private static LeanplumDeviceIdMode deviceIdMode = LeanplumDeviceIdMode.MD5_MAC_ADDRESS;
@ -98,11 +108,14 @@ public class Leanplum {
private static boolean userSpecifiedDeviceId;
private static boolean initializedMessageTemplates = false;
private static boolean locationCollectionEnabled = true;
private static ScheduledExecutorService heartbeatExecutor;
private static ScheduledExecutorService heartbeatExecutor = null;
private static Context context;
private static Runnable pushStartCallback;
private static CountAggregator countAggregator = new CountAggregator();
private static FeatureFlagManager featureFlagManager = FeatureFlagManager.INSTANCE;
private Leanplum() {
}
@ -221,7 +234,7 @@ public class Leanplum {
}
Constants.isDevelopmentModeEnabled = true;
Request.setAppId(appId, accessKey);
RequestOld.setAppId(appId, accessKey);
}
/**
@ -242,7 +255,7 @@ public class Leanplum {
}
Constants.isDevelopmentModeEnabled = false;
Request.setAppId(appId, accessKey);
RequestOld.setAppId(appId, accessKey);
}
/**
@ -252,6 +265,16 @@ public class Leanplum {
LeanplumInternal.enableAutomaticScreenTracking();
}
/**
* Set this to true if you want details about the variable assignments
* on the server.
* Default is NO.
*/
public static void setVariantDebugInfoEnabled(boolean variantDebugInfoEnabled) {
LeanplumInternal.setIsVariantDebugInfoEnabled(variantDebugInfoEnabled);
countAggregator.incrementCount("set_variant_debug_info_enabled");
}
/**
* Whether screen tracking is enabled or not.
*
@ -307,7 +330,7 @@ public class Leanplum {
Log.e("Leanplum.start() must be called before calling getDeviceId.");
return null;
}
return Request.deviceId();
return RequestOld.deviceId();
}
/**
@ -368,6 +391,7 @@ public class Leanplum {
} catch (Throwable t) {
Util.handleException(t);
}
countAggregator.incrementCount("sync_resources");
}
/**
@ -383,6 +407,7 @@ public class Leanplum {
} catch (Throwable t) {
Util.handleException(t);
}
countAggregator.incrementCount("sync_resources");
}
/**
@ -404,6 +429,7 @@ public class Leanplum {
} catch (Throwable t) {
Util.handleException(t);
}
countAggregator.incrementCount("sync_resource_paths");
}
/**
@ -425,6 +451,7 @@ public class Leanplum {
} catch (Throwable t) {
Util.handleException(t);
}
countAggregator.incrementCount("sync_resource_paths");
}
/**
@ -521,7 +548,8 @@ public class Leanplum {
VarCache.getUpdateRuleDiffs(),
VarCache.getEventRuleDiffs(),
new HashMap<String, Object>(),
new ArrayList<Map<String, Object>>());
new ArrayList<Map<String, Object>>(),
new HashMap<String, Object>());
LeanplumInbox.getInstance().update(new HashMap<String, LeanplumInboxMessage>(), 0, false);
return;
}
@ -558,7 +586,7 @@ public class Leanplum {
LeanplumInternal.getUserAttributeChanges().add(validAttributes);
}
Request.loadToken();
RequestOld.loadToken();
VarCache.setSilent(true);
VarCache.loadDiffs();
VarCache.setSilent(false);
@ -569,12 +597,12 @@ public class Leanplum {
@Override
public void updateCache() {
triggerVariablesChanged();
if (Request.numPendingDownloads() == 0) {
if (RequestOld.numPendingDownloads() == 0) {
triggerVariablesChangedAndNoDownloadsPending();
}
}
});
Request.onNoPendingDownloads(new Request.NoPendingDownloadsCallback() {
RequestOld.onNoPendingDownloads(new RequestOld.NoPendingDownloadsCallback() {
@Override
public void noPendingDownloads() {
triggerVariablesChangedAndNoDownloadsPending();
@ -593,18 +621,35 @@ public class Leanplum {
return null;
}
});
Util.initExceptionHandling(context);
} catch (Throwable t) {
Util.handleException(t);
}
countAggregator.incrementCount("start_with_user_id");
}
/**
* Checks for leanplum notifications modules and if someone present - invoke onStart method.
*/
private static void checkAndStartNotificationsModules() {
if (Util.hasPlayServices()) {
try {
Class.forName(LEANPLUM_PUSH_SERVICE).getDeclaredMethod("onStart")
.invoke(null);
} catch (Throwable ignored) {
}
} else {
Log.i("No valid Google Play Services APK found.");
}
}
private static void startHelper(
String userId, final Map<String, ?> attributes, final boolean isBackground) {
LeanplumEventDataManager.init(context);
LeanplumPushService.onStart();
checkAndStartNotificationsModules();
Boolean limitAdTracking = null;
String deviceId = Request.deviceId();
String deviceId = RequestOld.deviceId();
if (deviceId == null) {
if (!userSpecifiedDeviceId && Constants.defaultDeviceId != null) {
deviceId = Constants.defaultDeviceId;
@ -615,16 +660,16 @@ public class Leanplum {
deviceId = deviceIdInfo.id;
limitAdTracking = deviceIdInfo.limitAdTracking;
}
Request.setDeviceId(deviceId);
RequestOld.setDeviceId(deviceId);
}
if (userId == null) {
userId = Request.userId();
userId = RequestOld.userId();
if (userId == null) {
userId = Request.deviceId();
userId = RequestOld.deviceId();
}
}
Request.setUserId(userId);
RequestOld.setUserId(userId);
// Setup parameters.
String versionName = Util.getVersionName();
@ -649,7 +694,9 @@ public class Leanplum {
params.put(Constants.Params.DEVICE_MODEL, Util.getDeviceModel());
params.put(Constants.Params.DEVICE_SYSTEM_NAME, Util.getSystemName());
params.put(Constants.Params.DEVICE_SYSTEM_VERSION, Util.getSystemVersion());
params.put(Constants.Params.DEVICE_PUSH_TOKEN, registrationId);
if (!TextUtils.isEmpty(registrationId)) {
params.put(Constants.Params.DEVICE_PUSH_TOKEN, registrationId);
}
params.put(Constants.Keys.TIMEZONE, localTimeZone.getID());
params.put(Constants.Keys.TIMEZONE_OFFSET_SECONDS, Integer.toString(timezoneOffsetSeconds));
params.put(Constants.Keys.LOCALE, Util.getLocale());
@ -670,11 +717,13 @@ public class Leanplum {
// Get the current inbox messages on the device.
params.put(Constants.Params.INBOX_MESSAGES, LeanplumInbox.getInstance().messagesIds());
params.put(Constants.Params.INCLUDE_VARIANT_DEBUG_INFO, LeanplumInternal.getIsVariantDebugInfoEnabled());
Util.initializePreLeanplumInstall(params);
// Issue start API call.
final Request request = Request.post(Constants.Methods.START, params);
request.onApiResponse(new Request.ApiResponseCallback() {
final RequestOld request = RequestOld.post(Constants.Methods.START, params);
request.onApiResponse(new RequestOld.ApiResponseCallback() {
@Override
public void response(List<Map<String, Object>> requests, JSONObject response, int countOfEvents) {
Leanplum.handleApiResponse(response, requests, request, countOfEvents);
@ -691,8 +740,7 @@ public class Leanplum {
}
private static void handleApiResponse(JSONObject response, List<Map<String, Object>> requests,
final Request request, int countOfUnsentRequests) {
boolean hasStartResponse = false;
final RequestOld request, int countOfUnsentRequests) {
JSONObject lastStartResponse = null;
// Find and handle the last start response.
@ -704,23 +752,12 @@ public class Leanplum {
request.setDataBaseIndex(request.getDataBaseIndex() - countOfUnsentRequests);
return;
}
final int responseCount = Request.numResponses(response);
for (int i = requests.size() - 1; i >= 0; i--) {
Map<String, Object> currentRequest = requests.get(i);
if (Constants.Methods.START.equals(currentRequest.get(Constants.Params.ACTION))) {
if (i < responseCount) {
lastStartResponse = Request.getResponseAt(response, i);
}
hasStartResponse = true;
break;
}
}
lastStartResponse = parseLastStartResponse(response, requests);
} catch (Throwable t) {
Util.handleException(t);
}
if (hasStartResponse) {
if (lastStartResponse != null) {
if (!LeanplumInternal.hasStarted()) {
// Set start response to null.
request.onApiResponse(null);
@ -729,11 +766,35 @@ public class Leanplum {
}
}
@VisibleForTesting
public static JSONObject parseLastStartResponse(JSONObject response, List<Map<String, Object>> requests) {
final int responseCount = RequestOld.numResponses(response);
for (int i = requests.size() - 1; i >= 0; i--) {
Map<String, Object> currentRequest = requests.get(i);
if (Constants.Methods.START.equals(currentRequest.get(Constants.Params.ACTION))) {
if (currentRequest.containsKey(RequestOld.REQUEST_ID_KEY)) {
for (int j = RequestOld.numResponses(response) - 1; j >= 0; j--) {
JSONObject currentResponse = RequestOld.getResponseAt(response, j);
if (currentRequest.get(RequestOld.REQUEST_ID_KEY)
.equals(currentResponse.optString(RequestOld.REQUEST_ID_KEY))) {
return currentResponse;
}
}
}
if (i < responseCount) {
return RequestOld.getResponseAt(response, i);
}
}
}
return null;
}
private static void handleStartResponse(final JSONObject response) {
Util.executeAsyncTask(false, new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
boolean success = Request.isResponseSuccess(response);
boolean success = RequestOld.isResponseSuccess(response);
Leanplum.countAggregator().incrementCount("on_start_response");
if (!success) {
try {
LeanplumInternal.setHasStarted(true);
@ -781,24 +842,23 @@ public class Leanplum {
String defaultNotificationChannel = response.optString(
Constants.Keys.DEFAULT_NOTIFICATION_CHANNEL);
// Configure notification channels and groups
LeanplumNotificationChannel.configureNotificationGroups(
context, notificationGroups);
LeanplumNotificationChannel.configureNotificationChannels(
context, notificationChannels);
if (notificationChannels == null && (defaultNotificationChannel == null
|| defaultNotificationChannel.isEmpty())) {
defaultNotificationChannel = getDefaultChannelId();
}
LeanplumNotificationChannel.configureDefaultNotificationChannel(
context, defaultNotificationChannel);
try {
Class.forName(LEANPLUM_NOTIFICATION_CHANNEL)
.getDeclaredMethod("configureChannels", Context.class, JSONArray.class,
JSONArray.class, String.class).invoke(new Object(), context,
notificationGroups, notificationChannels, defaultNotificationChannel);
} catch (Throwable ignored) {
}
}
String token = response.optString(Constants.Keys.TOKEN, null);
Request.setToken(token);
Request.saveToken();
RequestOld.setToken(token);
RequestOld.saveToken();
applyContentInResponse(response, true);
@ -813,6 +873,12 @@ public class Leanplum {
Constants.loggingEnabled = true;
}
Set<String> enabledCounters = parseSdkCounters(response);
countAggregator.setEnabledCounters(enabledCounters);
Set<String> enabledFeatureFlags = parseFeatureFlags(response);
FeatureFlagManager.INSTANCE.setEnabledFeatureFlags((enabledFeatureFlags));
parseVariantDebugInfo(response);
// Allow bidirectional realtime variable updates.
if (Constants.isDevelopmentModeEnabled) {
@ -861,24 +927,10 @@ public class Leanplum {
@Override
public void run() {
try {
NotificationCompat.Builder builder =
LeanplumNotificationHelper.getDefaultCompatNotificationBuilder(context,
BuildUtil.isNotificationChannelSupported(context));
if (builder == null) {
return;
}
builder.setSmallIcon(android.R.drawable.star_on)
.setContentTitle("Leanplum")
.setContentText("Your device is registered.");
builder.setContentIntent(PendingIntent.getActivity(
currentContext.getApplicationContext(), 0, new Intent(), 0));
NotificationManager mNotificationManager =
(NotificationManager) currentContext.getSystemService(
Context.NOTIFICATION_SERVICE);
// mId allows you to update the notification later on.
mNotificationManager.notify(0, builder.build());
} catch (Throwable t) {
Log.i("Device is registered.");
Class.forName(Leanplum.LEANPLUM_PUSH_SERVICE)
.getDeclaredMethod("showDeviceRegistedPush", Context.class,
Context.class).invoke(new Object(), context, currentContext);
} catch (Throwable ignored) {
}
}
});
@ -948,6 +1000,8 @@ public class Leanplum {
response.optJSONObject(Constants.Keys.REGIONS));
List<Map<String, Object>> variants = JsonConverter.listFromJsonOrDefault(
response.optJSONArray(Constants.Keys.VARIANTS));
Map<String, Object> variantDebugInfo = JsonConverter.mapFromJsonOrDefault(
response.optJSONObject(Constants.Keys.VARIANT_DEBUG_INFO));
if (alwaysApply
|| !values.equals(VarCache.getDiffs())
@ -956,7 +1010,7 @@ public class Leanplum {
|| !eventRules.equals(VarCache.getEventRuleDiffs())
|| !regions.equals(VarCache.regions())) {
VarCache.applyVariableDiffs(values, messages, updateRules,
eventRules, regions, variants);
eventRules, regions, variants, variantDebugInfo);
}
}
@ -998,7 +1052,7 @@ public class Leanplum {
}
private static void pauseInternal() {
Request.post(Constants.Methods.PAUSE_SESSION, null).sendIfConnected();
RequestOld.post(Constants.Methods.PAUSE_SESSION, null).sendIfConnected();
pauseHeartbeat();
LeanplumInternal.setIsPaused(true);
}
@ -1032,7 +1086,7 @@ public class Leanplum {
}
private static void resumeInternal() {
Request request = Request.post(Constants.Methods.RESUME_SESSION, null);
RequestOld request = RequestOld.post(Constants.Methods.RESUME_SESSION, null);
if (LeanplumInternal.hasStartedInBackground()) {
LeanplumInternal.setStartedInBackground(false);
request.sendIfConnected();
@ -1050,16 +1104,9 @@ public class Leanplum {
*/
private static void startHeartbeat() {
synchronized (heartbeatLock) {
heartbeatExecutor = Executors.newSingleThreadScheduledExecutor();
heartbeatExecutor.scheduleAtFixedRate(new Runnable() {
public void run() {
try {
Request.post(Constants.Methods.HEARTBEAT, null).sendIfDelayed();
} catch (Throwable t) {
Util.handleException(t);
}
}
}, 15, 15, TimeUnit.MINUTES);
if (heartbeatExecutor == null) {
createHeartbeatExecutor();
}
}
}
@ -1067,6 +1114,7 @@ public class Leanplum {
synchronized (heartbeatLock) {
if (heartbeatExecutor != null) {
heartbeatExecutor.shutdown();
heartbeatExecutor = null;
}
}
}
@ -1075,6 +1123,19 @@ public class Leanplum {
startHeartbeat();
}
private static void createHeartbeatExecutor() {
heartbeatExecutor = Executors.newSingleThreadScheduledExecutor();
heartbeatExecutor.scheduleAtFixedRate(new Runnable() {
public void run() {
try {
RequestOld.post(Constants.Methods.HEARTBEAT, null).sendIfDelayed();
} catch (Throwable t) {
Util.handleException(t);
}
}
}, 15, 15, TimeUnit.MINUTES);
}
/**
* Call this to explicitly end the session. This should not be used in most cases, so we won't
* make it public for now.
@ -1105,7 +1166,7 @@ public class Leanplum {
}
private static void stopInternal() {
Request.post(Constants.Methods.STOP, null).sendIfConnected();
RequestOld.post(Constants.Methods.STOP, null).sendIfConnected();
}
/**
@ -1115,6 +1176,19 @@ public class Leanplum {
return LeanplumInternal.hasStarted();
}
/**
* Returns the userId in the current Leanplum session. This should only be called after
* Leanplum.start().
*/
public static String getUserId() {
if (hasStarted()) {
return RequestOld.userId();
} else {
Log.e("Leanplum.start() must be called before calling getUserId()");
}
return null;
}
/**
* Returns an instance to the singleton LeanplumInbox object.
*/
@ -1233,7 +1307,7 @@ public class Leanplum {
noDownloadsHandlers.add(handler);
}
if (VarCache.hasReceivedDiffs()
&& Request.numPendingDownloads() == 0) {
&& RequestOld.numPendingDownloads() == 0) {
handler.variablesChanged();
}
}
@ -1254,6 +1328,60 @@ public class Leanplum {
}
}
/**
* Add a callback for when a message is displayed.
*/
public static void addMessageDisplayedHandler(
MessageDisplayedCallback handler) {
if (handler == null) {
Log.e("addMessageDisplayedHandler - Invalid handler parameter " +
"provided.");
return;
}
synchronized (messageDisplayedHandlers) {
messageDisplayedHandlers.add(handler);
}
}
/**
* Removes a variables changed and no downloads pending callback.
*/
public static void removeMessageDisplayedHandler(
MessageDisplayedCallback handler) {
if (handler == null) {
Log.e("removeMessageDisplayedHandler - Invalid handler parameter " +
"provided.");
return;
}
synchronized (messageDisplayedHandlers) {
messageDisplayedHandlers.remove(handler);
}
}
public static void triggerMessageDisplayed(ActionContext actionContext) {
ActionManager.getInstance().recordMessageImpression(actionContext.getMessageId());
synchronized (messageDisplayedHandlers) {
for (MessageDisplayedCallback callback : messageDisplayedHandlers) {
String messageID = actionContext.getMessageId();
String messageBody = "";
try {
messageBody = (String) actionContext.getArgs().get("Message");
} catch (Throwable t) {
Util.handleException(t);
}
String recipientUserID = Leanplum.getUserId();
Date deliveryDateTime = new Date();
MessageArchiveData messageArchiveData = new MessageArchiveData(messageID,
messageBody, recipientUserID, deliveryDateTime);
callback.setMessageArchiveData(messageArchiveData);
OsHandler.getInstance().post(callback);
}
}
}
/**
* Add a callback to call ONCE when no more file downloads are pending (either when no files
* needed to be downloaded or all downloads have been completed).
@ -1267,7 +1395,7 @@ public class Leanplum {
}
if (VarCache.hasReceivedDiffs()
&& Request.numPendingDownloads() == 0) {
&& RequestOld.numPendingDownloads() == 0) {
handler.variablesChanged();
} else {
synchronized (onceNoDownloadsHandlers) {
@ -1363,6 +1491,7 @@ public class Leanplum {
} catch (Throwable t) {
Util.handleException(t);
}
Leanplum.countAggregator().incrementCount("define_action");
}
/**
@ -1433,9 +1562,9 @@ public class Leanplum {
private static void setUserAttributesInternal(String userId,
HashMap<String, Object> requestArgs) {
Request.post(Constants.Methods.SET_USER_ATTRIBUTES, requestArgs).send();
RequestOld.post(Constants.Methods.SET_USER_ATTRIBUTES, requestArgs).send();
if (userId != null && userId.length() > 0) {
Request.setUserId(userId);
RequestOld.setUserId(userId);
if (LeanplumInternal.hasStarted()) {
VarCache.saveDiffs();
}
@ -1483,7 +1612,7 @@ public class Leanplum {
try {
HashMap<String, Object> params = new HashMap<>();
params.put(Constants.Params.DEVICE_PUSH_TOKEN, registrationId);
Request.post(Constants.Methods.SET_DEVICE_ATTRIBUTES, params).send();
RequestOld.post(Constants.Methods.SET_DEVICE_ATTRIBUTES, params).send();
} catch (Throwable t) {
Util.handleException(t);
}
@ -1534,7 +1663,7 @@ public class Leanplum {
}
private static void setTrafficSourceInfoInternal(HashMap<String, Object> params) {
Request.post(Constants.Methods.SET_TRAFFIC_SOURCE_INFO, params).send();
RequestOld.post(Constants.Methods.SET_TRAFFIC_SOURCE_INFO, params).send();
}
/**
@ -1557,6 +1686,7 @@ public class Leanplum {
public static void track(final String event, double value, String info,
Map<String, ?> params) {
LeanplumInternal.track(event, value, info, params, null);
countAggregator.incrementCount("track");
}
/**
@ -1792,6 +1922,7 @@ public class Leanplum {
} catch (Throwable t) {
Util.handleException(t);
}
countAggregator.incrementCount("advance_to");
}
/**
@ -1803,7 +1934,7 @@ public class Leanplum {
*/
private static void advanceToInternal(String state, Map<String, ?> params,
Map<String, Object> requestParams) {
Request.post(Constants.Methods.ADVANCE, requestParams).send();
RequestOld.post(Constants.Methods.ADVANCE, requestParams).send();
ContextualValues contextualValues = new ContextualValues();
contextualValues.parameters = params;
@ -1883,7 +2014,7 @@ public class Leanplum {
}
private static void pauseStateInternal() {
Request.post(Constants.Methods.PAUSE_STATE, new HashMap<String, Object>()).send();
RequestOld.post(Constants.Methods.PAUSE_STATE, new HashMap<String, Object>()).send();
}
/**
@ -1919,7 +2050,7 @@ public class Leanplum {
}
private static void resumeStateInternal() {
Request.post(Constants.Methods.RESUME_STATE, new HashMap<String, Object>()).send();
RequestOld.post(Constants.Methods.RESUME_STATE, new HashMap<String, Object>()).send();
}
/**
@ -1951,8 +2082,10 @@ public class Leanplum {
Map<String, Object> params = new HashMap<>();
params.put(Constants.Params.INCLUDE_DEFAULTS, Boolean.toString(false));
params.put(Constants.Params.INBOX_MESSAGES, LeanplumInbox.getInstance().messagesIds());
Request req = Request.post(Constants.Methods.GET_VARS, params);
req.onResponse(new Request.ResponseCallback() {
params.put(Constants.Params.INCLUDE_VARIANT_DEBUG_INFO, LeanplumInternal.getIsVariantDebugInfoEnabled());
RequestOld req = RequestOld.post(Constants.Methods.GET_VARS, params);
req.onResponse(new RequestOld.ResponseCallback() {
@Override
public void response(JSONObject response) {
try {
@ -1968,6 +2101,8 @@ public class Leanplum {
if (response.optBoolean(Constants.Keys.LOGGING_ENABLED, false)) {
Constants.loggingEnabled = true;
}
parseVariantDebugInfo(response);
}
if (callback != null) {
OsHandler.getInstance().post(callback);
@ -1977,7 +2112,7 @@ public class Leanplum {
}
}
});
req.onError(new Request.ErrorCallback() {
req.onError(new RequestOld.ErrorCallback() {
@Override
public void error(Exception e) {
if (callback != null) {
@ -1990,6 +2125,7 @@ public class Leanplum {
} catch (Throwable t) {
Util.handleException(t);
}
countAggregator.incrementCount("force_content_update");
}
/**
@ -2070,6 +2206,13 @@ public class Leanplum {
return messages;
}
/**
* Details about the variable assignments on the server.
*/
public static Map<String, Object> getVariantDebugInfo() {
return VarCache.getVariantDebugInfo();
}
/**
* Set location manually. Calls setDeviceLocation with cell type. Best if used in after calling
* disableLocationCollection.
@ -2119,4 +2262,56 @@ public class Leanplum {
public static boolean isLocationCollectionEnabled() {
return locationCollectionEnabled;
}
private static void parseVariantDebugInfo(JSONObject response) {
Map<String, Object> variantDebugInfo = JsonConverter.mapFromJsonOrDefault(
response.optJSONObject(Constants.Keys.VARIANT_DEBUG_INFO));
if (variantDebugInfo.size() > 0) {
VarCache.setVariantDebugInfo(variantDebugInfo);
}
}
/**
* Clears cached values for messages, variables and test assignments.
* Use sparingly as if the app is updated, you'll have to deal with potentially
* inconsistent state or user experience.
*/
public static void clearUserContent() {
VarCache.clearUserContent();
countAggregator.incrementCount("clear_user_content");
}
@VisibleForTesting
public static Set<String> parseSdkCounters(JSONObject response) {
JSONArray enabledCounters = response.optJSONArray(
Constants.Keys.ENABLED_COUNTERS);
Set<String> counterSet = toSet(enabledCounters);
return counterSet;
}
@VisibleForTesting
public static Set<String> parseFeatureFlags(JSONObject response) {
JSONArray enabledFeatureFlags = response.optJSONArray(
Constants.Keys.ENABLED_FEATURE_FLAGS);
Set<String> featureFlagSet = toSet(enabledFeatureFlags);
return featureFlagSet;
}
private static Set<String> toSet(JSONArray array) {
Set<String> set = new HashSet<>();
if (array != null) {
for (int i = 0; i < array.length(); i++) {
set.add(array.optString(i));
}
}
return set;
}
public static CountAggregator countAggregator() {
return countAggregator;
}
public static FeatureFlagManager featureFlagManager() {
return featureFlagManager;
}
}

View File

@ -1,4 +1,26 @@
/*
* Copyright 2018, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum;
import android.annotation.TargetApi;
import android.app.job.JobParameters;
import android.app.job.JobService;
@ -10,16 +32,16 @@ import android.app.job.JobService;
*/
@TargetApi(21)
public class LeanplumGcmRegistrationJobService extends JobService {
public static final int JOB_ID = -63722755;
public static final int JOB_ID = -63722755;
@Override
public boolean onStartJob(JobParameters jobParameters) {
LeanplumNotificationHelper.startPushRegistrationService(this, "GCM");
return false;
}
@Override
public boolean onStartJob(JobParameters jobParameters) {
LeanplumNotificationHelper.startPushRegistrationService(this, "GCM");
return false;
}
@Override
public boolean onStopJob(JobParameters jobParameters) {
return false;
}
}
@Override
public boolean onStopJob(JobParameters jobParameters) {
return false;
}
}

View File

@ -33,7 +33,7 @@ import com.leanplum.internal.Constants;
import com.leanplum.internal.JsonConverter;
import com.leanplum.internal.Log;
import com.leanplum.internal.OsHandler;
import com.leanplum.internal.Request;
import com.leanplum.internal.RequestOld;
import com.leanplum.internal.Util;
import com.leanplum.utils.SharedPreferencesUtil;
@ -61,15 +61,15 @@ public class LeanplumInbox {
static Set<String> downloadedImageUrls;
static boolean isInboxImagePrefetchingEnabled = true;
private int unreadCount;
private Map<String, LeanplumInboxMessage> messages;
private boolean didLoad = false;
private volatile int unreadCount;
private volatile Map<String, LeanplumInboxMessage> messages;
private volatile boolean didLoad = false;
private final List<InboxChangedCallback> changedCallbacks;
private final List<InboxSyncedCallback> syncedCallbacks;
private final Object updatingLock = new Object();
protected LeanplumInbox() {
private LeanplumInbox() {
this.unreadCount = 0;
this.messages = new HashMap<>();
this.didLoad = false;
@ -255,7 +255,7 @@ public class LeanplumInbox {
Map<String, Object> params = new HashMap<>();
params.put(Constants.Params.INBOX_MESSAGE_ID, messageId);
Request req = Request.post(Constants.Methods.DELETE_INBOX_MESSAGE, params);
RequestOld req = RequestOld.post(Constants.Methods.DELETE_INBOX_MESSAGE, params);
req.send();
}
@ -287,12 +287,12 @@ public class LeanplumInbox {
Context context = Leanplum.getContext();
SharedPreferences defaults = context.getSharedPreferences(
"__leanplum__", Context.MODE_PRIVATE);
if (Request.token() == null) {
if (RequestOld.token() == null) {
update(new HashMap<String, LeanplumInboxMessage>(), 0, false);
return;
}
int unreadCount = 0;
AESCrypt aesContext = new AESCrypt(Request.appId(), Request.token());
AESCrypt aesContext = new AESCrypt(RequestOld.appId(), RequestOld.token());
String newsfeedString = aesContext.decodePreference(
defaults, Constants.Defaults.INBOX_KEY, "{}");
Map<String, Object> newsfeed = JsonConverter.fromJson(newsfeedString);
@ -322,7 +322,7 @@ public class LeanplumInbox {
if (Constants.isNoop()) {
return;
}
if (Request.token() == null) {
if (RequestOld.token() == null) {
return;
}
Context context = Leanplum.getContext();
@ -337,7 +337,7 @@ public class LeanplumInbox {
messages.put(messageId, data);
}
String messagesJson = JsonConverter.toJson(messages);
AESCrypt aesContext = new AESCrypt(Request.appId(), Request.token());
AESCrypt aesContext = new AESCrypt(RequestOld.appId(), RequestOld.token());
editor.putString(Constants.Defaults.INBOX_KEY, aesContext.encrypt(messagesJson));
SharedPreferencesUtil.commitChanges(editor);
}
@ -347,8 +347,8 @@ public class LeanplumInbox {
return;
}
final Request req = Request.post(Constants.Methods.GET_INBOX_MESSAGES, null);
req.onResponse(new Request.ResponseCallback() {
final RequestOld req = RequestOld.post(Constants.Methods.GET_INBOX_MESSAGES, null);
req.onResponse(new RequestOld.ResponseCallback() {
@Override
public void response(JSONObject response) {
try {
@ -411,7 +411,7 @@ public class LeanplumInbox {
}
}
});
req.onError(new Request.ErrorCallback() {
req.onError(new RequestOld.ErrorCallback() {
@Override
public void error(Exception e) {
triggerInboxSyncedWithStatus(false);
@ -430,11 +430,15 @@ public class LeanplumInbox {
}
try {
for (String messageId : messagesIds()) {
messages.add(messageForId(messageId));
LeanplumInboxMessage message = messageForId(messageId);
if (message != null) {
messages.add(message);
}
}
} catch (Throwable t) {
Util.handleException(t);
}
Leanplum.countAggregator().incrementCount("all_messages_inbox");
return messages;
}

View File

@ -26,9 +26,10 @@ import android.text.TextUtils;
import com.leanplum.internal.CollectionUtil;
import com.leanplum.internal.Constants;
import com.leanplum.internal.FileManager;
import com.leanplum.internal.JsonConverter;
import com.leanplum.internal.Log;
import com.leanplum.internal.Request;
import com.leanplum.internal.RequestOld;
import com.leanplum.internal.Util;
import org.json.JSONObject;
@ -39,9 +40,6 @@ import java.util.HashMap;
import java.util.Map;
import static com.leanplum.internal.FileManager.DownloadFileResult;
import static com.leanplum.internal.FileManager.fileExistsAtPath;
import static com.leanplum.internal.FileManager.fileValue;
import static com.leanplum.internal.FileManager.maybeDownloadFile;
/**
* LeanplumInboxMessage class.
@ -77,8 +75,8 @@ public class LeanplumInboxMessage {
* Returns the image file path of the inbox message. Can be null.
*/
public String getImageFilePath() {
String path = fileValue(imageFileName);
if (fileExistsAtPath(path)) {
String path = FileManager.fileValue(imageFileName);
if (FileManager.fileExistsAtPath(path)) {
return new File(path).getAbsolutePath();
}
if (!LeanplumInbox.getInstance().isInboxImagePrefetchingEnabled()) {
@ -94,8 +92,8 @@ public class LeanplumInboxMessage {
* It will return the file Uri path instead if the image is in cache.
*/
public Uri getImageUrl() {
String path = fileValue(imageFileName);
if (fileExistsAtPath(path)) {
String path = FileManager.fileValue(imageFileName);
if (FileManager.fileExistsAtPath(path)) {
return Uri.fromFile(new File(path));
}
if (TextUtils.isEmpty(imageUrl)) {
@ -187,7 +185,7 @@ public class LeanplumInboxMessage {
Map<String, Object> params = new HashMap<>();
params.put(Constants.Params.INBOX_MESSAGE_ID, messageId);
Request req = Request.post(Constants.Methods.MARK_INBOX_MESSAGE_AS_READ,
RequestOld req = RequestOld.post(Constants.Methods.MARK_INBOX_MESSAGE_AS_READ,
params);
req.send();
}
@ -253,7 +251,7 @@ public class LeanplumInboxMessage {
return false;
}
DownloadFileResult result = maybeDownloadFile(true, imageFileName,
DownloadFileResult result = FileManager.maybeDownloadFile(true, imageFileName,
imageUrl, imageUrl, null);
LeanplumInbox.downloadedImageUrls.add(imageUrl);
return DownloadFileResult.DOWNLOADING == result;

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,3 @@
package com.leanplum;
/*
* Copyright 2017, Leanplum, Inc. All rights reserved.
*
@ -20,6 +18,7 @@ package com.leanplum;
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum;
import android.annotation.TargetApi;
import android.app.Notification;
@ -35,6 +34,7 @@ import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import android.util.TypedValue;
@ -43,8 +43,11 @@ import android.widget.RemoteViews;
import com.leanplum.internal.Constants;
import com.leanplum.internal.JsonConverter;
import com.leanplum.internal.Log;
import com.leanplum.internal.Util;
import com.leanplum.utils.BitmapUtil;
import com.leanplum.utils.BuildUtil;
import java.util.IllegalFormatCodePointException;
import java.util.List;
import java.util.Map;
import java.util.Random;
@ -57,345 +60,470 @@ import java.util.TreeSet;
*/
class LeanplumNotificationHelper {
private static final int BIGPICTURE_TEXT_TOP_PADDING = -14;
private static final int BIGPICTURE_TEXT_SIZE = 14;
private static final String LEANPLUM_DEFAULT_PUSH_ICON = "leanplum_default_push_icon";
private static final int BIGPICTURE_TEXT_TOP_PADDING = -14;
private static final int BIGPICTURE_TEXT_SIZE = 14;
private static final String LEANPLUM_DEFAULT_PUSH_ICON = "leanplum_default_push_icon";
private static final int MAX_ONE_LINE_TEXT_LENGTH = 37;
/**
* If notification channels are supported this method will try to create
* NotificationCompat.Builder with default notification channel if default channel id is provided.
* If notification channels not supported this method will return NotificationCompat.Builder for
* context.
*
* @param context The application context.
* @param isNotificationChannelSupported True if notification channels are supported.
* @return NotificationCompat.Builder for provided context or null.
*/
// NotificationCompat.Builder(Context context) constructor was deprecated in API level 26.
@SuppressWarnings("deprecation")
static NotificationCompat.Builder getDefaultCompatNotificationBuilder(Context context,
boolean isNotificationChannelSupported) {
if (!isNotificationChannelSupported) {
return new NotificationCompat.Builder(context);
}
String channelId = LeanplumNotificationChannel.getDefaultNotificationChannelId(context);
if (!TextUtils.isEmpty(channelId)) {
return new NotificationCompat.Builder(context, channelId);
/**
* If notification channels are supported this method will try to create
* NotificationCompat.Builder with default notification channel if default channel id is provided.
* If notification channels not supported this method will return NotificationCompat.Builder for
* context.
*
* @param context The application context.
* @param isNotificationChannelSupported True if notification channels are supported.
* @return NotificationCompat.Builder for provided context or null.
*/
// NotificationCompat.Builder(Context context) constructor was deprecated in API level 26.
@SuppressWarnings("deprecation")
static NotificationCompat.Builder getDefaultCompatNotificationBuilder(Context context,
boolean isNotificationChannelSupported) {
if (!isNotificationChannelSupported) {
return new NotificationCompat.Builder(context);
}
String channelId = LeanplumNotificationChannel.getDefaultNotificationChannelId(context);
if (!TextUtils.isEmpty(channelId)) {
return new NotificationCompat.Builder(context, channelId);
} else {
Log.w("Failed to post notification, there are no notification channels configured.");
return null;
}
}
/**
* If notification channels are supported this method will try to create
* Notification.Builder with default notification channel if default channel id is provided.
* If notification channels not supported this method will return Notification.Builder for
* context.
*
* @param context The application context.
* @param isNotificationChannelSupported True if notification channels are supported.
* @return Notification.Builder for provided context or null.
*/
// Notification.Builder(Context context) constructor was deprecated in API level 26.
@SuppressWarnings("deprecation")
private static Notification.Builder getDefaultNotificationBuilder(Context context,
boolean isNotificationChannelSupported) {
if (!isNotificationChannelSupported) {
return new Notification.Builder(context);
}
String channelId = LeanplumNotificationChannel.getDefaultNotificationChannelId(context);
if (!TextUtils.isEmpty(channelId)) {
return new Notification.Builder(context, channelId);
} else {
Log.w("Failed to post notification, there are no notification channels configured.");
return null;
}
}
/**
* If notification channels are supported this method will try to create a channel with
* information from the message if it doesn't exist and return NotificationCompat.Builder for this
* channel. In the case where no channel information inside the message, we will try to get a
* channel with default channel id. If notification channels not supported this method will return
* NotificationCompat.Builder for context.
*
* @param context The application context.
* @param message Push notification Bundle.
* @return NotificationCompat.Builder or null.
*/
// NotificationCompat.Builder(Context context) constructor was deprecated in API level 26.
@SuppressWarnings("deprecation")
static NotificationCompat.Builder getNotificationCompatBuilder(Context context, Bundle message) {
NotificationCompat.Builder builder = null;
// If we are targeting API 26, try to find supplied channel to post notification.
if (BuildUtil.isNotificationChannelSupported(context)) {
try {
String channel = message.getString("lp_channel");
if (!TextUtils.isEmpty(channel)) {
// Create channel if it doesn't exist and post notification to that channel.
Map<String, Object> channelDetails = JsonConverter.fromJson(channel);
String channelId = LeanplumNotificationChannel.createNotificationChannel(context,
channelDetails);
if (!TextUtils.isEmpty(channelId)) {
builder = new NotificationCompat.Builder(context, channelId);
} else {
Log.w("Failed to post notification to specified channel.");
}
} else {
Log.w("Failed to post notification, there are no notification channels configured.");
return null;
// If channel isn't supplied, try to look up for default channel.
builder = LeanplumNotificationHelper.getDefaultCompatNotificationBuilder(context, true);
}
} catch (Exception e) {
Log.e("Failed to post notification to specified channel.");
}
} else {
builder = new NotificationCompat.Builder(context);
}
return builder;
}
/**
* If notification channels are supported this method will try to create a channel with
* information from the message if it doesn't exist and return Notification.Builder for this
* channel. In the case where no channel information inside the message, we will try to get a
* channel with default channel id. If notification channels not supported this method will return
* Notification.Builder for context.
*
* @param context The application context.
* @param message Push notification Bundle.
* @return Notification.Builder or null.
*/
private static Notification.Builder getNotificationBuilder(Context context, Bundle message) {
Notification.Builder builder = null;
// If we are targeting API 26, try to find supplied channel to post notification.
if (BuildUtil.isNotificationChannelSupported(context)) {
try {
String channel = message.getString("lp_channel");
if (!TextUtils.isEmpty(channel)) {
// Create channel if it doesn't exist and post notification to that channel.
Map<String, Object> channelDetails = JsonConverter.fromJson(channel);
String channelId = LeanplumNotificationChannel.createNotificationChannel(context,
channelDetails);
if (!TextUtils.isEmpty(channelId)) {
builder = new Notification.Builder(context, channelId);
} else {
Log.w("Failed to post notification to specified channel.");
}
} else {
// If channel isn't supplied, try to look up for default channel.
builder = LeanplumNotificationHelper.getDefaultNotificationBuilder(context, true);
}
} catch (Exception e) {
Log.e("Failed to post notification to specified channel.");
}
} else {
builder = new Notification.Builder(context);
}
return builder;
}
/**
* Gets NotificationCompat.Builder for provided parameters.
*
* @param context The application context.
* @param message Push notification Bundle.
* @param contentIntent PendingIntent.
* @param title String with title for push notification.
* @param messageText String with text for push notification.
* @param bigPicture Bitmap for BigPictureStyle notification.
* @param defaultNotificationIconResourceId int Resource id for default push notification icon.
* @return NotificationCompat.Builder or null.
*/
static NotificationCompat.Builder getNotificationCompatBuilder(Context context, Bundle message,
PendingIntent contentIntent, String title, final String messageText, Bitmap bigPicture,
int defaultNotificationIconResourceId) {
if (message == null) {
return null;
}
/**
* Starts push registration service to update GCM/FCM InstanceId token.
*
* @param context Current application context.
* @param providerName Name of push notification provider.
*/
static void startPushRegistrationService(Context context, String providerName) {
NotificationCompat.Builder notificationCompatBuilder =
getNotificationCompatBuilder(context, message);
if (notificationCompatBuilder == null) {
return null;
}
if (defaultNotificationIconResourceId == 0) {
notificationCompatBuilder.setSmallIcon(context.getApplicationInfo().icon);
} else {
notificationCompatBuilder.setSmallIcon(defaultNotificationIconResourceId);
}
notificationCompatBuilder.setContentTitle(title)
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(messageText))
.setContentText(messageText);
if (bigPicture != null) {
notificationCompatBuilder.setStyle(new NotificationCompat.BigPictureStyle()
.bigPicture(bigPicture)
.setBigContentTitle(title)
.setSummaryText(messageText));
}
// Try to put a notification on top of the notification area. This method was deprecated in API
// level 26. For API level 26 and above we must use setImportance(int) for each notification
// channel, not for each notification message.
if (Build.VERSION.SDK_INT >= 16 && !BuildUtil.isNotificationChannelSupported(context)) {
//noinspection deprecation
notificationCompatBuilder.setPriority(Notification.PRIORITY_MAX);
}
notificationCompatBuilder.setAutoCancel(true);
notificationCompatBuilder.setContentIntent(contentIntent);
return notificationCompatBuilder;
}
/**
* Calls setStyle for notificationBuilder and it tries modify notification layout to support two
* lines.
*
* @param notificationBuilder current Notification.Builder.
* @param bigPictureStyle current Notification.BigPictureStyle.
*/
static void setModifiedBigPictureStyle(Notification.Builder notificationBuilder,
Notification.Style bigPictureStyle) {
if (Build.VERSION.SDK_INT < 16 || notificationBuilder == null || bigPictureStyle == null) {
return;
}
try {
notificationBuilder.setStyle(bigPictureStyle);
if (Build.VERSION.SDK_INT >= 24) {
// By default we cannot reach getStandardView method on API>=24. If we call
// createBigContentView, Android will call getStandardView method and we can get
// modified RemoteView.
try {
if (context == null) {
return;
}
Log.i("Updating " + providerName + " InstanceId token.");
// Fetch updated Instance ID token and notify our app's server of any changes (if applicable).
Intent intent = new Intent(context, LeanplumPushRegistrationService.class);
context.startService(intent);
RemoteViews remoteView = notificationBuilder.createBigContentView();
if (remoteView != null) {
// We need to set received RemoteView as a custom big content view.
notificationBuilder.setCustomBigContentView(remoteView);
}
} catch (Throwable t) {
Log.e("Couldn't update " + providerName + " InstanceId token.", t);
Log.e("Cannot modify push notification layout.", t);
}
}
} catch (Throwable t) {
Log.e("Cannot set BigPicture style for push notification.", t);
}
}
/**
* Gets Notification.BigPictureStyle with 2 lines text.
*
* @param message Push notification Bundle.
* @param bigPicture Bitmap for BigPictureStyle notification.
* @param title String with title for push notification.
* @param messageText String with text for push notification.
* @return Notification.BigPictureStyle or null.
*/
static Notification.BigPictureStyle getBigPictureStyle(Bundle message, Bitmap bigPicture,
String title, final String messageText) {
if (Build.VERSION.SDK_INT < 16 || message == null || bigPicture == null) {
return null;
}
/**
* Schedule JobService to JobScheduler.
*
* @param context Current application context.
* @param clazz JobService class.
* @param jobId JobService id.
*/
@TargetApi(21)
static void scheduleJobService(Context context, Class clazz, int jobId) {
if (context == null) {
return;
}
ComponentName serviceName = new ComponentName(context, clazz);
JobScheduler jobScheduler =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
if (jobScheduler != null) {
jobId = verifyJobId(jobScheduler.getAllPendingJobs(), jobId);
JobInfo startMyServiceJobInfo = new JobInfo.Builder(jobId, serviceName)
.setMinimumLatency(10).build();
jobScheduler.schedule(startMyServiceJobInfo);
}
}
/**
* Verifies that jobId don't present on JobScheduler pending jobs. If jobId present on
* JobScheduler pending jobs generates a new one.
*
* @param allPendingJobs List of current pending jobs.
* @param jobId JobService id.
* @return jobId if jobId don't present on JobScheduler pending jobs
*/
@TargetApi(21)
private static int verifyJobId(List<JobInfo> allPendingJobs, int jobId) {
if (allPendingJobs != null && !allPendingJobs.isEmpty()) {
TreeSet<Integer> idsSet = new TreeSet<>();
for (JobInfo jobInfo : allPendingJobs) {
idsSet.add(jobInfo.getId());
}
if (idsSet.contains(jobId)) {
if (idsSet.first() > Integer.MIN_VALUE) {
jobId = idsSet.first() - 1;
} else if (idsSet.last() < Integer.MIN_VALUE) {
jobId = idsSet.last() + 1;
} else {
while (idsSet.contains(jobId)) {
jobId = new Random().nextInt();
}
}
Notification.BigPictureStyle bigPictureStyle = new Notification.BigPictureStyle() {
@Override
protected RemoteViews getStandardView(int layoutId) {
RemoteViews remoteViews = super.getStandardView(layoutId);
if (messageText != null && messageText.length() >= MAX_ONE_LINE_TEXT_LENGTH) {
// Modifications of standard push RemoteView.
try {
int id = Resources.getSystem().getIdentifier("text", "id", "android");
remoteViews.setBoolean(id, "setSingleLine", false);
remoteViews.setInt(id, "setLines", 2);
if (Build.VERSION.SDK_INT < 23) {
// Make text smaller.
remoteViews.setViewPadding(id, 0, BIGPICTURE_TEXT_TOP_PADDING, 0, 0);
remoteViews.setTextViewTextSize(id, TypedValue.COMPLEX_UNIT_SP, BIGPICTURE_TEXT_SIZE);
}
} catch (Throwable throwable) {
Log.e("Cannot modify push notification layout.");
}
}
return jobId;
return remoteViews;
}
};
bigPictureStyle.bigPicture(bigPicture)
.setBigContentTitle(title)
.setSummaryText(message.getString(Constants.Keys.PUSH_MESSAGE_TEXT));
return bigPictureStyle;
}
/**
* Gets Notification.Builder for provided parameters.
*
* @param context The application context.
* @param message Push notification Bundle.
* @param contentIntent PendingIntent.
* @param title String with title for push notification.
* @param messageText String with text for push notification.
* @param defaultNotificationIconResourceId int Resource id for default push notification icon.
* @return Notification.Builder or null.
*/
static Notification.Builder getNotificationBuilder(Context context, Bundle message,
PendingIntent contentIntent, String title, final String messageText,
int defaultNotificationIconResourceId) {
Notification.Builder notificationBuilder = getNotificationBuilder(context, message);
if (notificationBuilder == null) {
return null;
}
/**
* If notification channels are supported this method will try to create
* Notification.Builder with default notification channel if default channel id is provided.
* If notification channels not supported this method will return Notification.Builder for
* context.
*
* @param context The application context.
* @param isNotificationChannelSupported True if notification channels are supported.
* @return Notification.Builder for provided context or null.
*/
// Notification.Builder(Context context) constructor was deprecated in API level 26.
@TargetApi(Build.VERSION_CODES.O)
@SuppressWarnings("deprecation")
private static Notification.Builder getDefaultNotificationBuilder(Context context,
boolean isNotificationChannelSupported) {
if (!isNotificationChannelSupported) {
return new Notification.Builder(context);
}
String channelId = LeanplumNotificationChannel.getDefaultNotificationChannelId(context);
if (!TextUtils.isEmpty(channelId)) {
return new Notification.Builder(context, channelId);
if (defaultNotificationIconResourceId == 0) {
notificationBuilder.setSmallIcon(context.getApplicationInfo().icon);
} else {
notificationBuilder.setSmallIcon(defaultNotificationIconResourceId);
}
notificationBuilder.setContentTitle(title)
.setContentText(messageText);
if (Build.VERSION.SDK_INT > 16) {
notificationBuilder.setStyle(new Notification.BigTextStyle()
.bigText(messageText));
if (!BuildUtil.isNotificationChannelSupported(context)) {
//noinspection deprecation
notificationBuilder.setPriority(Notification.PRIORITY_MAX);
}
}
notificationBuilder.setAutoCancel(true);
notificationBuilder.setContentIntent(contentIntent);
return notificationBuilder;
}
/**
* Checks a possibility to create icon drawable from current app icon.
*
* @param context Current application context.
* @return boolean True if it is possible to create a drawable from current app icon.
*/
private static boolean canCreateIconDrawable(Context context) {
try {
// Try to create icon drawable.
Drawable drawable = AdaptiveIconDrawable.createFromStream(
context.getResources().openRawResource(context.getApplicationInfo().icon),
"applicationInfo.icon");
// If there was no crash, we still need to check for null.
if (drawable != null) {
return true;
}
} catch (Throwable ignored) {
}
return false;
}
/**
* Validation of Application icon for small icon on push notification.
*
* @param context Current application context.
* @return boolean True if application icon can be used for small icon on push notification.
*/
static boolean isApplicationIconValid(Context context) {
if (context == null) {
return false;
}
// TODO: Potentially there should be checked for Build.VERSION.SDK_INT != 26, but we need to
// TODO: confirm that adaptive icon works well on 27, before to change it.
if (Build.VERSION.SDK_INT < 26) {
return true;
}
return canCreateIconDrawable(context);
}
/**
* Gets default push notification resource id for LEANPLUM_DEFAULT_PUSH_ICON in drawable.
*
* @param context Current application context.
* @return int Resource id.
*/
static int getDefaultPushNotificationIconResourceId(Context context) {
try {
Resources resources = context.getResources();
return resources.getIdentifier(LEANPLUM_DEFAULT_PUSH_ICON, "drawable",
context.getPackageName());
} catch (Throwable ignored) {
return 0;
}
}
/**
* Schedule JobService to JobScheduler.
*
* @param context Current application context.
* @param clazz JobService class.
* @param jobId JobService id.
*/
@TargetApi(21)
static void scheduleJobService(Context context, Class clazz, int jobId) {
if (context == null) {
return;
}
ComponentName serviceName = new ComponentName(context, clazz);
JobScheduler jobScheduler =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
if (jobScheduler != null) {
jobId = verifyJobId(jobScheduler.getAllPendingJobs(), jobId);
JobInfo startMyServiceJobInfo = new JobInfo.Builder(jobId, serviceName)
.setMinimumLatency(10).build();
jobScheduler.schedule(startMyServiceJobInfo);
}
}
/**
* Verifies that jobId don't present on JobScheduler pending jobs. If jobId present on
* JobScheduler pending jobs generates a new one.
*
* @param allPendingJobs List of current pending jobs.
* @param jobId JobService id.
* @return jobId if jobId don't present on JobScheduler pending jobs
*/
@TargetApi(21)
private static int verifyJobId(List<JobInfo> allPendingJobs, int jobId) {
if (allPendingJobs != null && !allPendingJobs.isEmpty()) {
TreeSet<Integer> idsSet = new TreeSet<>();
for (JobInfo jobInfo : allPendingJobs) {
idsSet.add(jobInfo.getId());
}
if (idsSet.contains(jobId)) {
if (idsSet.first() > Integer.MIN_VALUE) {
jobId = idsSet.first() - 1;
} else if (idsSet.last() < Integer.MIN_VALUE) {
jobId = idsSet.last() + 1;
} else {
Log.w("Failed to post notification, there are no notification channels configured.");
return null;
while (idsSet.contains(jobId)) {
jobId = new Random().nextInt();
}
}
}
}
return jobId;
}
/**
* If notification channels are supported this method will try to create a channel with
* information from the message if it doesn't exist and return NotificationCompat.Builder for this
* channel. In the case where no channel information inside the message, we will try to get a
* channel with default channel id. If notification channels not supported this method will return
* NotificationCompat.Builder for context.
*
* @param context The application context.
* @param message Push notification Bundle.
* @return NotificationCompat.Builder or null.
*/
// NotificationCompat.Builder(Context context) constructor was deprecated in API level 26.
@SuppressWarnings("deprecation")
static NotificationCompat.Builder getNotificationCompatBuilder(Context context, Bundle message) {
NotificationCompat.Builder builder = null;
// If we are targeting API 26, try to find supplied channel to post notification.
if (BuildUtil.isNotificationChannelSupported(context)) {
try {
String channel = message.getString("lp_channel");
if (!TextUtils.isEmpty(channel)) {
// Create channel if it doesn't exist and post notification to that channel.
Map<String, Object> channelDetails = JsonConverter.fromJson(channel);
String channelId = LeanplumNotificationChannel.createNotificationChannel(context,
channelDetails);
if (!TextUtils.isEmpty(channelId)) {
builder = new NotificationCompat.Builder(context, channelId);
} else {
Log.w("Failed to post notification to specified channel.");
}
} else {
// If channel isn't supplied, try to look up for default channel.
builder = LeanplumNotificationHelper.getDefaultCompatNotificationBuilder(context, true);
}
} catch (Exception e) {
Log.e("Failed to post notification to specified channel.");
}
} else {
builder = new NotificationCompat.Builder(context);
}
return builder;
/**
* Starts push registration service to update GCM/FCM InstanceId token.
*
* @param context Current application context.
* @param providerName Name of push notification provider.
*/
static void startPushRegistrationService(Context context, String providerName) {
try {
if (context == null) {
return;
}
Log.i("Updating " + providerName + " InstanceId token.");
// Fetch updated Instance ID token and notify our app's server of any changes (if applicable).
Intent intent = new Intent(context, LeanplumPushRegistrationService.class);
context.startService(intent);
} catch (Throwable t) {
Log.e("Couldn't update " + providerName + " InstanceId token.", t);
}
}
/**
* If notification channels are supported this method will try to create a channel with
* information from the message if it doesn't exist and return Notification.Builder for this
* channel. In the case where no channel information inside the message, we will try to get a
* channel with default channel id. If notification channels not supported this method will return
* Notification.Builder for context.
*
* @param context The application context.
* @param message Push notification Bundle.
* @return Notification.Builder or null.
*/
static Notification.Builder getNotificationBuilder(Context context, Bundle message) {
Notification.Builder builder = null;
// If we are targeting API 26, try to find supplied channel to post notification.
if (BuildUtil.isNotificationChannelSupported(context)) {
try {
String channel = message.getString("lp_channel");
if (!TextUtils.isEmpty(channel)) {
// Create channel if it doesn't exist and post notification to that channel.
Map<String, Object> channelDetails = JsonConverter.fromJson(channel);
String channelId = LeanplumNotificationChannel.createNotificationChannel(context,
channelDetails);
if (!TextUtils.isEmpty(channelId)) {
builder = new Notification.Builder(context, channelId);
} else {
Log.w("Failed to post notification to specified channel.");
}
} else {
// If channel isn't supplied, try to look up for default channel.
builder = LeanplumNotificationHelper.getDefaultNotificationBuilder(context, true);
}
} catch (Exception e) {
Log.e("Failed to post notification to specified channel.");
}
} else {
builder = new Notification.Builder(context);
}
return builder;
/**
* Gets bitmap for BigPicture style push notification.
*
* @param context Current application context.
* @param imageUrl String with url to image.
* @return Scaled bitmap for push notification with big image or null.
*/
@Nullable
static Bitmap getBigPictureBitmap(Context context, String imageUrl) {
Bitmap bigPicture = null;
// BigPictureStyle support requires API 16 and higher.
if (!TextUtils.isEmpty(imageUrl) && Build.VERSION.SDK_INT >= 16) {
bigPicture = BitmapUtil.getScaledBitmap(context, imageUrl);
if (bigPicture == null) {
Log.w(String.format("Image download failed for push notification with big picture. " +
"No image will be included with the push notification. Image URL: %s.", imageUrl));
}
}
/**
* Gets Notification.Builder with 2 lines at BigPictureStyle notification text.
*
* @param context The application context.
* @param message Push notification Bundle.
* @param contentIntent PendingIntent.
* @param title String with title for push notification.
* @param messageText String with text for push notification.
* @param bigPicture Bitmap for BigPictureStyle notification.
* @param defaultNotificationIconResourceId int Resource id for default push notification icon.
* @return Notification.Builder or null.
*/
static Notification.Builder getNotificationBuilder(Context context, Bundle message,
PendingIntent contentIntent, String title, final String messageText, Bitmap bigPicture,
int defaultNotificationIconResourceId) {
if (Build.VERSION.SDK_INT < 16) {
return null;
}
Notification.Builder notificationBuilder =
getNotificationBuilder(context, message);
if (defaultNotificationIconResourceId == 0) {
notificationBuilder.setSmallIcon(context.getApplicationInfo().icon);
} else {
notificationBuilder.setSmallIcon(defaultNotificationIconResourceId);
}
notificationBuilder.setContentTitle(title)
.setContentText(messageText);
Notification.BigPictureStyle bigPictureStyle = new Notification.BigPictureStyle() {
@Override
protected RemoteViews getStandardView(int layoutId) {
RemoteViews remoteViews = super.getStandardView(layoutId);
// Modifications of stanxdard push RemoteView.
try {
int id = Resources.getSystem().getIdentifier("text", "id", "android");
remoteViews.setBoolean(id, "setSingleLine", false);
remoteViews.setInt(id, "setLines", 2);
if (Build.VERSION.SDK_INT < 23) {
// Make text smaller.
remoteViews.setViewPadding(id, 0, BIGPICTURE_TEXT_TOP_PADDING, 0, 0);
remoteViews.setTextViewTextSize(id, TypedValue.COMPLEX_UNIT_SP, BIGPICTURE_TEXT_SIZE);
}
} catch (Throwable throwable) {
Log.e("Cannot modify push notification layout.");
}
return remoteViews;
}
};
bigPictureStyle.bigPicture(bigPicture)
.setBigContentTitle(title)
.setSummaryText(message.getString(Constants.Keys.PUSH_MESSAGE_TEXT));
notificationBuilder.setStyle(bigPictureStyle);
if (Build.VERSION.SDK_INT >= 24) {
// By default we cannot reach getStandardView method on API>=24. I we call
// createBigContentView, Android will call getStandardView method and we can get
// modified RemoteView.
try {
RemoteViews remoteView = notificationBuilder.createBigContentView();
if (remoteView != null) {
// We need to set received RemoteView as a custom big content view.
notificationBuilder.setCustomBigContentView(remoteView);
}
} catch (Throwable t) {
Log.e("Cannot modify push notification layout.", t);
}
}
notificationBuilder.setAutoCancel(true);
notificationBuilder.setContentIntent(contentIntent);
return notificationBuilder;
}
/**
* Checks a possibility to create icon drawable from current app icon.
*
* @param context Current application context.
* @return boolean True if it is possible to create a drawable from current app icon.
*/
private static boolean canCreateIconDrawable(Context context) {
try {
// Try to create icon drawable.
Drawable drawable = AdaptiveIconDrawable.createFromStream(
context.getResources().openRawResource(context.getApplicationInfo().icon),
"applicationInfo.icon");
// If there was no crash, we still need to check for null.
if (drawable != null) {
return true;
}
} catch (Throwable ignored) {
}
return false;
}
/**
* Validation of Application icon for small icon on push notification.
*
* @param context Current application context.
* @return boolean True if application icon can be used for small icon on push notification.
*/
static boolean isApplicationIconValid(Context context) {
if (context == null) {
return false;
}
// TODO: Potentially there should be checked for Build.VERSION.SDK_INT != 26, but we need to
// TODO: confirm that adaptive icon works well on 27, before to change it.
if (Build.VERSION.SDK_INT < 26) {
return true;
}
return canCreateIconDrawable(context);
}
/**
* Gets default push notification resource id for LEANPLUM_DEFAULT_PUSH_ICON in drawable.
*
* @param context Current application context.
* @return int Resource id.
*/
static int getDefaultPushNotificationIconResourceId(Context context) {
try {
Resources resources = context.getResources();
return resources.getIdentifier(LEANPLUM_DEFAULT_PUSH_ICON, "drawable",
context.getPackageName());
} catch (Throwable ignored) {
return 0;
}
}
}
return bigPicture;
}
}

View File

@ -44,7 +44,7 @@ public class LeanplumPushInstanceIDService extends InstanceIDListenerService {
LeanplumNotificationHelper.startPushRegistrationService(this, "GCM");
} else {
LeanplumNotificationHelper.scheduleJobService(this,
LeanplumGcmRegistrationJobService.class, LeanplumGcmRegistrationJobService.JOB_ID);
LeanplumGcmRegistrationJobService.class, LeanplumGcmRegistrationJobService.JOB_ID);
}
} catch (Throwable t) {
Log.e("Failed to update GCM token.", t);

View File

@ -44,6 +44,9 @@ public class LeanplumPushListenerService extends GcmListenerService {
public void onMessageReceived(String senderId, Bundle data) {
try {
if (data.containsKey(Keys.PUSH_MESSAGE_TEXT)) {
if (Leanplum.getContext() == null) {
Leanplum.setApplicationContext(this);
}
LeanplumPushService.handleNotification(this, data);
}
Log.i("Received: " + data.toString());

View File

@ -21,12 +21,45 @@
package com.leanplum;
import android.app.Notification;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
/**
* Implement LeanplumPushNotificationCustomizer to customize the appearance of notifications.
*/
public interface LeanplumPushNotificationCustomizer {
/**
* Implement this method to customize push notification. Please call {@link
* LeanplumPushService#setCustomizer(LeanplumPushNotificationCustomizer)} to activate this method.
* Leave this method empty if you want to support 2 lines of text
* in BigPicture style push notification and implement {@link
* LeanplumPushNotificationCustomizer#customize(Notification.Builder, Bundle, Notification.Style)}
*
* @param builder NotificationCompat.Builder for push notification.
* @param notificationPayload Bundle notification payload.
*/
void customize(NotificationCompat.Builder builder, Bundle notificationPayload);
/**
* Implement this method to support 2 lines of text in BigPicture style push notification,
* otherwise implement {@link
* LeanplumPushNotificationCustomizer#customize(NotificationCompat.Builder, Bundle)} and leave
* this method empty. Please call {@link
* LeanplumPushService#setCustomizer(LeanplumPushNotificationCustomizer, boolean)} with true
* value to activate this method.
*
* @param builder Notification.Builder for push notification.
* @param notificationPayload Bundle notification payload.
* @param notificationStyle - Notification.BigPictureStyle or null - BigPicture style for current
* push notification. Call ((Notification.BigPictureStyle) notificationStyle).bigLargeIcon(largeIcon)
* if you want to set large icon on expanded push notification. If notificationStyle wasn't null
* it will be set to push notification. Note: If you call notificationStyle = new
* Notification.BigPictureStyle() or other Notification.Style - there will be no support 2 lines
* of text on BigPicture push and you need to call builder.setStyle(notificationStyle) to set
* yours expanded layout for push notification.
*/
void customize(Notification.Builder builder, Bundle notificationPayload,
@Nullable Notification.Style notificationStyle);
}

View File

@ -64,5 +64,6 @@ public class LeanplumPushReceiver extends BroadcastReceiver {
} catch (Throwable t) {
Util.handleException(t);
}
Leanplum.countAggregator().incrementCount("did_receive_remote_notification");
}
}

View File

@ -23,6 +23,7 @@ package com.leanplum;
import android.app.IntentService;
import android.content.Intent;
import android.text.TextUtils;
import com.leanplum.internal.Log;
@ -41,22 +42,26 @@ public class LeanplumPushRegistrationService extends IntentService {
@Override
protected void onHandleIntent(Intent intent) {
LeanplumCloudMessagingProvider provider = LeanplumPushService.getCloudMessagingProvider();
if (provider == null) {
Log.e("Failed to complete registration token refresh.");
return;
}
String registrationId = provider.getRegistrationId();
if (registrationId != null) {
if (existingRegistrationId != null && !registrationId.equals(existingRegistrationId)) {
Log.e("WARNING: It appears your app is registering " +
"with GCM/FCM using multiple GCM/FCM sender ids. Please be sure to call " +
"LeanplumPushService.setGcmSenderIds() with " +
"all of the GCM sender ids that you use, not just the one that you use with " +
"Leanplum. Otherwise, GCM/FCM push notifications may not work consistently.");
try {
LeanplumCloudMessagingProvider provider = LeanplumPushService.getCloudMessagingProvider();
if (provider == null) {
Log.e("Failed to complete registration token refresh.");
return;
}
existingRegistrationId = registrationId;
provider.onRegistrationIdReceived(getApplicationContext(), registrationId);
String registrationId = provider.getRegistrationId();
if (!TextUtils.isEmpty(registrationId)) {
if (existingRegistrationId != null && !registrationId.equals(existingRegistrationId)) {
Log.e("WARNING: It appears your app is registering " +
"with GCM/FCM using multiple GCM/FCM sender ids. Please be sure to call " +
"LeanplumPushService.setGcmSenderIds() with " +
"all of the GCM sender ids that you use, not just the one that you use with " +
"Leanplum. Otherwise, GCM/FCM push notifications may not work consistently.");
}
existingRegistrationId = registrationId;
provider.onRegistrationIdReceived(getApplicationContext(), registrationId);
}
} catch (Throwable t) {
Log.e("Failed to complete registration token refresh.", t);
}
}
}

View File

@ -31,10 +31,8 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import com.leanplum.callbacks.VariablesChangedCallback;
import com.leanplum.internal.ActionManager;
@ -44,12 +42,10 @@ import com.leanplum.internal.Constants.Methods;
import com.leanplum.internal.Constants.Params;
import com.leanplum.internal.JsonConverter;
import com.leanplum.internal.LeanplumInternal;
import com.leanplum.internal.LeanplumManifestHelper;
import com.leanplum.internal.Log;
import com.leanplum.internal.Request;
import com.leanplum.internal.RequestOld;
import com.leanplum.internal.Util;
import com.leanplum.internal.VarCache;
import com.leanplum.utils.BitmapUtil;
import com.leanplum.utils.BuildUtil;
import com.leanplum.utils.SharedPreferencesUtil;
@ -91,24 +87,88 @@ public class LeanplumPushService {
* LeanplumPushService#parseNotificationBundle(Bundle)}.
*/
public static final String LEANPLUM_MESSAGE_ID = "lp_message_id";
private static final String LEANPLUM_PUSH_SERVICE_GCM = "com.leanplum.LeanplumPushServiceGcm";
private static final String LEANPLUM_PUSH_SERVICE_FCM = "com.leanplum.LeanplumPushServiceFcm";
private static final String LEANPLUM_PUSH_FCM_LISTENER_SERVICE_CLASS =
"com.leanplum.LeanplumPushFcmListenerService";
private static final String PUSH_FIREBASE_MESSAGING_SERVICE_CLASS =
"com.leanplum.LeanplumPushFirebaseMessagingService";
private static final String LEANPLUM_PUSH_INSTANCE_ID_SERVICE_CLASS =
"com.leanplum.LeanplumPushInstanceIDService";
private static final String LEANPLUM_PUSH_LISTENER_SERVICE_CLASS =
"com.leanplum.LeanplumPushListenerService";
private static final String GCM_RECEIVER_CLASS = "com.google.android.gms.gcm.GcmReceiver";
private static final int NOTIFICATION_ID = 1;
private static final String OPEN_URL = "Open URL";
private static final String URL = "URL";
private static final String OPEN_ACTION = "Open";
private static final int MAX_ONE_LINE_TEXT_LENGTH = 37;
private static final String COM_LEANPLUM_GCM_PROVIDER = "com.leanplum.LeanplumGcmProvider";
private static Class<? extends Activity> callbackClass;
private static LeanplumCloudMessagingProvider provider;
private static boolean isFirebaseEnabled = false;
private static LeanplumPushNotificationCustomizer customizer;
private static boolean useNotificationBuilderCustomizer = false;
/**
* Sets the Google Cloud Messaging sender ID. Required for push GCM notifications to work.
*
* @param senderId The GCM sender ID to permit notifications from. Use {@link
* LeanplumPushService#LEANPLUM_SENDER_ID} to use the built-in sender ID for GCM. If you have
* multiple sender IDs, use {@link LeanplumPushService#setGcmSenderIds}.
*/
public static void setGcmSenderId(String senderId) {
try {
Class.forName(COM_LEANPLUM_GCM_PROVIDER).getDeclaredMethod("setSenderId",
String.class).invoke(new Object(), senderId);
} catch (Throwable throwable) {
Log.e("Couldn't invoke a LeanplumGcmProvider.setGcmSenderId method, please be " +
"sure you include LeanplumGCM module.", throwable);
}
}
/**
* Sets the Google Cloud Messaging sender ID. Required for push GCM notifications to work.
*
* @param senderIds The GCM sender IDs to permit notifications from. Use {@link
* LeanplumPushService#LEANPLUM_SENDER_ID} to use the built-in sender ID.
*/
public static void setGcmSenderIds(String... senderIds) {
StringBuilder joinedSenderIds = new StringBuilder();
for (String senderId : senderIds) {
if (joinedSenderIds.length() > 0) {
joinedSenderIds.append(',');
}
joinedSenderIds.append(senderId);
}
try {
Class.forName(COM_LEANPLUM_GCM_PROVIDER).getDeclaredMethod("setSenderId",
String.class).invoke(new Object(), joinedSenderIds.toString());
} catch (Throwable throwable) {
Log.e("Couldn't invoke a LeanplumGcmProvider.setGcmSenderId method, please be " +
"sure you include LeanplumGCM module.", throwable);
}
}
/**
* Use Firebase Cloud Messaging, instead of the default Google Cloud Messaging.
*
* @deprecated FCM is no longer packaged in the SDK. Instead it is split up into modules.
* Modify your build.gradle by replacing implementation 'com.leanplum:Leanplum:+'
* with each module separately.
*
* For example:
* implementation 'com.leanplum:leanplum-fcm:+'
* implementation 'com.leanplum:leanplum-location:+'
*/
@Deprecated
public static void enableFirebase() {
LeanplumPushService.isFirebaseEnabled = true;
Log.e("enableFirebase() is deprecated and FCM is not enabled! " +
"SDK has been split up into modules and you need to modify your build.gradle. " +
"See the doc for more info.");
}
/**
* Whether Firebase Cloud Messaging is enabled or not.
*
* @return Boolean - true if enabled
*/
static boolean isFirebaseEnabled() {
return isFirebaseEnabled;
}
/**
* Get Cloud Messaging provider. By default - GCM.
@ -119,6 +179,10 @@ public class LeanplumPushService {
return provider;
}
static void setCloudMessagingProvider(LeanplumCloudMessagingProvider cloudMessagingProvider) {
provider = cloudMessagingProvider;
}
/**
* Changes the default activity to launch if the user opens a push notification.
*
@ -129,43 +193,31 @@ public class LeanplumPushService {
}
/**
* Sets an object used to customize the appearance of notifications. <p>Call this from your
* Sets an object used to customize the appearance of notifications. Call this from your
* Application class's onCreate method so that the customizer is set when your application starts
* in the background.
*
* @param customizer LeanplumPushNotificationCustomizer push notification customizer.
*/
public static void setCustomizer(LeanplumPushNotificationCustomizer customizer) {
setCustomizer(customizer, false);
}
/**
* Sets an object used to customize the appearance of notifications. Call this from your
* Application class's onCreate method so that the customizer is set when your application starts
* in the background.
*
* @param customizer LeanplumPushNotificationCustomizer push notification customizer.
* @param useNotificationBuilderCustomizer True if if you want to support 2 lines of text on
* BigPicture style push notification.
*/
public static void setCustomizer(LeanplumPushNotificationCustomizer customizer,
boolean useNotificationBuilderCustomizer) {
LeanplumPushService.customizer = customizer;
LeanplumPushService.useNotificationBuilderCustomizer = useNotificationBuilderCustomizer;
}
/**
* Sets the Google Cloud Messaging/Firebase Cloud Messaging sender ID. Required for push
* notifications to work.
*
* @param senderId The GCM/FCM sender ID to permit notifications from. Use {@link
* LeanplumPushService#LEANPLUM_SENDER_ID} to use the built-in sender ID for GCM. If you have
* multiple sender IDs, use {@link LeanplumPushService#setGcmSenderIds}.
*/
public static void setGcmSenderId(String senderId) {
LeanplumGcmProvider.setSenderId(senderId);
}
/**
* Sets the Google Cloud Messaging/Firebase Cloud Messaging sender ID. Required for push
* notifications to work.
*
* @param senderIds The GCM/FCM sender IDs to permit notifications from. Use {@link
* LeanplumPushService#LEANPLUM_SENDER_ID} to use the built-in sender ID.
*/
public static void setGcmSenderIds(String... senderIds) {
StringBuilder joinedSenderIds = new StringBuilder();
for (String senderId : senderIds) {
if (joinedSenderIds.length() > 0) {
joinedSenderIds.append(',');
}
joinedSenderIds.append(senderId);
}
LeanplumGcmProvider.setSenderId(joinedSenderIds.toString());
}
private static Class<? extends Activity> getCallbackClass() {
return callbackClass;
@ -190,8 +242,8 @@ public class LeanplumPushService {
Map<String, Object> params = new HashMap<>();
params.put(Params.INCLUDE_DEFAULTS, Boolean.toString(false));
params.put(Params.INCLUDE_MESSAGE_ID, messageId);
Request req = Request.post(Methods.GET_VARS, params);
req.onResponse(new Request.ResponseCallback() {
RequestOld req = RequestOld.post(Methods.GET_VARS, params);
req.onResponse(new RequestOld.ResponseCallback() {
@Override
public void response(JSONObject response) {
try {
@ -215,7 +267,7 @@ public class LeanplumPushService {
messages = null;
}
if (values != null || messages != null) {
VarCache.applyVariableDiffs(values, messages, null, null, regions, variants);
VarCache.applyVariableDiffs(values, messages, null, null, regions, variants, null);
}
}
onComplete.variablesChanged();
@ -224,7 +276,7 @@ public class LeanplumPushService {
}
}
});
req.onError(new Request.ErrorCallback() {
req.onError(new RequestOld.ErrorCallback() {
@Override
public void error(Exception e) {
onComplete.variablesChanged();
@ -279,6 +331,8 @@ public class LeanplumPushService {
// Leanplum.track("Displayed", 0.0, null, null, requestArgs);
showNotification(context, message);
Leanplum.countAggregator().incrementCount("handle_notification");
}
/**
@ -309,75 +363,69 @@ public class LeanplumPushService {
final NotificationManager notificationManager = (NotificationManager)
context.getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager == null) {
return;
}
Intent intent = new Intent(context, LeanplumPushReceiver.class);
intent.addCategory("lpAction");
intent.putExtras(message);
PendingIntent contentIntent = PendingIntent.getBroadcast(
context.getApplicationContext(), new Random().nextInt(),
intent, 0);
PendingIntent contentIntent = PendingIntent.getBroadcast(context.getApplicationContext(),
new Random().nextInt(), intent, 0);
String title = Util.getApplicationName(context.getApplicationContext());
if (message.getString("title") != null) {
title = message.getString("title");
}
final NotificationCompat.Builder notificationCompatBuilder =
LeanplumNotificationHelper.getNotificationCompatBuilder(context, message);
NotificationCompat.Builder notificationCompatBuilder = null;
Notification.Builder notificationBuilder = null;
if (notificationCompatBuilder == null) {
final String messageText = message.getString(Keys.PUSH_MESSAGE_TEXT);
Bitmap bigPicture = LeanplumNotificationHelper.getBigPictureBitmap(context,
message.getString(Keys.PUSH_MESSAGE_IMAGE_URL));
if (customizer != null && !useNotificationBuilderCustomizer) {
notificationCompatBuilder = LeanplumNotificationHelper.getNotificationCompatBuilder(context,
message, contentIntent, title, messageText, bigPicture, defaultIconId);
} else {
notificationBuilder = LeanplumNotificationHelper.getNotificationBuilder(context, message,
contentIntent, title, messageText, defaultIconId);
}
if ((notificationCompatBuilder == null && notificationBuilder == null) ||
(customizer != null & !useNotificationBuilderCustomizer &&
notificationCompatBuilder == null) ||
(customizer != null && useNotificationBuilderCustomizer && notificationBuilder == null) ||
(customizer == null && notificationBuilder == null)) {
return;
}
final String messageText = message.getString(Keys.PUSH_MESSAGE_TEXT);
if (defaultIconId == 0) {
notificationCompatBuilder.setSmallIcon(context.getApplicationInfo().icon);
} else {
notificationCompatBuilder.setSmallIcon(defaultIconId);
}
notificationCompatBuilder.setContentTitle(title)
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(messageText))
.setContentText(messageText);
String imageUrl = message.getString(Keys.PUSH_MESSAGE_IMAGE_URL);
Notification.Builder notificationBuilder = null;
// BigPictureStyle support requires API 16 and higher.
if (!TextUtils.isEmpty(imageUrl) && Build.VERSION.SDK_INT >= 16) {
Bitmap bigPicture = BitmapUtil.getScaledBitmap(context, imageUrl);
if (bigPicture != null) {
if ((messageText != null && messageText.length() < MAX_ONE_LINE_TEXT_LENGTH) ||
customizer != null) {
notificationCompatBuilder.setStyle(new NotificationCompat.BigPictureStyle()
.bigPicture(bigPicture)
.setBigContentTitle(title)
.setSummaryText(messageText));
} else {
notificationBuilder = LeanplumNotificationHelper.getNotificationBuilder(context, message,
contentIntent, title, messageText, bigPicture, defaultIconId);
}
} else {
Log.w(String.format("Image download failed for push notification with big picture. " +
"No image will be included with the push notification. Image URL: %s.", imageUrl));
}
}
// Try to put a notification on top of the notification area. This method was deprecated in API
// level 26. For API level 26 and above we must use setImportance(int) for each notification
// channel, not for each notification message.
if (Build.VERSION.SDK_INT >= 16 && !BuildUtil.isNotificationChannelSupported(context)) {
//noinspection deprecation
notificationCompatBuilder.setPriority(Notification.PRIORITY_MAX);
}
notificationCompatBuilder.setAutoCancel(true);
notificationCompatBuilder.setContentIntent(contentIntent);
if (LeanplumPushService.customizer != null) {
if (customizer != null) {
try {
LeanplumPushService.customizer.customize(notificationCompatBuilder, message);
if (useNotificationBuilderCustomizer) {
if (bigPicture != null) {
Notification.BigPictureStyle bigPictureStyle =
LeanplumNotificationHelper.getBigPictureStyle(message, bigPicture, title,
messageText);
customizer.customize(notificationBuilder, message, bigPictureStyle);
LeanplumNotificationHelper.setModifiedBigPictureStyle(notificationBuilder,
bigPictureStyle);
} else {
customizer.customize(notificationBuilder, message, null);
}
} else {
customizer.customize(notificationCompatBuilder, message);
}
} catch (Throwable t) {
Log.e("Unable to customize push notification: ", Log.getStackTraceString(t));
return;
}
} else {
if (bigPicture != null) {
Notification.BigPictureStyle bigPictureStyle =
LeanplumNotificationHelper.getBigPictureStyle(message, bigPicture, title, messageText);
LeanplumNotificationHelper.setModifiedBigPictureStyle(notificationBuilder, bigPictureStyle);
}
}
int notificationId = LeanplumPushService.NOTIFICATION_ID;
@ -403,13 +451,14 @@ public class LeanplumPushService {
JsonConverter.fromJson(message.getString(Keys.PUSH_MESSAGE_ACTION)))) {
final int currentNotificationId = notificationId;
final Notification.Builder currentNotificationBuilder = notificationBuilder;
final NotificationCompat.Builder currentNotificationCompatBuilder = notificationCompatBuilder;
Leanplum.forceContentUpdate(new VariablesChangedCallback() {
@Override
public void variablesChanged() {
if (currentNotificationBuilder != null) {
notificationManager.notify(currentNotificationId, currentNotificationBuilder.build());
} else {
notificationManager.notify(currentNotificationId, notificationCompatBuilder.build());
notificationManager.notify(currentNotificationId, currentNotificationCompatBuilder.build());
}
}
});
@ -426,6 +475,7 @@ public class LeanplumPushService {
Log.e("Unable to show push notification.", t);
Util.handleException(t);
}
Leanplum.countAggregator().incrementCount("show_with_title");
}
static void openNotification(Context context, Intent intent) {
@ -669,13 +719,16 @@ public class LeanplumPushService {
* Stores the registration ID and app versionCode in the application's shared preferences.
*/
private static void registerInBackground() {
Context context = Leanplum.getContext();
if (context == null) {
Log.e("Failed to register application with GCM/FCM. Your application context is not set.");
return;
try {
Context context = Leanplum.getContext();
if (context == null) {
Log.e("Failed to register application with GCM/FCM. Your application context is not set.");
return;
}
Intent registerIntent = new Intent(context, LeanplumPushRegistrationService.class);
context.startService(registerIntent);
} catch (Throwable ignored) {
}
Intent registerIntent = new Intent(context, LeanplumPushRegistrationService.class);
context.startService(registerIntent);
}
/**
@ -688,126 +741,57 @@ public class LeanplumPushService {
}
/**
* Call this when Leanplum starts.
* Call this when Leanplum starts. This method will call by reflection from AndroidSDKCore.
*/
static void onStart() {
Class leanplumGcmPushServiceClass = null;
Class leanplumFcmPushServiceClass = null;
try {
if (Util.hasPlayServices()) {
initPushService();
} else {
Log.i("No valid Google Play Services APK found.");
}
} catch (LeanplumException e) {
Log.e("There was an error registering for push notifications.\n" +
Log.getStackTraceString(e));
leanplumGcmPushServiceClass = Class.forName(LEANPLUM_PUSH_SERVICE_GCM);
} catch (Throwable ignored) {
}
try {
leanplumFcmPushServiceClass = Class.forName(LEANPLUM_PUSH_SERVICE_FCM);
} catch (Throwable ignored) {
}
if (leanplumGcmPushServiceClass != null && leanplumFcmPushServiceClass != null) {
Log.e("Leanplum does not support leanplum-gcm and leanplum-fcm library at the " +
"same time. To support Leanplum GCM and Location services modify your build.gradle by " +
"including only implementation 'com.leanplum:leanplum:+' " +
"To support only GCM services, use implementation 'com.leanplum:leanplum-gcm:+' " +
"For FCM services include implementation 'com.leanplum:leanplum-fcm:+'" +
" If you wish to use Leanplum FCM and Location services you also need to include " +
"implementation 'com.leanplum:leanplum-location:+'.");
} else if (leanplumGcmPushServiceClass != null) {
try {
leanplumGcmPushServiceClass.getDeclaredMethod("onStart").invoke(null);
} catch (Throwable ignored) {
}
} else if (leanplumFcmPushServiceClass != null) {
try {
leanplumFcmPushServiceClass.getDeclaredMethod("onStart").invoke(null);
} catch (Throwable ignored) {
}
}
}
/**
* Initialize push service.
*/
private static void initPushService() {
if (!enableGcmServices()) {
Log.w("Failed to initialize GCM services.");
return;
}
provider = new LeanplumGcmProvider();
static void initPushService() {
if (!provider.isInitialized() || !provider.isManifestSetup()) {
return;
}
if (hasAppIDChanged(Request.appId())) {
if (hasAppIDChanged(RequestOld.appId())) {
provider.unregister();
}
registerInBackground();
}
/**
* Enables GCM services. By default, all GCM services are disabled.
*
* @return true if services are successfully enabled, false otherwise
*/
private static boolean enableGcmServices() {
Context context = Leanplum.getContext();
if (context == null) {
Log.i("Failed to enable FCM services, context is null.");
return false;
}
PackageManager packageManager = context.getPackageManager();
if (packageManager == null) {
Log.i("Failed to enable FCM services, PackageManager is null.");
return false;
}
Class gcm = LeanplumManifestHelper.getClassForName(LEANPLUM_PUSH_INSTANCE_ID_SERVICE_CLASS);
if (gcm == null) {
Log.e("Failed to setup GCM, please compile GCM library.");
return false;
}
// We will only enable component once, if we are switching from FCM to GCM, we have to disable
// FCM services first.
if (!LeanplumManifestHelper.wasComponentEnabled(context, packageManager, gcm)) {
// Try to disable FCM first.
disableFcmServices();
LeanplumManifestHelper.enableComponent(context, packageManager, gcm);
// Make sure we can find the class before enabling it.
Class gcmReceiver = LeanplumManifestHelper.getClassForName(GCM_RECEIVER_CLASS);
if (gcmReceiver != null) {
LeanplumManifestHelper.enableComponent(context, packageManager, gcmReceiver);
}
Class pushListener = LeanplumManifestHelper.getClassForName(LEANPLUM_PUSH_LISTENER_SERVICE_CLASS);
if (pushListener != null) {
LeanplumManifestHelper.enableComponent(context, packageManager, pushListener);
}
}
return true;
}
/**
* Disables FCM services.
*/
private static void disableFcmServices() {
Context context = Leanplum.getContext();
if (context == null) {
return;
}
PackageManager packageManager = context.getPackageManager();
if (packageManager == null) {
return;
}
LeanplumManifestHelper.disableComponent(context, packageManager,
LEANPLUM_PUSH_FCM_LISTENER_SERVICE_CLASS);
LeanplumManifestHelper.disableComponent(context, packageManager,
PUSH_FIREBASE_MESSAGING_SERVICE_CLASS);
}
/**
* Disables GCM services
*/
private static void disableGcmServices() {
Context context = Leanplum.getContext();
if (context == null) {
return;
}
PackageManager packageManager = context.getPackageManager();
if (packageManager == null) {
return;
}
LeanplumManifestHelper.disableComponent(context, packageManager,
LEANPLUM_PUSH_INSTANCE_ID_SERVICE_CLASS);
LeanplumManifestHelper.disableComponent(context, packageManager,
GCM_RECEIVER_CLASS);
LeanplumManifestHelper.disableComponent(context, packageManager,
LEANPLUM_PUSH_LISTENER_SERVICE_CLASS);
}
/**
* Check if current application id is different from stored one.
*
@ -837,4 +821,34 @@ public class LeanplumPushService {
}
return false;
}
/**
* Show notification that device was registered for push notifications. This method will call by
* reflection from AndroidSDKCore.
*
* @param context The application context.
* @param currentContext Current application context.
*/
static void showDeviceRegistedPush(Context context, Context currentContext) {
try {
NotificationCompat.Builder builder =
LeanplumNotificationHelper.getDefaultCompatNotificationBuilder(context,
BuildUtil.isNotificationChannelSupported(context));
if (builder == null) {
return;
}
builder.setSmallIcon(android.R.drawable.star_on)
.setContentTitle("Leanplum")
.setContentText("Your device is registered.");
builder.setContentIntent(PendingIntent.getActivity(
currentContext.getApplicationContext(), 0, new Intent(), 0));
NotificationManager mNotificationManager =
(NotificationManager) currentContext.getSystemService(
Context.NOTIFICATION_SERVICE);
// mId allows you to update the notification later on.
mNotificationManager.notify(0, builder.build());
} catch (Throwable t) {
Log.i("Device is registered.");
}
}
}

View File

@ -0,0 +1,24 @@
package com.leanplum;
import com.leanplum.internal.Log;
/**
* Created by anna on 12/15/17.
*/
public class LeanplumPushServiceGcm {
/**
* Call this when from {@link LeanplumPushService#onStart()} when Leanplum start for initialize
* push provider and init push service. This method will call by reflection from AndroidSDKPush.
*/
static void onStart() {
try {
LeanplumPushService.setCloudMessagingProvider(new LeanplumGcmProvider());
LeanplumPushService.initPushService();
} catch (LeanplumException e) {
Log.e("There was an error registering for push notifications.\n" +
Log.getStackTraceString(e));
} catch (Throwable ignored) {
}
}
}

View File

@ -26,7 +26,7 @@ import java.util.Set;
/**
* Public interface to LocationManager. This is abstracted away so that the Google Play Services
* dependencies are constrained to {@link LocationManagerImplementation}.
* dependencies are constrained to LocationManagerImplementation in AndroidSDKLocation.
*
* @author Andrew First
*/

View File

@ -1,67 +0,0 @@
/*
* Copyright 2015, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum;
import com.leanplum.callbacks.InboxChangedCallback;
import com.leanplum.callbacks.NewsfeedChangedCallback;
/**
* Newsfeed class.
*
* @author Aleksandar Gyorev
*/
public class Newsfeed extends LeanplumInbox {
private static Newsfeed instance = new Newsfeed();
/**
* A private constructor, which prevents any other class from instantiating.
*/
private Newsfeed() {
super();
}
/**
* Static 'getInstance' method.
*/
static Newsfeed getInstance() {
return instance;
}
/**
* Add a callback for when the newsfeed receives new values from the server.
*
* @deprecated use {@link #addChangedHandler(InboxChangedCallback)} instead
*/
@Deprecated
public void addNewsfeedChangedHandler(NewsfeedChangedCallback handler) {
super.addChangedHandler(handler);
}
/**
* Removes a newsfeed changed callback.
*
* @deprecated use {@link #removeChangedHandler(InboxChangedCallback)} instead
*/
@Deprecated
public void removeNewsfeedChangedHandler(NewsfeedChangedCallback handler) {
super.removeChangedHandler(handler);
}
}

View File

@ -1,219 +0,0 @@
/*
* Copyright 2015, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum;
import com.leanplum.internal.Constants;
import com.leanplum.internal.Request;
import com.leanplum.internal.Util;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* NewsfeedMessage class.
*
* @author Aleksandar Gyorev
*/
public abstract class NewsfeedMessage {
private String messageId;
private Long deliveryTimestamp;
private Long expirationTimestamp;
private boolean isRead;
private ActionContext context;
NewsfeedMessage(String messageId, Long deliveryTimestamp, Long expirationTimestamp,
boolean isRead, ActionContext context) {
this.messageId = messageId;
this.deliveryTimestamp = deliveryTimestamp;
this.expirationTimestamp = expirationTimestamp;
this.isRead = isRead;
this.context = context;
}
Map<String, Object> toJsonMap() {
Map<String, Object> map = new HashMap<>();
map.put(Constants.Keys.DELIVERY_TIMESTAMP, this.deliveryTimestamp);
map.put(Constants.Keys.EXPIRATION_TIMESTAMP, this.expirationTimestamp);
map.put(Constants.Keys.MESSAGE_DATA, this.actionArgs());
map.put(Constants.Keys.IS_READ, this.isRead());
return map;
}
Map<String, Object> actionArgs() {
return context.getArgs();
}
void setIsRead(boolean isRead) {
this.isRead = isRead;
}
boolean isActive() {
if (expirationTimestamp == null) {
return true;
}
Date now = new Date();
return now.before(new Date(expirationTimestamp));
}
static boolean isValidMessageId(String messageId) {
return messageId.split("##").length == 2;
}
ActionContext getContext() {
return context;
}
/**
* Returns the message identifier of the newsfeed message.
*
* @deprecated As of release 1.3.0, replaced by {@link #getMessageId()}
*/
@Deprecated
public String messageId() {
return getMessageId();
}
/**
* Returns the message identifier of the newsfeed message.
*/
public String getMessageId() {
return messageId;
}
/**
* Returns the title of the newsfeed message.
*
* @deprecated As of release 1.3.0, replaced by {@link #getTitle()}
*/
@Deprecated
public String title() {
return getTitle();
}
/**
* Returns the title of the newsfeed message.
*/
public String getTitle() {
return context.stringNamed(Constants.Keys.TITLE);
}
/**
* Returns the subtitle of the newsfeed message.
*
* @deprecated As of release 1.3.0, replaced by {@link #getSubtitle()}
*/
@Deprecated
public String subtitle() {
return getSubtitle();
}
/**
* Returns the subtitle of the newsfeed message.
*/
public String getSubtitle() {
return context.stringNamed(Constants.Keys.SUBTITLE);
}
/**
* Returns the delivery timestamp of the newsfeed message.
*
* @deprecated As of release 1.3.0, replaced by {@link #getDeliveryTimestamp()}
*/
@Deprecated
public Date deliveryTimestamp() {
return getDeliveryTimestamp();
}
/**
* Returns the delivery timestamp of the newsfeed message.
*/
public Date getDeliveryTimestamp() {
return new Date(deliveryTimestamp);
}
/**
* Return the expiration timestamp of the newsfeed message.
*
* @deprecated As of release 1.3.0, replaced by {@link #getExpirationTimestamp()}
*/
@Deprecated
public Date expirationTimestamp() {
return getExpirationTimestamp();
}
/**
* Return the expiration timestamp of the newsfeed message.
*/
public Date getExpirationTimestamp() {
if (expirationTimestamp == null) {
return null;
}
return new Date(expirationTimestamp);
}
/**
* Returns 'true' if the newsfeed message is read.
*/
public boolean isRead() {
return isRead;
}
/**
* Read the newsfeed message, marking it as read and invoking its open action.
*/
public void read() {
try {
if (Constants.isNoop()) {
return;
}
if (!this.isRead) {
setIsRead(true);
int unreadCount = Newsfeed.getInstance().unreadCount() - 1;
Newsfeed.getInstance().updateUnreadCount(unreadCount);
Map<String, Object> params = new HashMap<>();
params.put(Constants.Params.INBOX_MESSAGE_ID, messageId);
Request req = Request.post(Constants.Methods.MARK_INBOX_MESSAGE_AS_READ,
params);
req.send();
}
this.context.runTrackedActionNamed(Constants.Values.DEFAULT_PUSH_ACTION);
} catch (Throwable t) {
Util.handleException(t);
}
}
/**
* Remove the newsfeed message from the newsfeed.
*/
public void remove() {
try {
Newsfeed.getInstance().removeMessage(messageId);
} catch (Throwable t) {
Util.handleException(t);
}
}
}

View File

@ -1,80 +0,0 @@
/*
* Copyright 2013, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.activities;
import android.accounts.AccountAuthenticatorActivity;
import android.content.res.Resources;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumActivityHelper;
/**
* @deprecated due to rising minimal API to 14. This class will be removed in a
* future major release. Please use {@link LeanplumActivityHelper} to track your activities
* automatically.
*/
@Deprecated
public class LeanplumAccountAuthenticatorActivity extends AccountAuthenticatorActivity {
private LeanplumActivityHelper helper;
private LeanplumActivityHelper getHelper() {
if (helper == null) {
helper = new LeanplumActivityHelper(this);
}
return helper;
}
@Override
protected void onPause() {
super.onPause();
getHelper().onPause();
}
@Override
protected void onStop() {
super.onStop();
getHelper().onStop();
}
@Override
protected void onResume() {
super.onResume();
getHelper().onResume();
}
@Override
public Resources getResources() {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
return super.getResources();
}
return getHelper().getLeanplumResources(super.getResources());
}
@Override
public void setContentView(final int layoutResID) {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
super.setContentView(layoutResID);
return;
}
getHelper().setContentView(layoutResID);
}
}

View File

@ -1,80 +0,0 @@
/*
* Copyright 2013, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.activities;
import android.app.Activity;
import android.content.res.Resources;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumActivityHelper;
/**
* @deprecated due to rising minimal API to 14. This class will be removed in a
* future major release. Please use {@link LeanplumActivityHelper} to track your activities
* automatically.
*/
@Deprecated
public abstract class LeanplumActivity extends Activity {
private LeanplumActivityHelper helper;
private LeanplumActivityHelper getHelper() {
if (helper == null) {
helper = new LeanplumActivityHelper(this);
}
return helper;
}
@Override
protected void onPause() {
super.onPause();
getHelper().onPause();
}
@Override
protected void onStop() {
super.onStop();
getHelper().onStop();
}
@Override
protected void onResume() {
super.onResume();
getHelper().onResume();
}
@Override
public Resources getResources() {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
return super.getResources();
}
return getHelper().getLeanplumResources(super.getResources());
}
@Override
public void setContentView(final int layoutResID) {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
super.setContentView(layoutResID);
return;
}
getHelper().setContentView(layoutResID);
}
}

View File

@ -1,80 +0,0 @@
/*
* Copyright 2013, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.activities;
import android.app.ActivityGroup;
import android.content.res.Resources;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumActivityHelper;
/**
* @deprecated due to rising minimal API to 14. This class will be removed in a
* future major release. Please use {@link LeanplumActivityHelper} to track your activities
* automatically.
*/
@Deprecated
public class LeanplumActivityGroup extends ActivityGroup {
private LeanplumActivityHelper helper;
private LeanplumActivityHelper getHelper() {
if (helper == null) {
helper = new LeanplumActivityHelper(this);
}
return helper;
}
@Override
protected void onPause() {
super.onPause();
getHelper().onPause();
}
@Override
protected void onStop() {
super.onStop();
getHelper().onStop();
}
@Override
protected void onResume() {
super.onResume();
getHelper().onResume();
}
@Override
public Resources getResources() {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
return super.getResources();
}
return getHelper().getLeanplumResources(super.getResources());
}
@Override
public void setContentView(final int layoutResID) {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
super.setContentView(layoutResID);
return;
}
getHelper().setContentView(layoutResID);
}
}

View File

@ -1,80 +0,0 @@
/*
* Copyright 2013, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.activities;
import android.app.AliasActivity;
import android.content.res.Resources;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumActivityHelper;
/**
* @deprecated due to rising minimal API to 14. This class will be removed in a
* future major release. Please use {@link LeanplumActivityHelper} to track your activities
* automatically.
*/
@Deprecated
public class LeanplumAliasActivity extends AliasActivity {
private LeanplumActivityHelper helper;
private LeanplumActivityHelper getHelper() {
if (helper == null) {
helper = new LeanplumActivityHelper(this);
}
return helper;
}
@Override
protected void onPause() {
super.onPause();
getHelper().onPause();
}
@Override
protected void onStop() {
super.onStop();
getHelper().onStop();
}
@Override
protected void onResume() {
super.onResume();
getHelper().onResume();
}
@Override
public Resources getResources() {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
return super.getResources();
}
return getHelper().getLeanplumResources(super.getResources());
}
@Override
public void setContentView(final int layoutResID) {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
super.setContentView(layoutResID);
return;
}
getHelper().setContentView(layoutResID);
}
}

View File

@ -1,80 +0,0 @@
/*
* Copyright 2015, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.activities;
import android.content.res.Resources;
import android.support.v7.app.AppCompatActivity;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumActivityHelper;
/**
* @deprecated due to rising minimal API to 14. This class will be removed in a
* future major release. Please use {@link LeanplumActivityHelper} to track your activities
* automatically.
*/
@Deprecated
public class LeanplumAppCompatActivity extends AppCompatActivity {
private LeanplumActivityHelper helper;
private LeanplumActivityHelper getHelper() {
if (helper == null) {
helper = new LeanplumActivityHelper(this);
}
return helper;
}
@Override
protected void onPause() {
super.onPause();
getHelper().onPause();
}
@Override
protected void onStop() {
super.onStop();
getHelper().onStop();
}
@Override
protected void onResume() {
super.onResume();
getHelper().onResume();
}
@Override
public Resources getResources() {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
return super.getResources();
}
return getHelper().getLeanplumResources(super.getResources());
}
@Override
public void setContentView(final int layoutResID) {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
super.setContentView(layoutResID);
return;
}
getHelper().setContentView(layoutResID);
}
}

View File

@ -1,80 +0,0 @@
/*
* Copyright 2013, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.activities;
import android.app.ExpandableListActivity;
import android.content.res.Resources;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumActivityHelper;
/**
* @deprecated due to rising minimal API to 14. This class will be removed in a
* future major release. Please use {@link LeanplumActivityHelper} to track your activities
* automatically.
*/
@Deprecated
public class LeanplumExpandableListActivity extends ExpandableListActivity {
private LeanplumActivityHelper helper;
private LeanplumActivityHelper getHelper() {
if (helper == null) {
helper = new LeanplumActivityHelper(this);
}
return helper;
}
@Override
protected void onPause() {
super.onPause();
getHelper().onPause();
}
@Override
protected void onStop() {
super.onStop();
getHelper().onStop();
}
@Override
protected void onResume() {
super.onResume();
getHelper().onResume();
}
@Override
public Resources getResources() {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
return super.getResources();
}
return getHelper().getLeanplumResources(super.getResources());
}
@Override
public void setContentView(final int layoutResID) {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
super.setContentView(layoutResID);
return;
}
getHelper().setContentView(layoutResID);
}
}

View File

@ -1,80 +0,0 @@
/*
* Copyright 2013, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.activities;
import android.content.res.Resources;
import android.support.v4.app.FragmentActivity;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumActivityHelper;
/**
* @deprecated due to rising minimal API to 14. This class will be removed in a
* future major release. Please use {@link LeanplumActivityHelper} to track your activities
* automatically.
*/
@Deprecated
public abstract class LeanplumFragmentActivity extends FragmentActivity {
private LeanplumActivityHelper helper;
private LeanplumActivityHelper getHelper() {
if (helper == null) {
helper = new LeanplumActivityHelper(this);
}
return helper;
}
@Override
protected void onPause() {
super.onPause();
getHelper().onPause();
}
@Override
protected void onStop() {
super.onStop();
getHelper().onStop();
}
@Override
protected void onResume() {
super.onResume();
getHelper().onResume();
}
@Override
public Resources getResources() {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
return super.getResources();
}
return getHelper().getLeanplumResources(super.getResources());
}
@Override
public void setContentView(final int layoutResID) {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
super.setContentView(layoutResID);
return;
}
getHelper().setContentView(layoutResID);
}
}

View File

@ -1,80 +0,0 @@
/*
* Copyright 2013, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.activities;
import android.app.LauncherActivity;
import android.content.res.Resources;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumActivityHelper;
/**
* @deprecated due to rising minimal API to 14. This class will be removed in a
* future major release. Please use {@link LeanplumActivityHelper} to track your activities
* automatically.
*/
@Deprecated
public class LeanplumLauncherActivity extends LauncherActivity {
private LeanplumActivityHelper helper;
private LeanplumActivityHelper getHelper() {
if (helper == null) {
helper = new LeanplumActivityHelper(this);
}
return helper;
}
@Override
protected void onPause() {
super.onPause();
getHelper().onPause();
}
@Override
protected void onStop() {
super.onStop();
getHelper().onStop();
}
@Override
protected void onResume() {
super.onResume();
getHelper().onResume();
}
@Override
public Resources getResources() {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
return super.getResources();
}
return getHelper().getLeanplumResources(super.getResources());
}
@Override
public void setContentView(final int layoutResID) {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
super.setContentView(layoutResID);
return;
}
getHelper().setContentView(layoutResID);
}
}

View File

@ -1,80 +0,0 @@
/*
* Copyright 2013, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.activities;
import android.app.ListActivity;
import android.content.res.Resources;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumActivityHelper;
/**
* @deprecated due to rising minimal API to 14. This class will be removed in a
* future major release. Please use {@link LeanplumActivityHelper} to track your activities
* automatically.
*/
@Deprecated
public class LeanplumListActivity extends ListActivity {
private LeanplumActivityHelper helper;
private LeanplumActivityHelper getHelper() {
if (helper == null) {
helper = new LeanplumActivityHelper(this);
}
return helper;
}
@Override
protected void onPause() {
super.onPause();
getHelper().onPause();
}
@Override
protected void onStop() {
super.onStop();
getHelper().onStop();
}
@Override
protected void onResume() {
super.onResume();
getHelper().onResume();
}
@Override
public Resources getResources() {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
return super.getResources();
}
return getHelper().getLeanplumResources(super.getResources());
}
@Override
public void setContentView(final int layoutResID) {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
super.setContentView(layoutResID);
return;
}
getHelper().setContentView(layoutResID);
}
}

View File

@ -1,80 +0,0 @@
/*
* Copyright 2013, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.activities;
import android.app.NativeActivity;
import android.content.res.Resources;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumActivityHelper;
/**
* @deprecated due to rising minimal API to 14. This class will be removed in a
* future major release. Please use {@link LeanplumActivityHelper} to track your activities
* automatically.
*/
@Deprecated
public class LeanplumNativeActivity extends NativeActivity {
private LeanplumActivityHelper helper;
private LeanplumActivityHelper getHelper() {
if (helper == null) {
helper = new LeanplumActivityHelper(this);
}
return helper;
}
@Override
protected void onPause() {
super.onPause();
getHelper().onPause();
}
@Override
protected void onStop() {
super.onStop();
getHelper().onStop();
}
@Override
protected void onResume() {
super.onResume();
getHelper().onResume();
}
@Override
public Resources getResources() {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
return super.getResources();
}
return getHelper().getLeanplumResources(super.getResources());
}
@Override
public void setContentView(final int layoutResID) {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
super.setContentView(layoutResID);
return;
}
getHelper().setContentView(layoutResID);
}
}

View File

@ -1,79 +0,0 @@
/*
* Copyright 2013, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.activities;
import android.content.res.Resources;
import android.preference.PreferenceActivity;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumActivityHelper;
/**
* @deprecated due to rising minimal API to 14. This class will be removed in a
* future major release. Please use {@link LeanplumActivityHelper} to track your activities
* automatically.
*/
@Deprecated
public class LeanplumPreferenceActivity extends PreferenceActivity {
private LeanplumActivityHelper helper;
private LeanplumActivityHelper getHelper() {
if (helper == null) {
helper = new LeanplumActivityHelper(this);
}
return helper;
}
@Override
protected void onPause() {
super.onPause();
getHelper().onPause();
}
@Override
protected void onStop() {
super.onStop();
getHelper().onStop();
}
@Override
protected void onResume() {
super.onResume();
getHelper().onResume();
}
@Override
public Resources getResources() {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
return super.getResources();
}
return getHelper().getLeanplumResources(super.getResources());
}
@Override
public void setContentView(final int layoutResID) {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
super.setContentView(layoutResID);
return;
}
getHelper().setContentView(layoutResID);
}
}

View File

@ -1,80 +0,0 @@
/*
* Copyright 2013, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.activities;
import android.app.TabActivity;
import android.content.res.Resources;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumActivityHelper;
/**
* @deprecated due to rising minimal API to 14. This class will be removed in a
* future major release. Please use {@link LeanplumActivityHelper} to track your activities
* automatically.
*/
@Deprecated
public class LeanplumTabActivity extends TabActivity {
private LeanplumActivityHelper helper;
private LeanplumActivityHelper getHelper() {
if (helper == null) {
helper = new LeanplumActivityHelper(this);
}
return helper;
}
@Override
protected void onPause() {
super.onPause();
getHelper().onPause();
}
@Override
protected void onStop() {
super.onStop();
getHelper().onStop();
}
@Override
protected void onResume() {
super.onResume();
getHelper().onResume();
}
@Override
public Resources getResources() {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
return super.getResources();
}
return getHelper().getLeanplumResources(super.getResources());
}
@Override
public void setContentView(final int layoutResID) {
if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
super.setContentView(layoutResID);
return;
}
getHelper().setContentView(layoutResID);
}
}

View File

@ -1,3 +1,24 @@
/*
* Copyright 2017, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.callbacks;
/**
@ -6,20 +27,19 @@ package com.leanplum.callbacks;
* @author Anna Orlova
*/
public abstract class InboxSyncedCallback implements Runnable {
private boolean success;
private boolean success;
public void setSuccess(boolean success) {
this.success = success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public void run() {
this.onForceContentUpdate(success);
}
public void run() {
this.onForceContentUpdate(success);
}
/**
* Call when forceContentUpdate was called.
*
* @param success True if syncing was successful.
*/
public abstract void onForceContentUpdate(boolean success);
}
/**
* Call when forceContentUpdate was called.
* @param success True if syncing was successful.
*/
public abstract void onForceContentUpdate(boolean success);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2015, Leanplum, Inc. All rights reserved.
* Copyright 2013, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@ -21,16 +21,24 @@
package com.leanplum.callbacks;
import com.leanplum.models.MessageArchiveData;
/**
* Newsfeed changed callback.
* Message displayed callback.
*
* @author Aleksandar Gyorev
* @author Mayank Sanganeria
*/
public abstract class NewsfeedChangedCallback extends InboxChangedCallback {
@Override
public void inboxChanged() {
newsfeedChanged();
public abstract class MessageDisplayedCallback implements Runnable {
private MessageArchiveData messageArchiveData;
public void setMessageArchiveData(MessageArchiveData messageArchiveData) {
this.messageArchiveData = messageArchiveData;
}
public abstract void newsfeedChanged();
public void run() {
this.messageDisplayed(messageArchiveData);
}
public abstract void messageDisplayed(MessageArchiveData messageArchiveData);
}

View File

@ -21,6 +21,8 @@
package com.leanplum.callbacks;
import com.leanplum.Leanplum;
/**
* Callback that gets run when the device needs to be registered.
*
@ -45,10 +47,10 @@ public abstract class RegisterDeviceCallback implements Runnable {
public void setResponseHandler(EmailCallback callback) {
this.callback = callback;
Leanplum.countAggregator().incrementCount("init_with_callback");
}
public void run() {
this.onResponse(callback);
public void run() { this.onResponse(callback);
}
public abstract void onResponse(EmailCallback callback);

View File

@ -0,0 +1,47 @@
/*
* Copyright 2018, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.internal;
public class APIConfig {
private final FeatureFlagManager featureFlagManager;
private final CountAggregator countAggregator;
private String appId;
private String accessKey;
private String token;
public APIConfig(FeatureFlagManager featureFlagManager, CountAggregator countAggregator) {
this.featureFlagManager = featureFlagManager;
this.countAggregator = countAggregator;
}
public void setAppId(String appId, String accessKey) {
this.appId = appId;
this.accessKey = accessKey;
this.countAggregator.incrementCount("set_app_id");
}
public void loadToken(String token) {
this.token = token;
}
}

View File

@ -24,6 +24,7 @@ package com.leanplum.internal;
import android.text.TextUtils;
import java.io.InputStream;
import com.leanplum.Leanplum;
/**
* Represents an argument for a message or action.
@ -46,6 +47,7 @@ public class ActionArg<T> {
arg.name = name;
arg.kind = kind;
arg.defaultValue = defaultValue;
Leanplum.countAggregator().incrementCount("arg_named");
return arg;
}

View File

@ -21,21 +21,17 @@
package com.leanplum.internal;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import com.leanplum.ActionContext;
import com.leanplum.ActionContext.ContextualValues;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumLocalPushListenerService;
import com.leanplum.LocationManager;
import com.leanplum.callbacks.ActionCallback;
import com.leanplum.utils.SharedPreferencesUtil;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -55,13 +51,17 @@ public class ActionManager {
public static final String PUSH_NOTIFICATION_ACTION_NAME = "__Push Notification";
public static final String HELD_BACK_ACTION_NAME = "__held_back";
private static final String LEANPLUM_LOCAL_PUSH_HELPER =
"com.leanplum.internal.LeanplumLocalPushHelper";
private static final String PREFERENCES_NAME = "__leanplum_messaging__";
private static LocationManager locationManager;
private static boolean loggedLocationManagerFailure = false;
public static class MessageMatchResult {
public boolean matchedTrigger;
public boolean matchedUnlessTrigger;
public boolean matchedLimit;
public boolean matchedActivePeriod;
}
public static synchronized ActionManager getInstance() {
@ -71,11 +71,29 @@ public class ActionManager {
return instance;
}
private static boolean loggedLocationManagerFailure = false;
public static synchronized LocationManager getLocationManager() {
if (locationManager != null) {
return locationManager;
}
public static LocationManager getLocationManager() {
if (Util.hasPlayServices()) {
loggedLocationManagerFailure = true;
try {
// Reflection here prevents linker errors
// if Google Play Services is not used in the client app.
locationManager = (LocationManager) Class
.forName("com.leanplum.LocationManagerImplementation")
.getMethod("instance").invoke(null);
return locationManager;
} catch (Throwable t) {
if (!loggedLocationManagerFailure) {
Log.w("Geofencing support requires leanplum-location module and Google Play " +
"Services v8.1 and higher.\n" +
"Add this to your build.gradle file:\n" +
"implementation 'com.google.android.gms:play-services-location:8.3.0+'\n" +
"implementation 'com.leanplum:leanplum-location:+'");
loggedLocationManagerFailure = true;
}
}
}
return null;
}
@ -112,91 +130,20 @@ public class ActionManager {
return false;
}
long eta = System.currentTimeMillis() + ((Number) countdownObj).longValue() * 1000L;
Context context = Leanplum.getContext();
Intent intentAlarm = new Intent(context, LeanplumLocalPushListenerService.class);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(
Context.ALARM_SERVICE);
// If there's already one scheduled before the eta, discard this.
// Otherwise, discard the scheduled one.
SharedPreferences preferences = context.getSharedPreferences(
PREFERENCES_NAME, Context.MODE_PRIVATE);
long existingEta = preferences.getLong(String.format(
Constants.Defaults.LOCAL_NOTIFICATION_KEY, messageId), 0L);
if (existingEta > 0L && existingEta > System.currentTimeMillis()) {
if (existingEta < eta) {
return false;
} else if (existingEta >= eta) {
PendingIntent existingIntent = PendingIntent.getService(
context, messageId.hashCode(), intentAlarm,
PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.cancel(existingIntent);
}
}
// Specify custom data for the notification
Map<String, Serializable> data = actionContext.objectNamed("Advanced options.Data");
if (data != null) {
for (String key : data.keySet()) {
intentAlarm.putExtra(key, data.get(key));
}
}
// Specify open action
String openAction = actionContext.stringNamed(Constants.Values.DEFAULT_PUSH_ACTION);
boolean muteInsideApp = Boolean.TRUE.equals(actionContext.objectNamed(
"Advanced options.Mute inside app"));
if (openAction != null) {
if (muteInsideApp) {
intentAlarm.putExtra(Constants.Keys.PUSH_MESSAGE_ID_MUTE_WITH_ACTION, messageId);
} else {
intentAlarm.putExtra(Constants.Keys.PUSH_MESSAGE_ID_NO_MUTE_WITH_ACTION, messageId);
}
} else {
if (muteInsideApp) {
intentAlarm.putExtra(Constants.Keys.PUSH_MESSAGE_ID_MUTE, messageId);
} else {
intentAlarm.putExtra(Constants.Keys.PUSH_MESSAGE_ID_NO_MUTE, messageId);
}
}
// Message.
String message = actionContext.stringNamed("Message");
intentAlarm.putExtra(Constants.Keys.PUSH_MESSAGE_TEXT,
message != null ? message : Constants.Values.DEFAULT_PUSH_MESSAGE);
// Collapse key.
String collapseKey = actionContext.stringNamed("Android options.Collapse key");
if (collapseKey != null) {
intentAlarm.putExtra("collapseKey", collapseKey);
}
// Delay while idle.
boolean delayWhileIdle = Boolean.TRUE.equals(actionContext.objectNamed(
"Android options.Delay while idle"));
if (delayWhileIdle) {
intentAlarm.putExtra("delayWhileIdle", true);
}
// Schedule notification.
PendingIntent operation = PendingIntent.getService(
context, messageId.hashCode(), intentAlarm,
PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.set(AlarmManager.RTC_WAKEUP, eta, operation);
// Save notification so we can cancel it later.
SharedPreferences.Editor editor = preferences.edit();
editor.putLong(String.format(Constants.Defaults.LOCAL_NOTIFICATION_KEY, messageId), eta);
SharedPreferencesUtil.commitChanges(editor);
Log.i("Scheduled notification");
return true;
try {
return (boolean) Class.forName(LEANPLUM_LOCAL_PUSH_HELPER)
.getDeclaredMethod("scheduleLocalPush", ActionContext.class, String.class,
long.class).invoke(new Object(), actionContext, messageId, eta);
} catch (Throwable throwable) {
return false;
}
} catch (Throwable t) {
Util.handleException(t);
return false;
}
}
});
Leanplum.onAction("__Cancel" + PUSH_NOTIFICATION_ACTION_NAME, new ActionCallback() {
@ -209,24 +156,26 @@ public class ActionManager {
Context context = Leanplum.getContext();
SharedPreferences preferences = context.getSharedPreferences(
PREFERENCES_NAME, Context.MODE_PRIVATE);
String preferencesKey = String.format(Constants.Defaults.LOCAL_NOTIFICATION_KEY, messageId);
String preferencesKey = String.format(Constants.Defaults.LOCAL_NOTIFICATION_KEY,
messageId);
long existingEta = preferences.getLong(preferencesKey, 0L);
SharedPreferences.Editor editor = preferences.edit();
editor.remove(preferencesKey);
SharedPreferencesUtil.commitChanges(editor);
// Cancel notification.
Intent intentAlarm = new Intent(context, LeanplumLocalPushListenerService.class);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent existingIntent = PendingIntent.getService(
context, messageId.hashCode(), intentAlarm, PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.cancel(existingIntent);
boolean didCancel = existingEta > System.currentTimeMillis();
if (didCancel) {
Log.i("Cancelled notification");
try {
Class.forName(LEANPLUM_LOCAL_PUSH_HELPER)
.getDeclaredMethod("cancelLocalPush", Context.class, String.class)
.invoke(new Object(), context, messageId);
boolean didCancel = existingEta > System.currentTimeMillis();
if (didCancel) {
Log.i("Cancelled notification");
}
return didCancel;
} catch (Throwable throwable) {
return false;
}
return didCancel;
} catch (Throwable t) {
Util.handleException(t);
return false;
@ -260,7 +209,7 @@ public class ActionManager {
String.format(Constants.Defaults.MESSAGE_IMPRESSION_OCCURRENCES_KEY, messageId),
JsonConverter.toJson(occurrences));
messageImpressionOccurrences.put(messageId, occurrences);
SharedPreferencesUtil.commitChanges(editor);
SharedPreferencesUtil.commitChanges(editor);
}
public int getMessageTriggerOccurrences(String messageId) {
@ -304,8 +253,8 @@ public class ActionManager {
// 2. Must match at least one trigger.
result.matchedTrigger = matchedTriggers(messageConfig.get("whenTriggers"), when, eventName,
contextualValues);
result.matchedUnlessTrigger = matchedTriggers(messageConfig.get("unlessTriggers"), when, eventName,
contextualValues);
result.matchedUnlessTrigger =
matchedTriggers(messageConfig.get("unlessTriggers"), when, eventName, contextualValues);
if (!result.matchedTrigger && !result.matchedUnlessTrigger) {
return result;
}
@ -317,6 +266,18 @@ public class ActionManager {
limitConfig = CollectionUtil.uncheckedCast(limitConfigObj);
}
result.matchedLimit = matchesLimits(messageId, limitConfig);
// 4. Must be within active period.
Object messageStartTime = messageConfig.get("startTime");
Object messageEndTime = messageConfig.get("endTime");
if (messageStartTime == null || messageEndTime == null) {
result.matchedActivePeriod = true;
} else {
long currentTime = new Date().getTime();
result.matchedActivePeriod = currentTime >= (long) messageStartTime &&
currentTime <= (long) messageEndTime;
}
return result;
}
@ -489,6 +450,7 @@ public class ActionManager {
int occurrences = getMessageTriggerOccurrences(messageId);
occurrences++;
saveMessageTriggerOccurrences(occurrences, messageId);
Leanplum.countAggregator().incrementCount("record_message_trigger");
}
/**

View File

@ -22,7 +22,6 @@
package com.leanplum.internal;
import org.mozilla.gecko.thirdparty_unused.BuildConfig;
/**
* Leanplum constants.
@ -39,7 +38,7 @@ public class Constants {
public static int NETWORK_TIMEOUT_SECONDS_FOR_DOWNLOADS = 10;
static final String LEANPLUM_PACKAGE_IDENTIFIER = "s"; //TODO investigate what this should be
public static String LEANPLUM_VERSION = "3.0.2";
public static String LEANPLUM_VERSION = "4.2.7";
public static String CLIENT = "android";
static final String INVALID_MAC_ADDRESS = "02:00:00:00:00:00";
@ -146,12 +145,15 @@ public class Constants {
public static final String IAP_CURRENCY_CODE = "currencyCode";
public static final String IAP_ITEM = "item";
public static final String INCLUDE_DEFAULTS = "includeDefaults";
public static final String INCLUDE_VARIANT_DEBUG_INFO = "includeVariantDebugInfo";
public static final String INCLUDE_MESSAGE_ID = "includeMessageId";
public static final String INFO = "info";
public static final String INSTALL_DATE = "installDate";
public static final String KINDS = "kinds";
public static final String LIMIT_TRACKING = "limitTracking";
public static final String MESSAGE = "message";
public static final String NAME = "name";
public static final String COUNT = "count";
public static final String MESSAGE_ID = "messageId";
public static final String NEW_USER_ID = "newUserId";
public static final String INBOX_MESSAGE_ID = "newsfeedMessageId";
@ -202,10 +204,13 @@ public class Constants {
public static final String REGION = "region";
public static final String REGION_STATE = "regionState";
public static final String REGIONS = "regions";
public static final String VARIANT_DEBUG_INFO = "variantDebugInfo";
public static final String SIZE = "size";
public static final String SUBTITLE = "Subtitle";
public static final String SYNC_INBOX = "syncNewsfeed";
public static final String LOGGING_ENABLED = "loggingEnabled";
public static final String ENABLED_COUNTERS = "enabledSdkCounters";
public static final String ENABLED_FEATURE_FLAGS = "enabledFeatureFlags";
public static final String TIMEZONE = "timezone";
public static final String TIMEZONE_OFFSET_SECONDS = "timezoneOffsetSeconds";
public static final String TITLE = "Title";
@ -250,6 +255,7 @@ public class Constants {
public static final String DEFAULT_PUSH_MESSAGE = "Push message goes here.";
public static final String SDK_LOG = "sdkLog";
public static final String SDK_ERROR = "sdkError";
public static final String SDK_COUNT = "sdkCount";
public static final String FILE_PREFIX = "__file__";
}

View File

@ -0,0 +1,71 @@
package com.leanplum.internal;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import java.util.HashSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class CountAggregator {
private Set<String> enabledCounters = new HashSet<>();
private final Map<String, Integer> counts = new HashMap<>();
public void setEnabledCounters(Set<String> enabledCounters) {
this.enabledCounters = enabledCounters;
}
public void incrementCount(@NonNull String name) {
incrementCount(name, 1);
}
public void incrementCount(@NonNull String name, int incrementCount) {
if (enabledCounters.contains(name)) {
Integer count = 0;
if (counts.containsKey(name)) {
count = counts.get(name);
}
count = count + incrementCount;
counts.put(name, count);
}
}
@VisibleForTesting
public Map<String, Integer> getAndClearCounts() {
Map<String, Integer> previousCounts = new HashMap<>();
previousCounts.putAll(counts);
counts.clear();
return previousCounts;
}
@VisibleForTesting
public Map<String, Object> makeParams(@NonNull String name, int count) {
Map<String, Object> params = new HashMap<>();
params.put(Constants.Params.TYPE, Constants.Values.SDK_COUNT);
params.put(Constants.Params.NAME, name);
params.put(Constants.Params.COUNT, count);
return params;
}
public void sendAllCounts() {
Map<String, Integer> counts = getAndClearCounts();
for(Map.Entry<String, Integer> entry : counts.entrySet()) {
String name = entry.getKey();
Integer count = entry.getValue();
Map<String, Object> params = makeParams(name, count);
try {
RequestOld.post(Constants.Methods.LOG, params).sendEventually();
} catch (Throwable t) {
android.util.Log.e("Leanplum", "Unable to send count.", t);
}
}
}
public Map<String, Integer> getCounts() {
return counts;
}
}

View File

@ -0,0 +1,30 @@
package com.leanplum.internal;
import android.support.annotation.VisibleForTesting;
import com.leanplum.Leanplum;
import java.util.HashSet;
import java.util.Set;
public class FeatureFlagManager {
public static final FeatureFlagManager INSTANCE = new FeatureFlagManager();
public static final String FEATURE_FLAG_REQUEST_REFACTOR = "request_refactor";
private Set<String> enabledFeatureFlags = new HashSet<>();
@VisibleForTesting
FeatureFlagManager() {
super();
}
public void setEnabledFeatureFlags(Set<String> enabledFeatureFlags) {
this.enabledFeatureFlags = enabledFeatureFlags;
}
public Boolean isFeatureFlagEnabled(String featureFlagName) {
Leanplum.countAggregator().incrementCount("is_feature_flag_enabled");
return this.enabledFeatureFlags.contains(featureFlagName);
}
}

View File

@ -108,8 +108,8 @@ public class FileManager {
if (!FileManager.fileExistsAtPath(realPath)) {
realPath = FileManager.fileRelativeToDocuments(stringValue);
if (!FileManager.fileExistsAtPath(realPath)) {
Request downloadRequest = Request.get(Constants.Methods.DOWNLOAD_FILE, null);
downloadRequest.onResponse(new Request.ResponseCallback() {
RequestOld downloadRequest = RequestOld.get(Constants.Methods.DOWNLOAD_FILE, null);
downloadRequest.onResponse(new RequestOld.ResponseCallback() {
@Override
public void response(JSONObject response) {
if (onComplete != null) {
@ -117,7 +117,7 @@ public class FileManager {
}
}
});
downloadRequest.onError(new Request.ErrorCallback() {
downloadRequest.onError(new RequestOld.ErrorCallback() {
@Override
public void error(Exception e) {
if (onComplete != null) {
@ -130,6 +130,7 @@ public class FileManager {
}
}
}
Leanplum.countAggregator().incrementCount("maybe_download_file");
return DownloadFileResult.NONE;
}

View File

@ -1,5 +1,3 @@
package com.leanplum.internal;
/*
* Copyright 2017, Leanplum, Inc. All rights reserved.
*
@ -20,10 +18,13 @@ package com.leanplum.internal;
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.internal;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import com.leanplum.Leanplum;
import org.json.JSONObject;
import java.util.HashMap;
@ -36,117 +37,120 @@ import java.util.Map;
* @author Anna Orlova
*/
class LeanplumEventCallbackManager {
// Event callbacks map.
private final Map<Request, LeanplumEventCallbacks> eventCallbacks = new HashMap<>();
// Event callbacks map.
private final Map<RequestOld, LeanplumEventCallbacks> eventCallbacks = new HashMap<>();
/**
* Add callbacks to the event callbacks Map.
*
* @param request Event.
* @param responseCallback Response callback.
* @param errorCallback Error callback.
*/
void addCallbacks(Request request, Request.ResponseCallback responseCallback,
Request.ErrorCallback errorCallback) {
if (request == null) {
return;
}
if (responseCallback == null && errorCallback == null) {
return;
}
eventCallbacks.put(request, new LeanplumEventCallbacks(responseCallback, errorCallback));
/**
* Add callbacks to the event callbacks Map.
*
* @param request Event.
* @param responseCallback Response callback.
* @param errorCallback Error callback.
*/
void addCallbacks(RequestOld request, RequestOld.ResponseCallback responseCallback,
RequestOld.ErrorCallback errorCallback) {
if (request == null) {
return;
}
/**
* Invoke potential error callbacks for all events with database index less than a count of events
* that we got from database.
*
* @param error Exception.
* @param countOfEvents Count of events that we got from database.
*/
void invokeAllCallbacksWithError(@NonNull final Exception error, int countOfEvents) {
if (eventCallbacks.size() == 0) {
return;
}
Iterator<Map.Entry<Request, LeanplumEventCallbacks>> iterator =
eventCallbacks.entrySet().iterator();
// Loop over all callbacks.
for (; iterator.hasNext(); ) {
final Map.Entry<Request, LeanplumEventCallbacks> entry = iterator.next();
if (entry.getKey() == null) {
continue;
}
if (entry.getKey().getDataBaseIndex() >= countOfEvents) {
entry.getKey().setDataBaseIndex(entry.getKey().getDataBaseIndex() - countOfEvents);
} else {
if (entry.getValue() != null && entry.getValue().errorCallback != null) {
// Start callback asynchronously, to avoid creation of new Request object from the same
// thread.
Util.executeAsyncTask(false, new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
entry.getValue().errorCallback.error(error);
return null;
}
});
}
iterator.remove();
}
}
if (responseCallback == null && errorCallback == null) {
return;
}
/**
* Invoke potential response callbacks for all events with database index less than a count of
* events that we got from database.
*
* @param responseBody JSONObject withs server response.
* @param countOfEvents Count of events that we got from database.
*/
void invokeAllCallbacksForResponse(@NonNull final JSONObject responseBody, int countOfEvents) {
if (eventCallbacks.size() == 0) {
return;
}
eventCallbacks.put(request, new LeanplumEventCallbacks(responseCallback, errorCallback));
Leanplum.countAggregator().incrementCount("add_event_callback_at");
}
Iterator<Map.Entry<Request, LeanplumEventCallbacks>> iterator =
eventCallbacks.entrySet().iterator();
// Loop over all callbacks.
for (; iterator.hasNext(); ) {
final Map.Entry<Request, LeanplumEventCallbacks> entry = iterator.next();
if (entry.getKey() == null) {
continue;
}
if (entry.getKey().getDataBaseIndex() >= countOfEvents) {
entry.getKey().setDataBaseIndex(entry.getKey().getDataBaseIndex() - countOfEvents);
} else {
if (entry.getValue() != null && entry.getValue().responseCallback != null) {
// Start callback asynchronously, to avoid creation of new Request object from the same
// thread.
Util.executeAsyncTask(false, new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
entry.getValue().responseCallback.response(Request.getResponseAt(responseBody,
(int) entry.getKey().getDataBaseIndex()));
return null;
}
});
}
iterator.remove();
}
}
/**
* Invoke potential error callbacks for all events with database index less than a count of events
* that we got from database.
*
* @param error Exception.
* @param countOfEvents Count of events that we got from database.
*/
void invokeAllCallbacksWithError(@NonNull final Exception error, int countOfEvents) {
if (eventCallbacks.size() == 0) {
return;
}
private static class LeanplumEventCallbacks {
private Request.ResponseCallback responseCallback;
private Request.ErrorCallback errorCallback;
LeanplumEventCallbacks(Request.ResponseCallback responseCallback, Request.ErrorCallback
errorCallback) {
this.responseCallback = responseCallback;
this.errorCallback = errorCallback;
Iterator<Map.Entry<RequestOld, LeanplumEventCallbacks>> iterator =
eventCallbacks.entrySet().iterator();
// Loop over all callbacks.
for (; iterator.hasNext(); ) {
final Map.Entry<RequestOld, LeanplumEventCallbacks> entry = iterator.next();
if (entry.getKey() == null) {
continue;
}
if (entry.getKey().getDataBaseIndex() >= countOfEvents) {
entry.getKey().setDataBaseIndex(entry.getKey().getDataBaseIndex() - countOfEvents);
} else {
if (entry.getValue() != null && entry.getValue().errorCallback != null) {
// Start callback asynchronously, to avoid creation of new RequestOld object from the same
// thread.
Util.executeAsyncTask(false, new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
entry.getValue().errorCallback.error(error);
return null;
}
});
}
iterator.remove();
}
}
Leanplum.countAggregator().incrementCount("invoke_error_callbacks_on_responses");
}
/**
* Invoke potential response callbacks for all events with database index less than a count of
* events that we got from database.
*
* @param responseBody JSONObject withs server response.
* @param countOfEvents Count of events that we got from database.
*/
void invokeAllCallbacksForResponse(@NonNull final JSONObject responseBody, int countOfEvents) {
if (eventCallbacks.size() == 0) {
return;
}
Iterator<Map.Entry<RequestOld, LeanplumEventCallbacks>> iterator =
eventCallbacks.entrySet().iterator();
// Loop over all callbacks.
for (; iterator.hasNext(); ) {
final Map.Entry<RequestOld, LeanplumEventCallbacks> entry = iterator.next();
if (entry.getKey() == null) {
continue;
}
if (entry.getKey().getDataBaseIndex() >= countOfEvents) {
entry.getKey().setDataBaseIndex(entry.getKey().getDataBaseIndex() - countOfEvents);
} else {
if (entry.getValue() != null && entry.getValue().responseCallback != null) {
// Start callback asynchronously, to avoid creation of new RequestOld object from the same
// thread.
Util.executeAsyncTask(false, new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
entry.getValue().responseCallback.response(RequestOld.getResponseAt(responseBody,
(int) entry.getKey().getDataBaseIndex()));
return null;
}
});
}
iterator.remove();
}
}
Leanplum.countAggregator().incrementCount("invoke_success_callbacks_on_responses");
}
private static class LeanplumEventCallbacks {
private RequestOld.ResponseCallback responseCallback;
private RequestOld.ErrorCallback errorCallback;
LeanplumEventCallbacks(RequestOld.ResponseCallback responseCallback, RequestOld.ErrorCallback
errorCallback) {
this.responseCallback = responseCallback;
this.errorCallback = errorCallback;
}
}
}

View File

@ -1,5 +1,3 @@
package com.leanplum.internal;
/*
* Copyright 2017, Leanplum, Inc. All rights reserved.
*
@ -21,6 +19,8 @@ package com.leanplum.internal;
* under the License.
*/
package com.leanplum.internal;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
@ -48,212 +48,215 @@ import java.util.UUID;
* @author Anna Orlova
*/
public class LeanplumEventDataManager {
private static final String DATABASE_NAME = "__leanplum.db";
private static final int DATABASE_VERSION = 1;
private static final String EVENT_TABLE_NAME = "event";
private static final String COLUMN_DATA = "data";
private static final String KEY_ROWID = "rowid";
private static final String DATABASE_NAME = "__leanplum.db";
private static final int DATABASE_VERSION = 1;
private static final String EVENT_TABLE_NAME = "event";
private static final String COLUMN_DATA = "data";
private static final String KEY_ROWID = "rowid";
private static SQLiteDatabase database;
private static LeanplumDataBaseManager databaseManager;
private static ContentValues contentValues = new ContentValues();
private static SQLiteDatabase database;
private static LeanplumDataBaseManager databaseManager;
private static ContentValues contentValues = new ContentValues();
static boolean willSendErrorLog = false;
static boolean willSendErrorLog = false;
/**
* Creates connection to database, if database is not present, it will automatically create it.
*
* @param context Current context.
*/
public static void init(Context context) {
if (database != null) {
Log.e("Database is already initialized.");
return;
}
// Create database if needed.
try {
if (databaseManager == null) {
databaseManager = new LeanplumDataBaseManager(context);
}
database = databaseManager.getWritableDatabase();
} catch (Throwable t) {
handleSQLiteError("Cannot create database.", t);
}
}
/**
* Inserts event to event table.
*
* @param event String with json of event.
*/
static void insertEvent(String event) {
if (database == null) {
return;
}
contentValues.put(COLUMN_DATA, event);
try {
database.insert(EVENT_TABLE_NAME, null, contentValues);
willSendErrorLog = false;
} catch (Throwable t) {
handleSQLiteError("Unable to insert event to database.", t);
}
contentValues.clear();
Leanplum.countAggregator().incrementCount("add_event");
}
/**
* Gets first count events from event table.
*
* @param count Number of events.
* @return List of events.
*/
static List<Map<String, Object>> getEvents(int count) {
List<Map<String, Object>> events = new ArrayList<>();
if (database == null) {
return events;
}
Cursor cursor = null;
try {
cursor = database.query(EVENT_TABLE_NAME, new String[] {COLUMN_DATA}, null, null, null,
null, KEY_ROWID + " ASC", "" + count);
willSendErrorLog = false;
while (cursor.moveToNext()) {
Map<String, Object> requestArgs = JsonConverter.mapFromJson(new JSONObject(
cursor.getString(cursor.getColumnIndex(COLUMN_DATA))));
events.add(requestArgs);
}
} catch (Throwable t) {
handleSQLiteError("Unable to get events from the table.", t);
} finally {
if (cursor != null) {
cursor.close();
}
}
Leanplum.countAggregator().incrementCount("events_with_limit");
return events;
}
/**
* Deletes first count elements from event table.
*
* @param count Number of event that need to be deleted.
*/
static void deleteEvents(int count) {
if (database == null) {
return;
}
try {
database.delete(EVENT_TABLE_NAME, KEY_ROWID + " in (select " + KEY_ROWID + " from " +
EVENT_TABLE_NAME + " ORDER BY " + KEY_ROWID + " ASC LIMIT " + count + ")", null);
willSendErrorLog = false;
} catch (Throwable t) {
handleSQLiteError("Unable to delete events from the table.", t);
}
Leanplum.countAggregator().incrementCount("delete_events_with_limit");
}
/**
* Gets number of rows in the event table.
*
* @return Number of rows in the event table.
*/
static long getEventsCount() {
long count = 0;
if (database == null) {
return count;
}
try {
count = DatabaseUtils.queryNumEntries(database, EVENT_TABLE_NAME);
willSendErrorLog = false;
} catch (Throwable t) {
handleSQLiteError("Unable to get a number of rows in the table.", t);
}
return count;
}
/**
* Helper function that logs and sends errors to the server.
*/
private static void handleSQLiteError(String log, Throwable t) {
Log.e(log, t);
// Send error log. Using willSendErrorLog to prevent infinte loop.
if (!willSendErrorLog) {
willSendErrorLog = true;
Util.handleException(t);
}
}
private static class LeanplumDataBaseManager extends SQLiteOpenHelper {
LeanplumDataBaseManager(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// Create event table.
db.execSQL("CREATE TABLE IF NOT EXISTS " + EVENT_TABLE_NAME + "(" + COLUMN_DATA +
" TEXT)");
// Migrate old data from shared preferences.
try {
migrateFromSharedPreferences(db);
} catch (Throwable t) {
Log.e("Cannot move old data from shared preferences to SQLite table.", t);
Util.handleException(t);
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// No used for now.
}
/**
* Creates connection to database, if database is not present, it will automatically create it.
*
* @param context Current context.
* Migrate data from shared preferences to SQLite.
*/
public static void init(Context context) {
if (database != null) {
Log.e("Database is already initialized.");
return;
private static void migrateFromSharedPreferences(SQLiteDatabase db) {
synchronized (RequestOld.class) {
Context context = Leanplum.getContext();
SharedPreferences preferences = context.getSharedPreferences(
RequestOld.LEANPLUM, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
int count = preferences.getInt(Constants.Defaults.COUNT_KEY, 0);
if (count == 0) {
return;
}
// Create database if needed.
List<Map<String, Object>> requestData = new ArrayList<>();
for (int i = 0; i < count; i++) {
String itemKey = String.format(Locale.US, Constants.Defaults.ITEM_KEY, i);
Map<String, Object> requestArgs;
try {
requestArgs = JsonConverter.mapFromJson(new JSONObject(
preferences.getString(itemKey, "{}")));
requestData.add(requestArgs);
} catch (JSONException e) {
e.printStackTrace();
}
editor.remove(itemKey);
}
editor.remove(Constants.Defaults.COUNT_KEY);
try {
if (databaseManager == null) {
databaseManager = new LeanplumDataBaseManager(context);
}
database = databaseManager.getWritableDatabase();
String uuid = preferences.getString(Constants.Defaults.UUID_KEY, null);
if (uuid == null || count % RequestOld.MAX_EVENTS_PER_API_CALL == 0) {
uuid = UUID.randomUUID().toString();
editor.putString(Constants.Defaults.UUID_KEY, uuid);
}
for (Map<String, Object> event : requestData) {
event.put(RequestOld.UUID_KEY, uuid);
contentValues.put(COLUMN_DATA, JsonConverter.toJson(event));
db.insert(EVENT_TABLE_NAME, null, contentValues);
contentValues.clear();
}
SharedPreferencesUtil.commitChanges(editor);
} catch (Throwable t) {
handleSQLiteError("Cannot create database.", t);
Log.e("Failed on migration data from shared preferences.", t);
Util.handleException(t);
}
}
}
/**
* Inserts event to event table.
*
* @param event String with json of event.
*/
static void insertEvent(String event) {
if (database == null) {
return;
}
contentValues.put(COLUMN_DATA, event);
try {
database.insert(EVENT_TABLE_NAME, null, contentValues);
willSendErrorLog = false;
} catch (Throwable t) {
handleSQLiteError("Unable to insert event to database.", t);
}
contentValues.clear();
}
/**
* Gets first count events from event table.
*
* @param count Number of events.
* @return List of events.
*/
static List<Map<String, Object>> getEvents(int count) {
List<Map<String, Object>> events = new ArrayList<>();
if (database == null) {
return events;
}
Cursor cursor = null;
try {
cursor = database.query(EVENT_TABLE_NAME, new String[]{COLUMN_DATA}, null, null, null,
null, KEY_ROWID + " ASC", "" + count);
willSendErrorLog = false;
while (cursor.moveToNext()) {
Map<String, Object> requestArgs = JsonConverter.mapFromJson(new JSONObject(
cursor.getString(cursor.getColumnIndex(COLUMN_DATA))));
events.add(requestArgs);
}
} catch (Throwable t) {
handleSQLiteError("Unable to get events from the table.", t);
} finally {
if (cursor != null) {
cursor.close();
}
}
return events;
}
/**
* Deletes first count elements from event table.
*
* @param count Number of event that need to be deleted.
*/
static void deleteEvents(int count) {
if (database == null) {
return;
}
try {
database.delete(EVENT_TABLE_NAME, KEY_ROWID + " in (select " + KEY_ROWID + " from " +
EVENT_TABLE_NAME + " ORDER BY " + KEY_ROWID + " ASC LIMIT " + count + ")", null);
willSendErrorLog = false;
} catch (Throwable t) {
handleSQLiteError("Unable to delete events from the table.", t);
}
}
/**
* Gets number of rows in the event table.
*
* @return Number of rows in the event table.
*/
static long getEventsCount() {
long count = 0;
if (database == null) {
return count;
}
try {
count = DatabaseUtils.queryNumEntries(database, EVENT_TABLE_NAME);
willSendErrorLog = false;
} catch (Throwable t) {
handleSQLiteError("Unable to get a number of rows in the table.", t);
}
return count;
}
/**
* Helper function that logs and sends errors to the server.
*/
private static void handleSQLiteError(String log, Throwable t) {
Log.e(log, t);
// Send error log. Using willSendErrorLog to prevent infinte loop.
if (!willSendErrorLog) {
willSendErrorLog = true;
Util.handleException(t);
}
}
private static class LeanplumDataBaseManager extends SQLiteOpenHelper {
LeanplumDataBaseManager(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
// Create event table.
db.execSQL("CREATE TABLE IF NOT EXISTS " + EVENT_TABLE_NAME + "(" + COLUMN_DATA +
" TEXT)");
// Migrate old data from shared preferences.
try {
migrateFromSharedPreferences(db);
} catch (Throwable t) {
Log.e("Cannot move old data from shared preferences to SQLite table.", t);
Util.handleException(t);
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// No used for now.
}
/**
* Migrate data from shared preferences to SQLite.
*/
private static void migrateFromSharedPreferences(SQLiteDatabase db) {
synchronized (Request.class) {
Context context = Leanplum.getContext();
SharedPreferences preferences = context.getSharedPreferences(
Request.LEANPLUM, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
int count = preferences.getInt(Constants.Defaults.COUNT_KEY, 0);
if (count == 0) {
return;
}
List<Map<String, Object>> requestData = new ArrayList<>();
for (int i = 0; i < count; i++) {
String itemKey = String.format(Locale.US, Constants.Defaults.ITEM_KEY, i);
Map<String, Object> requestArgs;
try {
requestArgs = JsonConverter.mapFromJson(new JSONObject(
preferences.getString(itemKey, "{}")));
requestData.add(requestArgs);
} catch (JSONException e) {
e.printStackTrace();
}
editor.remove(itemKey);
}
editor.remove(Constants.Defaults.COUNT_KEY);
try {
String uuid = preferences.getString(Constants.Defaults.UUID_KEY, null);
if (uuid == null || count % Request.MAX_EVENTS_PER_API_CALL == 0) {
uuid = UUID.randomUUID().toString();
editor.putString(Constants.Defaults.UUID_KEY, uuid);
}
for (Map<String, Object> event : requestData) {
event.put(Request.UUID_KEY, uuid);
contentValues.put(COLUMN_DATA, JsonConverter.toJson(event));
db.insert(EVENT_TABLE_NAME, null, contentValues);
contentValues.clear();
}
SharedPreferencesUtil.commitChanges(editor);
} catch (Throwable t) {
Log.e("Failed on migration data from shared preferences.", t);
Util.handleException(t);
}
}
}
}
}
}
}

View File

@ -73,6 +73,7 @@ public class LeanplumInternal {
private static final Queue<Map<String, ?>> userAttributeChanges = new ConcurrentLinkedQueue<>();
private static final ArrayList<Runnable> startIssuedHandlers = new ArrayList<>();
private static boolean isScreenTrackingEnabled = false;
private static boolean isVariantDebugInfoEnabled = false;
private static void onHasStartedAndRegisteredAsDeveloperAndFinishedSyncing() {
if (!hasStartedAndRegisteredAsDeveloper) {
@ -183,6 +184,7 @@ public class LeanplumInternal {
result.matchedTrigger |= conditionResult.matchedTrigger;
result.matchedUnlessTrigger |= conditionResult.matchedUnlessTrigger;
result.matchedLimit |= conditionResult.matchedLimit;
result.matchedActivePeriod |= conditionResult.matchedActivePeriod;
}
// Make sure we cancel before matching in case the criteria overlap.
@ -205,6 +207,11 @@ public class LeanplumInternal {
});
}
// Make sure message is within the active period.
if(!result.matchedActivePeriod){
continue;
}
if (result.matchedTrigger) {
ActionManager.getInstance().recordMessageTrigger(internalMessageId);
@ -252,7 +259,7 @@ public class LeanplumInternal {
@Override
public void variablesChanged() {
try {
ActionManager.getInstance().recordMessageImpression(actionContext.getMessageId());
Leanplum.triggerMessageDisplayed(actionContext);
} catch (Throwable t) {
Util.handleException(t);
}
@ -328,7 +335,7 @@ public class LeanplumInternal {
*/
private static void trackInternal(String event, Map<String, ?> params,
Map<String, Object> requestArgs) {
Request.post(Constants.Methods.TRACK, requestArgs).send();
RequestOld.post(Constants.Methods.TRACK, requestArgs).send();
String eventTriggerName = event;
String messageId = null;
@ -426,14 +433,14 @@ public class LeanplumInternal {
} catch (Throwable ignored) {
}
}
Request req = Request.post(Constants.Methods.SET_USER_ATTRIBUTES, params);
req.onResponse(new Request.ResponseCallback() {
RequestOld req = RequestOld.post(Constants.Methods.SET_USER_ATTRIBUTES, params);
req.onResponse(new RequestOld.ResponseCallback() {
@Override
public void response(JSONObject response) {
callback.response(true);
}
});
req.onError(new Request.ErrorCallback() {
req.onError(new RequestOld.ErrorCallback() {
@Override
public void error(Exception e) {
callback.response(false);
@ -667,6 +674,14 @@ public class LeanplumInternal {
return isScreenTrackingEnabled;
}
public static boolean getIsVariantDebugInfoEnabled() {
return isVariantDebugInfoEnabled;
}
public static void setIsVariantDebugInfoEnabled(boolean isVariantDebugInfoEnabled) {
LeanplumInternal.isVariantDebugInfoEnabled = isVariantDebugInfoEnabled;
}
public static void enableAutomaticScreenTracking() {
isScreenTrackingEnabled = true;
}

View File

@ -0,0 +1,159 @@
/*
* Copyright 2018, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.internal;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import com.leanplum.ActionContext;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumLocalPushListenerService;
import com.leanplum.utils.SharedPreferencesUtil;
import java.io.Serializable;
import java.util.Map;
/**
* Leanplum local push notification helper class.
*
* @author Anna Orlova
*/
class LeanplumLocalPushHelper {
private static final String PREFERENCES_NAME = "__leanplum_messaging__";
/**
* Schedule local push notification. This method will call by reflection from AndroidSDKCore.
*
* @param actionContext Action Context.
* @param messageId String message id for local push notification.
* @param eta Eta for local push notification.
* @return True if notification was scheduled.
*/
static boolean scheduleLocalPush(ActionContext actionContext, String messageId, long eta) {
try {
Context context = Leanplum.getContext();
Intent intentAlarm = new Intent(context, LeanplumLocalPushListenerService.class);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(
Context.ALARM_SERVICE);
// If there's already one scheduled before the eta, discard this.
// Otherwise, discard the scheduled one.
SharedPreferences preferences = context.getSharedPreferences(
PREFERENCES_NAME, Context.MODE_PRIVATE);
long existingEta = preferences.getLong(String.format(
Constants.Defaults.LOCAL_NOTIFICATION_KEY, messageId), 0L);
if (existingEta > 0L && existingEta > System.currentTimeMillis()) {
if (existingEta < eta) {
return false;
} else if (existingEta >= eta) {
PendingIntent existingIntent = PendingIntent.getService(
context, messageId.hashCode(), intentAlarm,
PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.cancel(existingIntent);
}
}
// Specify custom data for the notification
Map<String, Serializable> data = actionContext.objectNamed("Advanced options.Data");
if (data != null) {
for (String key : data.keySet()) {
intentAlarm.putExtra(key, data.get(key));
}
}
// Specify open action
String openAction = actionContext.stringNamed(Constants.Values.DEFAULT_PUSH_ACTION);
boolean muteInsideApp = Boolean.TRUE.equals(actionContext.objectNamed(
"Advanced options.Mute inside app"));
if (openAction != null) {
if (muteInsideApp) {
intentAlarm.putExtra(Constants.Keys.PUSH_MESSAGE_ID_MUTE_WITH_ACTION, messageId);
} else {
intentAlarm.putExtra(Constants.Keys.PUSH_MESSAGE_ID_NO_MUTE_WITH_ACTION, messageId);
}
} else {
if (muteInsideApp) {
intentAlarm.putExtra(Constants.Keys.PUSH_MESSAGE_ID_MUTE, messageId);
} else {
intentAlarm.putExtra(Constants.Keys.PUSH_MESSAGE_ID_NO_MUTE, messageId);
}
}
// Message.
String message = actionContext.stringNamed("Message");
intentAlarm.putExtra(Constants.Keys.PUSH_MESSAGE_TEXT,
message != null ? message : Constants.Values.DEFAULT_PUSH_MESSAGE);
// Collapse key.
String collapseKey = actionContext.stringNamed("Android options.Collapse key");
if (collapseKey != null) {
intentAlarm.putExtra("collapseKey", collapseKey);
}
// Delay while idle.
boolean delayWhileIdle = Boolean.TRUE.equals(actionContext.objectNamed(
"Android options.Delay while idle"));
if (delayWhileIdle) {
intentAlarm.putExtra("delayWhileIdle", true);
}
// Schedule notification.
PendingIntent operation = PendingIntent.getService(
context, messageId.hashCode(), intentAlarm,
PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.set(AlarmManager.RTC_WAKEUP, eta, operation);
// Save notification so we can cancel it later.
SharedPreferences.Editor editor = preferences.edit();
editor.putLong(String.format(Constants.Defaults.LOCAL_NOTIFICATION_KEY, messageId), eta);
SharedPreferencesUtil.commitChanges(editor);
Log.i("Scheduled notification.");
return true;
} catch (Throwable t) {
Util.handleException(t);
return false;
}
}
/**
* Cancel local push notification. This method will call by reflection from AndroidSDKCore.
*
* @param context The application context.
* @param messageId Message id of notification that should be canceled.
*/
static void cancelLocalPush(Context context, String messageId) {
try {
Intent intentAlarm = new Intent(context, LeanplumLocalPushListenerService.class);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent existingIntent = PendingIntent.getService(
context, messageId.hashCode(), intentAlarm, PendingIntent.FLAG_UPDATE_CURRENT);
if (alarmManager != null && existingIntent != null) {
alarmManager.cancel(existingIntent);
}
} catch (Throwable ignored) {
}
}
}

View File

@ -1,160 +0,0 @@
/*
* Copyright 2017, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.internal;
/**
* LeanplumManifestParser class for get AndroidManifest.xml. http://stackoverflow.com/questions/2097813/how-to-parse-the-androidmanifest-xml-file-inside-an-apk-package
*
* @author Anna Orlova
*/
class LeanplumManifestParser {
// XML tags and attributes:
// Every XML start and end tag consists of 6 32 bit words:
// 0th word: 02011000 for START_TAG and 03011000 for END_TAG
// 1st word: a flag?, like 38000000
// 2nd word: Line of where this tag appeared in the original source file
// 3rd word: FFFFFFFF ??
// 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS
// 5th word: StringIndex of Element Name
// (Note: 01011000 in 0th word means end of XML document, END_DOC_TAG).
// Start tags (not end tags) contain 3 more words:
// 6th word: 14001400 meaning??
// 7th word: Number of Attributes that follow this tag(follow word 8th)
// 8th word: 00000000 meaning??
// Attributes consist of 5 words:
// 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF
// 1st word: StringIndex of Attribute Name
// 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId used
// 3rd word: Flags?
// 4th word: str ind of attr value again, or ResourceId of value.
// END_DOC_TAG = 0x00100101;
private static final int START_TAG = 0x00100102;
private static final int END_TAG = 0x00100103;
private static final String SPACES = " ";
/**
* Parse the 'compressed' binary form of Android XML docs such as for AndroidManifest.xml in .apk
* files.
*
* @param xml byte array of AndroidManifest.xml.
* @return String with data of AndroidManifest.xml.
*/
static String decompressXml(byte[] xml) {
String out = "";
// Compressed XML file/bytes starts with 24x bytes of data,
// 9 32 bit words in little endian order (LSB first):
// 0th word is 03 00 08 00
// 3rd word SEEMS TO BE: Offset at then of StringTable
// 4th word is: Number of strings in string table
// WARNING: Sometime I indiscriminately display or refer to word in
// little endian storage format, or in integer format (ie MSB first).
int numbStrings = littleEndianValue(xml, 4 * 4);
// StringIndexTable starts at offset 24x, an array of 32 bit LE offsets
// of the length/string data in the StringTable.
int sitOff = 0x24; // Offset of start of StringIndexTable.
// StringTable, each string is represented with a 16 bit little endian
// character count, followed by that number of 16 bit (LE) (Unicode) chars.
int stOff = sitOff + numbStrings * 4; // StringTable follows StrIndexTable.
// Step through the XML tree element tags and attributes.
int off = scanForFirstStartTag(xml);
int indent = 0;
while (off < xml.length) {
int tag0 = littleEndianValue(xml, off);
int nameSi = littleEndianValue(xml, off + 5 * 4);
if (tag0 == START_TAG) {
int numbAttrs = littleEndianValue(xml, off + 7 * 4); // Number of Attributes to follow.
off += 9 * 4; // Skip over 6+3 words of START_TAG data
String name = compXmlString(xml, sitOff, stOff, nameSi);
// Look for the Attributes
StringBuilder sb = new StringBuilder();
for (int ii = 0; ii < numbAttrs; ii++) {
int attrNameSi = littleEndianValue(xml, off + 4); // AttrName String Index.
int attrValueSi = littleEndianValue(xml, off + 2 * 4); // AttrValue Str Ind, or FFFFFFFF.
int attrResId = littleEndianValue(xml, off + 4 * 4); // AttrValue ResourceId or dup.
// AttrValue StrInd.
off += 5 * 4; // Skip over the 5 words of an attribute.
String attrName = compXmlString(xml, sitOff, stOff, attrNameSi);
String attrValue = attrValueSi != -1
? compXmlString(xml, sitOff, stOff, attrValueSi)
: "resourceID 0x" + Integer.toHexString(attrResId);
sb.append(" ").append(attrName).append("=\"").append(attrValue).append("\"");
}
out += SPACES.substring(0, Math.min(indent * 2, SPACES.length())) + "<" + name + sb + ">";
indent++;
} else if (tag0 == END_TAG) {
indent--;
off += 6 * 4; // Skip over 6 words of END_TAG data
String name = compXmlString(xml, sitOff, stOff, nameSi);
out += SPACES.substring(0, Math.min(indent * 2, SPACES.length())) + "</" + name + ">";
} else {
break;
}
}
return out;
}
private static String compXmlString(byte[] xml, int sitOff, int stOff, int strInd) {
if (strInd < 0) return null;
int strOff = stOff + littleEndianValue(xml, sitOff + strInd * 4);
return compXmlStringAt(xml, strOff);
}
/**
* @return Return the string stored in StringTable format at offset strOff. This offset points to
* the 16 bit string length, which is followed by that number of 16 bit (Unicode) chars.
*/
private static String compXmlStringAt(byte[] arr, int strOff) {
int strLen = arr[strOff + 1] << 8 & 0xff00 | arr[strOff] & 0xff;
byte[] chars = new byte[strLen];
for (int ii = 0; ii < strLen; ii++) {
chars[ii] = arr[strOff + 2 + ii * 2];
}
return new String(chars); // Hack, just use 8 byte chars.
}
/**
* @return Return value of a Little Endian 32 bit word from the byte array at offset off.
*/
private static int littleEndianValue(byte[] arr, int off) {
return arr[off + 3] << 24 & 0xff000000 | arr[off + 2] << 16 & 0xff0000
| arr[off + 1] << 8 & 0xff00 | arr[off] & 0xFF;
}
private static int scanForFirstStartTag(byte[] xml) {
// XMLTags, The XML tag tree starts after some unknown content after the
// StringTable. There is some unknown data after the StringTable, scan forward
// from this point to the flag for the start of an XML start tag.
int xmlTagOff = littleEndianValue(xml, 3 * 4); // Start from the offset in the 3rd word.
// Scan forward until we find the bytes: 0x02011000(x00100102 in normal int).
for (int ii = xmlTagOff; ii < xml.length - 4; ii += 4) {
if (littleEndianValue(xml, ii) == START_TAG) {
xmlTagOff = ii;
break;
}
}
return xmlTagOff;
}
}

View File

@ -23,6 +23,7 @@ package com.leanplum.internal;
import android.app.Activity;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumEditorMode;
import com.leanplum.LeanplumUIEditor;
@ -34,6 +35,8 @@ import static com.leanplum.internal.Constants.ClassUtil.UI_INTERFACE_EDITOR;
/**
* Wrapper class for the UI Editor. Method calls will be forwarded to UI Editor package if its
* available.
*
* @author Ben Marten
*/
public class LeanplumUIEditorWrapper implements LeanplumUIEditor {
private static LeanplumUIEditor interfaceEditorSingleton;
@ -44,17 +47,17 @@ public class LeanplumUIEditorWrapper implements LeanplumUIEditor {
}
static {
Class<?> clazz = null;
Class clazz = null;
try {
clazz = Class.forName(UI_INTERFACE_EDITOR);
} catch (ClassNotFoundException ignored) {
} catch (Throwable ignored) {
}
if (clazz != null) {
Method method = null;
try {
method = clazz.getMethod("getInstance");
} catch (NoSuchMethodException e) {
Util.handleException(e);
} catch (Throwable t) {
Util.handleException(t);
}
if (method != null) {
try {
@ -66,6 +69,8 @@ public class LeanplumUIEditorWrapper implements LeanplumUIEditor {
Util.handleException(e);
} catch (InvocationTargetException e) {
Util.handleException(e);
} catch (Throwable t) {
Util.handleException(t);
}
}
}
@ -104,6 +109,7 @@ public class LeanplumUIEditorWrapper implements LeanplumUIEditor {
if (interfaceEditorSingleton != null) {
interfaceEditorSingleton.startUpdating();
}
Leanplum.countAggregator().incrementCount("start_updating_ui");
}
/**
@ -121,6 +127,7 @@ public class LeanplumUIEditorWrapper implements LeanplumUIEditor {
if (interfaceEditorSingleton != null) {
interfaceEditorSingleton.sendUpdate();
}
Leanplum.countAggregator().incrementCount("send_update_ui");
}
@Override

View File

@ -22,6 +22,8 @@
package com.leanplum.internal;
import org.mozilla.gecko.thirdparty_unused.BuildConfig;
import java.util.HashMap;
/**
@ -132,7 +134,10 @@ public class Log {
case PRIVATE:
maybeSendLog(tag + prefix + message);
return;
default:
default: // DEBUG
if (BuildConfig.DEBUG) {
android.util.Log.d(tag, prefix + message);
}
}
}
@ -183,7 +188,7 @@ public class Log {
HashMap<String, Object> params = new HashMap<>();
params.put(Constants.Params.TYPE, Constants.Values.SDK_LOG);
params.put(Constants.Params.MESSAGE, message);
Request.post(Constants.Methods.LOG, params).sendEventually();
RequestOld.post(Constants.Methods.LOG, params).sendEventually();
} catch (Throwable t) {
android.util.Log.e("Leanplum", "Unable to send log.", t);
} finally {

View File

@ -21,6 +21,7 @@
package com.leanplum.internal;
import com.leanplum.Leanplum;
import com.leanplum.callbacks.StartCallback;
import org.json.JSONObject;
@ -32,21 +33,21 @@ public class Registration {
public static void registerDevice(String email, final StartCallback callback) {
Map<String, Object> params = new HashMap<>();
params.put(Constants.Params.EMAIL, email);
Request request = Request.post(Constants.Methods.REGISTER_FOR_DEVELOPMENT, params);
request.onResponse(new Request.ResponseCallback() {
RequestOld request = RequestOld.post(Constants.Methods.REGISTER_FOR_DEVELOPMENT, params);
request.onResponse(new RequestOld.ResponseCallback() {
@Override
public void response(final JSONObject response) {
OsHandler.getInstance().post(new Runnable() {
@Override
public void run() {
try {
boolean isSuccess = Request.isResponseSuccess(response);
boolean isSuccess = RequestOld.isResponseSuccess(response);
if (isSuccess) {
if (callback != null) {
callback.onResponse(true);
}
} else {
Log.e(Request.getResponseError(response));
Log.e(RequestOld.getResponseError(response));
if (callback != null) {
callback.onResponse(false);
}
@ -58,7 +59,7 @@ public class Registration {
});
}
});
request.onError(new Request.ErrorCallback() {
request.onError(new RequestOld.ErrorCallback() {
@Override
public void error(final Exception e) {
OsHandler.getInstance().post(new Runnable() {
@ -72,5 +73,6 @@ public class Registration {
}
});
request.sendIfConnected();
Leanplum.countAggregator().incrementCount("register_device");
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013, Leanplum, Inc. All rights reserved.
* Copyright 2018, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@ -21,935 +21,53 @@
package com.leanplum.internal;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Build;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import com.leanplum.Leanplum;
import com.leanplum.utils.SharedPreferencesUtil;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.EOFException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.UUID;
/**
* Leanplum request class.
*
* @author Andrew First
*/
public class Request {
private static final long DEVELOPMENT_MIN_DELAY_MS = 100;
private static final long DEVELOPMENT_MAX_DELAY_MS = 5000;
private static final long PRODUCTION_DELAY = 60000;
static final int MAX_EVENTS_PER_API_CALL;
static final String LEANPLUM = "__leanplum__";
static final String UUID_KEY = "uuid";
private static String appId;
private static String accessKey;
private static String deviceId;
private static String userId;
private static final LeanplumEventCallbackManager eventCallbackManager =
new LeanplumEventCallbackManager();
private static final Map<String, Boolean> fileTransferStatus = new HashMap<>();
private static int pendingDownloads;
private static NoPendingDownloadsCallback noPendingDownloadsBlock;
// The token is saved primarily for legacy SharedPreferences decryption. This could
// likely be removed in the future.
private static String token = null;
private static final Map<File, Long> fileUploadSize = new HashMap<>();
private static final Map<File, Double> fileUploadProgress = new HashMap<>();
private static String fileUploadProgressString = "";
private static long lastSendTimeMs;
private static final Object uploadFileLock = new Object();
public class Request implements Requesting {
private final String httpMethod;
private final String apiMethod;
private final Map<String, Object> params;
private ResponseCallback response;
private ErrorCallback error;
private boolean sent;
private long dataBaseIndex;
private static ApiResponseCallback apiResponse;
private static List<Map<String, Object>> localErrors = new ArrayList<>();
static {
if (Build.VERSION.SDK_INT <= 17) {
MAX_EVENTS_PER_API_CALL = 5000;
} else {
MAX_EVENTS_PER_API_CALL = 10000;
}
}
public static void setAppId(String appId, String accessKey) {
if (!TextUtils.isEmpty(appId)) {
Request.appId = appId.trim();
}
if (!TextUtils.isEmpty(accessKey)) {
Request.accessKey = accessKey.trim();
}
}
public static void setDeviceId(String deviceId) {
Request.deviceId = deviceId;
}
public static void setUserId(String userId) {
Request.userId = userId;
}
public static void setToken(String token) {
Request.token = token;
}
public static String token() {
return token;
}
/**
* Since requests are batched there can be a case where other Request can take future Request
* events. We need to have for each Request database index for handle response, error or start
* callbacks.
*
* @return Index of event at database.
*/
public long getDataBaseIndex() {
return dataBaseIndex;
}
// Update index of event at database.
public void setDataBaseIndex(long dataBaseIndex) {
this.dataBaseIndex = dataBaseIndex;
}
public static void loadToken() {
Context context = Leanplum.getContext();
SharedPreferences defaults = context.getSharedPreferences(
LEANPLUM, Context.MODE_PRIVATE);
String token = defaults.getString(Constants.Defaults.TOKEN_KEY, null);
if (token == null) {
return;
}
setToken(token);
}
public static void saveToken() {
Context context = Leanplum.getContext();
SharedPreferences defaults = context.getSharedPreferences(
LEANPLUM, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = defaults.edit();
editor.putString(Constants.Defaults.TOKEN_KEY, Request.token());
SharedPreferencesUtil.commitChanges(editor);
}
public static String appId() {
return appId;
}
public static String deviceId() {
return deviceId;
}
public static String userId() {
return Request.userId;
}
private ResponseCallback response = null;
private ErrorCallback error = null;
private final CountAggregator countAggregator;
public Request(String httpMethod, String apiMethod, Map<String, Object> params) {
this.httpMethod = httpMethod;
this.apiMethod = apiMethod;
this.params = params != null ? params : new HashMap<String, Object>();
// Check if it is error and here was SQLite exception.
if (Constants.Methods.LOG.equals(apiMethod) && LeanplumEventDataManager.willSendErrorLog) {
localErrors.add(createArgsDictionary());
}
// Make sure the Handler is initialized on the main thread.
OsHandler.getInstance();
dataBaseIndex = -1;
this.params = params;
this.countAggregator = Leanplum.countAggregator();
}
public static Request get(String apiMethod, Map<String, Object> params) {
Log.LeanplumLogType level = Constants.Methods.LOG.equals(apiMethod) ?
Log.LeanplumLogType.DEBUG : Log.LeanplumLogType.VERBOSE;
Log.log(level, "Will call API method " + apiMethod + " with arguments " + params);
return RequestFactory.getInstance().createRequest("GET", apiMethod, params);
Leanplum.countAggregator().incrementCount("get_lprequest");
return new Request("GET", apiMethod, params);
}
public static Request post(String apiMethod, Map<String, Object> params) {
Log.LeanplumLogType level = Constants.Methods.LOG.equals(apiMethod) ?
Log.LeanplumLogType.DEBUG : Log.LeanplumLogType.VERBOSE;
Log.log(level, "Will call API method " + apiMethod + " with arguments " + params);
return RequestFactory.getInstance().createRequest("POST", apiMethod, params);
Leanplum.countAggregator().incrementCount("post_lprequest");
return new Request("POST", apiMethod, params);
}
public void onResponse(ResponseCallback response) {
this.response = response;
this.countAggregator.incrementCount("on_response_lprequest");
}
public void onError(ErrorCallback error) {
this.error = error;
}
public void onApiResponse(ApiResponseCallback apiResponse) {
Request.apiResponse = apiResponse;
}
private Map<String, Object> createArgsDictionary() {
Map<String, Object> args = new HashMap<>();
args.put(Constants.Params.DEVICE_ID, deviceId);
args.put(Constants.Params.USER_ID, userId);
args.put(Constants.Params.ACTION, apiMethod);
args.put(Constants.Params.SDK_VERSION, Constants.LEANPLUM_VERSION);
args.put(Constants.Params.DEV_MODE, Boolean.toString(Constants.isDevelopmentModeEnabled));
args.put(Constants.Params.TIME, Double.toString(new Date().getTime() / 1000.0));
if (token != null) {
args.put(Constants.Params.TOKEN, token);
}
args.putAll(params);
return args;
}
private void saveRequestForLater(Map<String, Object> args) {
synchronized (Request.class) {
Context context = Leanplum.getContext();
SharedPreferences preferences = context.getSharedPreferences(
LEANPLUM, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
long count = LeanplumEventDataManager.getEventsCount();
String uuid = preferences.getString(Constants.Defaults.UUID_KEY, null);
if (uuid == null || count % MAX_EVENTS_PER_API_CALL == 0) {
uuid = UUID.randomUUID().toString();
editor.putString(Constants.Defaults.UUID_KEY, uuid);
SharedPreferencesUtil.commitChanges(editor);
}
args.put(UUID_KEY, uuid);
LeanplumEventDataManager.insertEvent(JsonConverter.toJson(args));
dataBaseIndex = count;
// Checks if here response and/or error callback for this request. We need to add callbacks to
// eventCallbackManager only if here was internet connection, otherwise triggerErrorCallback
// will handle error callback for this event.
if (response != null || error != null && !Util.isConnected()) {
eventCallbackManager.addCallbacks(this, response, error);
}
}
}
public void send() {
this.sendEventually();
if (Constants.isDevelopmentModeEnabled) {
long currentTimeMs = System.currentTimeMillis();
long delayMs;
if (lastSendTimeMs == 0 || currentTimeMs - lastSendTimeMs > DEVELOPMENT_MAX_DELAY_MS) {
delayMs = DEVELOPMENT_MIN_DELAY_MS;
} else {
delayMs = (lastSendTimeMs + DEVELOPMENT_MAX_DELAY_MS) - currentTimeMs;
}
OsHandler.getInstance().postDelayed(new Runnable() {
@Override
public void run() {
try {
sendIfConnected();
} catch (Throwable t) {
Util.handleException(t);
}
}
}, delayMs);
}
}
/**
* Wait 1 second for potential other API calls, and then sends the call synchronously if no other
* call has been sent within 1 minute.
*/
public void sendIfDelayed() {
sendEventually();
OsHandler.getInstance().postDelayed(new Runnable() {
@Override
public void run() {
try {
sendIfDelayedHelper();
} catch (Throwable t) {
Util.handleException(t);
}
}
}, 1000);
}
/**
* Sends the call synchronously if no other call has been sent within 1 minute.
*/
private void sendIfDelayedHelper() {
if (Constants.isDevelopmentModeEnabled) {
send();
} else {
long currentTimeMs = System.currentTimeMillis();
if (lastSendTimeMs == 0 || currentTimeMs - lastSendTimeMs > PRODUCTION_DELAY) {
sendIfConnected();
}
}
}
public void sendIfConnected() {
if (Util.isConnected()) {
this.sendNow();
} else {
this.sendEventually();
Log.i("Device is offline, will send later");
triggerErrorCallback(new Exception("Not connected to the Internet"));
}
}
private void triggerErrorCallback(Exception e) {
if (error != null) {
error.error(e);
}
if (apiResponse != null) {
List<Map<String, Object>> requests = getUnsentRequests();
List<Map<String, Object>> requestsToSend = removeIrrelevantBackgroundStartRequests(requests);
apiResponse.response(requestsToSend, null, requests.size());
}
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private static boolean attachApiKeys(Map<String, Object> dict) {
if (appId == null || accessKey == null) {
Log.e("API keys are not set. Please use Leanplum.setAppIdForDevelopmentMode or "
+ "Leanplum.setAppIdForProductionMode.");
return false;
}
dict.put(Constants.Params.APP_ID, appId);
dict.put(Constants.Params.CLIENT_KEY, accessKey);
dict.put(Constants.Params.CLIENT, Constants.CLIENT);
return true;
}
public interface ResponseCallback {
void response(JSONObject response);
}
public interface ApiResponseCallback {
void response(List<Map<String, Object>> requests, JSONObject response, int countOfEvents);
}
public interface ErrorCallback {
void error(Exception e);
}
public interface NoPendingDownloadsCallback {
void noPendingDownloads();
}
/**
* Parse response body from server. Invoke potential error or response callbacks for all events
* of this request.
*
* @param responseBody JSONObject with response body from server.
* @param requestsToSend List of requests that were sent to the server/
* @param error Exception.
* @param unsentRequestsSize Size of unsent request, that we will delete.
*/
private void parseResponseBody(JSONObject responseBody, List<Map<String, Object>>
requestsToSend, Exception error, int unsentRequestsSize) {
synchronized (Request.class) {
if (responseBody == null && error != null) {
// Invoke potential error callbacks for all events of this request.
eventCallbackManager.invokeAllCallbacksWithError(error, unsentRequestsSize);
return;
} else if (responseBody == null) {
return;
}
// Response for last start call.
if (apiResponse != null) {
apiResponse.response(requestsToSend, responseBody, unsentRequestsSize);
}
// We will replace it with error from response body, if we found it.
Exception lastResponseError = error;
// Valid response, parse and handle response body.
int numResponses = Request.numResponses(responseBody);
for (int i = 0; i < numResponses; i++) {
JSONObject response = Request.getResponseAt(responseBody, i);
if (Request.isResponseSuccess(response)) {
continue; // If event response is successful, proceed with next one.
}
// If event response was not successful, handle error.
String errorMessage = getReadableErrorMessage(Request.getResponseError(response));
Log.e(errorMessage);
// Throw an exception if last event response is negative.
if (i == numResponses - 1) {
lastResponseError = new Exception(errorMessage);
}
}
if (lastResponseError != null) {
// Invoke potential error callbacks for all events of this request.
eventCallbackManager.invokeAllCallbacksWithError(lastResponseError, unsentRequestsSize);
} else {
// Invoke potential response callbacks for all events of this request.
eventCallbackManager.invokeAllCallbacksForResponse(responseBody, unsentRequestsSize);
}
}
}
/**
* Parse error message from server response and return readable error message.
*
* @param errorMessage String of error from server response.
* @return String of readable error message.
*/
@NonNull
private String getReadableErrorMessage(String errorMessage) {
if (errorMessage == null || errorMessage.length() == 0) {
errorMessage = "API error";
} else if (errorMessage.startsWith("App not found")) {
errorMessage = "No app matching the provided app ID was found.";
Constants.isInPermanentFailureState = true;
} else if (errorMessage.startsWith("Invalid access key")) {
errorMessage = "The access key you provided is not valid for this app.";
Constants.isInPermanentFailureState = true;
} else if (errorMessage.startsWith("Development mode requested but not permitted")) {
errorMessage = "A call to Leanplum.setAppIdForDevelopmentMode "
+ "with your production key was made, which is not permitted.";
Constants.isInPermanentFailureState = true;
} else {
errorMessage = "API error: " + errorMessage;
}
return errorMessage;
}
private void sendNow() {
if (Constants.isTestMode) {
return;
}
if (appId == null) {
Log.e("Cannot send request. appId is not set.");
return;
}
if (accessKey == null) {
Log.e("Cannot send request. accessKey is not set.");
return;
}
this.sendEventually();
Util.executeAsyncTask(true, new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
sendRequests();
return null;
}
});
}
private void sendRequests() {
List<Map<String, Object>> unsentRequests = new ArrayList<>();
List<Map<String, Object>> requestsToSend;
// Check if we have localErrors, if yes then we will send only errors to the server.
if (localErrors.size() != 0) {
String uuid = UUID.randomUUID().toString();
for (Map<String, Object> error : localErrors) {
error.put(UUID_KEY, uuid);
unsentRequests.add(error);
}
requestsToSend = unsentRequests;
} else {
unsentRequests = getUnsentRequests();
requestsToSend = removeIrrelevantBackgroundStartRequests(unsentRequests);
}
if (requestsToSend.isEmpty()) {
return;
}
final Map<String, Object> multiRequestArgs = new HashMap<>();
if (!Request.attachApiKeys(multiRequestArgs)) {
return;
}
multiRequestArgs.put(Constants.Params.DATA, jsonEncodeUnsentRequests(requestsToSend));
multiRequestArgs.put(Constants.Params.SDK_VERSION, Constants.LEANPLUM_VERSION);
multiRequestArgs.put(Constants.Params.ACTION, Constants.Methods.MULTI);
multiRequestArgs.put(Constants.Params.TIME, Double.toString(new Date().getTime() / 1000.0));
JSONObject responseBody;
HttpURLConnection op = null;
try {
try {
op = Util.operation(
Constants.API_HOST_NAME,
Constants.API_SERVLET,
multiRequestArgs,
httpMethod,
Constants.API_SSL,
Constants.NETWORK_TIMEOUT_SECONDS);
responseBody = Util.getJsonResponse(op);
int statusCode = op.getResponseCode();
Exception errorException;
if (statusCode >= 200 && statusCode <= 299) {
if (responseBody == null) {
errorException = new Exception("Response JSON is null.");
deleteSentRequests(unsentRequests.size());
parseResponseBody(null, requestsToSend, errorException, unsentRequests.size());
return;
}
Exception exception = null;
// Checks if we received the same number of responses as a number of sent request.
int numResponses = Request.numResponses(responseBody);
if (numResponses != requestsToSend.size()) {
Log.w("Sent " + requestsToSend.size() + " requests but only" +
" received " + numResponses);
}
parseResponseBody(responseBody, requestsToSend, null, unsentRequests.size());
// Clear localErrors list.
localErrors.clear();
deleteSentRequests(unsentRequests.size());
// Send another request if the last request had maximum events per api call.
if (unsentRequests.size() == MAX_EVENTS_PER_API_CALL) {
sendRequests();
}
} else {
errorException = new Exception("HTTP error " + statusCode);
if (statusCode != -1 && statusCode != 408 && !(statusCode >= 500 && statusCode <= 599)) {
deleteSentRequests(unsentRequests.size());
parseResponseBody(responseBody, requestsToSend, errorException, unsentRequests.size());
}
}
} catch (JSONException e) {
Log.e("Error parsing JSON response: " + e.toString() + "\n" + Log.getStackTraceString(e));
deleteSentRequests(unsentRequests.size());
parseResponseBody(null, requestsToSend, e, unsentRequests.size());
} catch (Exception e) {
Log.e("Unable to send request: " + e.toString() + "\n" + Log.getStackTraceString(e));
} finally {
if (op != null) {
op.disconnect();
}
}
} catch (Throwable t) {
Util.handleException(t);
}
}
public void sendEventually() {
if (Constants.isTestMode) {
return;
}
if (LeanplumEventDataManager.willSendErrorLog) {
return;
}
if (!sent) {
sent = true;
Map<String, Object> args = createArgsDictionary();
saveRequestForLater(args);
}
}
static void deleteSentRequests(int requestsCount) {
if (requestsCount == 0) {
return;
}
synchronized (Request.class) {
LeanplumEventDataManager.deleteEvents(requestsCount);
}
}
private static List<Map<String, Object>> getUnsentRequests() {
List<Map<String, Object>> requestData;
synchronized (Request.class) {
lastSendTimeMs = System.currentTimeMillis();
Context context = Leanplum.getContext();
SharedPreferences preferences = context.getSharedPreferences(
LEANPLUM, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
requestData = LeanplumEventDataManager.getEvents(MAX_EVENTS_PER_API_CALL);
editor.remove(Constants.Defaults.UUID_KEY);
SharedPreferencesUtil.commitChanges(editor);
}
return requestData;
}
/**
* In various scenarios we can end up batching a big number of requests (e.g. device is offline,
* background sessions), which could make the stored API calls batch look something like:
* <p>
* <code>start(B), start(B), start(F), track, start(B), track, start(F), resumeSession</code>
* <p>
* where <code>start(B)</code> indicates a start in the background, and <code>start(F)</code>
* one in the foreground.
* <p>
* In this case the first two <code>start(B)</code> can be dropped because they don't contribute
* any relevant information for the batch call.
* <p>
* Essentially we drop every <code>start(B)</code> call, that is directly followed by any kind of
* a <code>start</code> call.
*
* @param requestData A list of the requests, stored on the device.
* @return A list of only these requests, which contain relevant information for the API call.
*/
private static List<Map<String, Object>> removeIrrelevantBackgroundStartRequests(
List<Map<String, Object>> requestData) {
List<Map<String, Object>> relevantRequests = new ArrayList<>();
int requestCount = requestData.size();
if (requestCount > 0) {
for (int i = 0; i < requestCount; i++) {
Map<String, Object> currentRequest = requestData.get(i);
if (i < requestCount - 1
&& Constants.Methods.START.equals(requestData.get(i + 1).get(Constants.Params.ACTION))
&& Constants.Methods.START.equals(currentRequest.get(Constants.Params.ACTION))
&& Boolean.TRUE.toString().equals(currentRequest.get(Constants.Params.BACKGROUND))) {
continue;
}
relevantRequests.add(currentRequest);
}
}
return relevantRequests;
}
private static String jsonEncodeUnsentRequests(List<Map<String, Object>> requestData) {
Map<String, Object> data = new HashMap<>();
data.put(Constants.Params.DATA, requestData);
return JsonConverter.toJson(data);
}
private static String getSizeAsString(int bytes) {
if (bytes < (1 << 10)) {
return bytes + " B";
} else if (bytes < (1 << 20)) {
return (bytes >> 10) + " KB";
} else {
return (bytes >> 20) + " MB";
}
}
private static void printUploadProgress() {
int totalFiles = fileUploadSize.size();
int sentFiles = 0;
int totalBytes = 0;
int sentBytes = 0;
for (Map.Entry<File, Long> entry : fileUploadSize.entrySet()) {
File file = entry.getKey();
long fileSize = entry.getValue();
double fileProgress = fileUploadProgress.get(file);
if (fileProgress == 1) {
sentFiles++;
}
sentBytes += (int) (fileSize * fileProgress);
totalBytes += fileSize;
}
String progressString = "Uploading resources. " +
sentFiles + '/' + totalFiles + " files completed; " +
getSizeAsString(sentBytes) + '/' + getSizeAsString(totalBytes) + " transferred.";
if (!fileUploadProgressString.equals(progressString)) {
fileUploadProgressString = progressString;
Log.i(progressString);
}
}
public void sendFilesNow(final List<String> filenames, final List<InputStream> streams) {
if (Constants.isTestMode) {
return;
}
final Map<String, Object> dict = createArgsDictionary();
if (!attachApiKeys(dict)) {
return;
}
final List<File> filesToUpload = new ArrayList<>();
// First set up the files for upload
for (int i = 0; i < filenames.size(); i++) {
String filename = filenames.get(i);
if (filename == null || Boolean.TRUE.equals(fileTransferStatus.get(filename))) {
continue;
}
File file = new File(filename);
long size;
try {
size = streams.get(i).available();
} catch (IOException e) {
size = file.length();
} catch (NullPointerException e) {
// Not good. Can't read asset.
Log.e("Unable to read file " + filename);
continue;
}
fileTransferStatus.put(filename, true);
filesToUpload.add(file);
fileUploadSize.put(file, size);
fileUploadProgress.put(file, 0.0);
}
if (filesToUpload.size() == 0) {
return;
}
printUploadProgress();
// Now upload the files
Util.executeAsyncTask(false, new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
synchronized (uploadFileLock) { // Don't overload app and server with many upload tasks
JSONObject result;
HttpURLConnection op = null;
try {
op = Util.uploadFilesOperation(
Constants.Params.FILE,
filesToUpload,
streams,
Constants.API_HOST_NAME,
Constants.API_SERVLET,
dict,
httpMethod,
Constants.API_SSL,
60);
if (op != null) {
result = Util.getJsonResponse(op);
int statusCode = op.getResponseCode();
if (statusCode != 200) {
throw new Exception("Leanplum: Error sending request: " + statusCode);
}
if (Request.this.response != null) {
Request.this.response.response(result);
}
} else {
if (error != null) {
error.error(new Exception("Leanplum: Unable to read file."));
}
}
} catch (JSONException e) {
Log.e("Unable to convert to JSON.", e);
if (error != null) {
error.error(e);
}
} catch (SocketTimeoutException e) {
Log.e("Timeout uploading files. Try again or limit the number of files " +
"to upload with parameters to syncResourcesAsync.");
if (error != null) {
error.error(e);
}
} catch (Exception e) {
Log.e("Unable to send file.", e);
if (error != null) {
error.error(e);
}
} finally {
if (op != null) {
op.disconnect();
}
}
for (File file : filesToUpload) {
fileUploadProgress.put(file, 1.0);
}
printUploadProgress();
return null;
}
}
});
// TODO: Upload progress
}
void downloadFile(final String path, final String url) {
if (Constants.isTestMode) {
return;
}
if (Boolean.TRUE.equals(fileTransferStatus.get(path))) {
return;
}
pendingDownloads++;
Log.i("Downloading resource " + path);
fileTransferStatus.put(path, true);
final Map<String, Object> dict = createArgsDictionary();
dict.put(Constants.Keys.FILENAME, path);
if (!attachApiKeys(dict)) {
return;
}
Util.executeAsyncTask(false, new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
try {
downloadHelper(Constants.API_HOST_NAME, Constants.API_SERVLET, path, url, dict);
} catch (Throwable t) {
Util.handleException(t);
}
return null;
}
});
// TODO: Download progress
}
private void downloadHelper(String hostName, String servlet, final String path, final String url,
final Map<String, Object> dict) {
HttpURLConnection op = null;
URL originalURL = null;
try {
if (url == null) {
op = Util.operation(
hostName,
servlet,
dict,
httpMethod,
Constants.API_SSL,
Constants.NETWORK_TIMEOUT_SECONDS_FOR_DOWNLOADS);
} else {
op = Util.createHttpUrlConnection(url, httpMethod, url.startsWith("https://"),
Constants.NETWORK_TIMEOUT_SECONDS_FOR_DOWNLOADS);
}
originalURL = op.getURL();
op.connect();
int statusCode = op.getResponseCode();
if (statusCode != 200) {
throw new Exception("Leanplum: Error sending request to: " + hostName +
", HTTP status code: " + statusCode);
}
Stack<String> dirs = new Stack<>();
String currentDir = path;
while ((currentDir = new File(currentDir).getParent()) != null) {
dirs.push(currentDir);
}
while (!dirs.isEmpty()) {
String directory = FileManager.fileRelativeToDocuments(dirs.pop());
boolean isCreated = new File(directory).mkdir();
if (!isCreated) {
Log.w("Failed to create directory: ", directory);
}
}
FileOutputStream out = new FileOutputStream(
new File(FileManager.fileRelativeToDocuments(path)));
Util.saveResponse(op, out);
pendingDownloads--;
if (Request.this.response != null) {
Request.this.response.response(null);
}
if (pendingDownloads == 0 && noPendingDownloadsBlock != null) {
noPendingDownloadsBlock.noPendingDownloads();
}
} catch (Exception e) {
if (e instanceof EOFException) {
if (op != null && !op.getURL().equals(originalURL)) {
downloadHelper(null, op.getURL().toString(), path, url, new HashMap<String, Object>());
return;
}
}
Log.e("Error downloading resource:" + path, e);
pendingDownloads--;
if (error != null) {
error.error(e);
}
if (pendingDownloads == 0 && noPendingDownloadsBlock != null) {
noPendingDownloadsBlock.noPendingDownloads();
}
} finally {
if (op != null) {
op.disconnect();
}
}
}
public static int numPendingDownloads() {
return pendingDownloads;
}
public static void onNoPendingDownloads(NoPendingDownloadsCallback block) {
noPendingDownloadsBlock = block;
}
public static int numResponses(JSONObject response) {
if (response == null) {
return 0;
}
try {
return response.getJSONArray("response").length();
} catch (JSONException e) {
Log.e("Could not parse JSON response.", e);
return 0;
}
}
public static JSONObject getResponseAt(JSONObject response, int index) {
try {
return response.getJSONArray("response").getJSONObject(index);
} catch (JSONException e) {
Log.e("Could not parse JSON response.", e);
return null;
}
}
public static JSONObject getLastResponse(JSONObject response) {
int numResponses = numResponses(response);
if (numResponses > 0) {
return getResponseAt(response, numResponses - 1);
} else {
return null;
}
}
public static boolean isResponseSuccess(JSONObject response) {
if (response == null) {
return false;
}
try {
return response.getBoolean("success");
} catch (JSONException e) {
Log.e("Could not parse JSON response.", e);
return false;
}
}
public static String getResponseError(JSONObject response) {
if (response == null) {
return null;
}
try {
JSONObject error = response.optJSONObject("error");
if (error == null) {
return null;
}
return error.getString("message");
} catch (JSONException e) {
Log.e("Could not parse JSON response.", e);
return null;
}
this.countAggregator.incrementCount("on_error_lprequest");
}
}

View File

@ -21,20 +21,179 @@
package com.leanplum.internal;
import com.leanplum.Leanplum;
import java.util.Map;
public class RequestFactory {
private static final String API_METHOD_START = "start";
private static final String API_METHOD_GET_VARS = "getVars";
private static final String API_METHOD_SET_VARS = "setVars";
private static final String API_METHOD_STOP = "stop";
private static final String API_METHOD_RESTART = "restart";
private static final String API_METHOD_TRACK = "track";
private static final String API_METHOD_ADVANCE = "advance";
private static final String API_METHOD_PAUSE_SESSION = "pauseSession";
private static final String API_METHOD_PAUSE_STATE = "pauseState";
private static final String API_METHOD_RESUME_SESSION = "resumeSession";
private static final String API_METHOD_RESUME_STATE = "resumeState";
private static final String API_METHOD_MULTI = "multi";
private static final String API_METHOD_REGISTER_FOR_DEVELOPMENT = "registerDevice";
private static final String API_METHOD_SET_USER_ATTRIBUTES = "setUserAttributes";
private static final String API_METHOD_SET_DEVICE_ATTRIBUTES = "setDeviceAttributes";
private static final String API_METHOD_SET_TRAFFIC_SOURCE_INFO = "setTrafficSourceInfo";
private static final String API_METHOD_UPLOAD_FILE = "uploadFile";
private static final String API_METHOD_DOWNLOAD_FILE = "downloadFile";
private static final String API_METHOD_HEARTBEAT = "heartbeat";
private static final String API_METHOD_SAVE_VIEW_CONTROLLER_VERSION = "saveInterface";
private static final String API_METHOD_SAVE_VIEW_CONTROLLER_IMAGE = "saveInterfaceImage";
private static final String API_METHOD_GET_VIEW_CONTROLLER_VERSIONS_LIST = "getViewControllerVersionsList";
private static final String API_METHOD_LOG = "log";
private static final String API_METHOD_GET_INBOX_MESSAGES = "getNewsfeedMessages";
private static final String API_METHOD_MARK_INBOX_MESSAGE_AS_READ = "markNewsfeedMessageAsRead";
private static final String API_METHOD_DELETE_INBOX_MESSAGE = "deleteNewsfeedMessage";
public static RequestFactory defaultFactory;
private CountAggregator countAggregator;
private FeatureFlagManager featureFlagManager;
public synchronized static RequestFactory getInstance() {
if (defaultFactory == null) {
defaultFactory = new RequestFactory();
defaultFactory.countAggregator = Leanplum.countAggregator();
defaultFactory.featureFlagManager = Leanplum.featureFlagManager();
}
return defaultFactory;
}
public Request createRequest(
public RequestOld createRequest(
String httpMethod, String apiMethod, Map<String, Object> params) {
return new Request(httpMethod, apiMethod, params);
Leanplum.countAggregator().incrementCount("createRequest");
return new RequestOld(httpMethod, apiMethod, params);
}
public Requesting startWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_START, params);
}
public Requesting getVarsWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_GET_VARS, params);
}
public Requesting setVarsWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_SET_VARS, params);
}
public Requesting stopWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_STOP, params);
}
public Requesting restartWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_RESTART, params);
}
public Requesting trackWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_TRACK, params);
}
public Requesting advanceWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_ADVANCE, params);
}
public Requesting pauseSessionWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_PAUSE_SESSION, params);
}
public Requesting pauseStateWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_PAUSE_STATE, params);
}
public Requesting resumeSessionWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_RESUME_SESSION, params);
}
public Requesting resumeStateWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_RESUME_STATE, params);
}
public Requesting multiWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_MULTI, params);
}
public Requesting registerDeviceWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_REGISTER_FOR_DEVELOPMENT, params);
}
public Requesting setUserAttributesWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_SET_USER_ATTRIBUTES, params);
}
public Requesting setDeviceAttributesWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_SET_DEVICE_ATTRIBUTES, params);
}
public Requesting setTrafficSourceInfoWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_SET_TRAFFIC_SOURCE_INFO, params);
}
public Requesting uploadFileWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_UPLOAD_FILE, params);
}
public Requesting downloadFileWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_DOWNLOAD_FILE, params);
}
public Requesting heartbeatWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_HEARTBEAT, params);
}
public Requesting saveInterfaceWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_SAVE_VIEW_CONTROLLER_VERSION, params);
}
public Requesting saveInterfaceImageWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_SAVE_VIEW_CONTROLLER_IMAGE, params);
}
public Requesting getViewControllerVersionsListWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_GET_VIEW_CONTROLLER_VERSIONS_LIST, params);
}
public Requesting logWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_LOG, params);
}
public Requesting getNewsfeedMessagesWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_GET_INBOX_MESSAGES, params);
}
public Requesting markNewsfeedMessageAsReadWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_MARK_INBOX_MESSAGE_AS_READ, params);
}
public Requesting deleteNewsfeedMessagesWithParams(Map<String, Object> params) {
return createPostForApiMethod(API_METHOD_DELETE_INBOX_MESSAGE, params);
}
private Requesting createGetForApiMethod(String apiMethod, Map<String, Object> params) {
if (shouldReturnLPRequestClass()) {
return Request.get(apiMethod, params);
}
return RequestOld.get(apiMethod, params);
}
private Requesting createPostForApiMethod(String apiMethod, Map<String, Object> params) {
if (shouldReturnLPRequestClass()) {
return Request.post(apiMethod, params);
}
return RequestOld.post(apiMethod, params);
}
private Boolean shouldReturnLPRequestClass() {
return Leanplum.featureFlagManager().isFeatureFlagEnabled(Leanplum.featureFlagManager().FEATURE_FLAG_REQUEST_REFACTOR);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
package com.leanplum.internal;
/** Records request call sequence of read/write operations to database. */
public interface RequestSequenceRecorder {
/** Executes before database read in RequestOld. */
void beforeRead();
/** Executes after database read in RequestOld. */
void afterRead();
/** Executes before database write in RequestOld. */
void beforeWrite();
/** Executes after database write in RequestOld. */
void afterWrite();
}

View File

@ -0,0 +1,14 @@
package com.leanplum.internal;
import org.json.JSONObject;
public interface Requesting {
interface ResponseCallback {
void response(JSONObject response);
}
interface ErrorCallback {
void error(Exception e);
}
}

View File

@ -97,8 +97,8 @@ public class Socket {
Log.i("Connected to development server");
try {
Map<String, String> args = Util.newMap(
Constants.Params.APP_ID, Request.appId(),
Constants.Params.DEVICE_ID, Request.deviceId());
Constants.Params.APP_ID, RequestOld.appId(),
Constants.Params.DEVICE_ID, RequestOld.deviceId());
try {
sio.emit("auth", new JSONArray(Collections.singletonList(new JSONObject(args))));
} catch (JSONException e) {
@ -111,6 +111,7 @@ public class Socket {
connected = true;
connecting = false;
}
Leanplum.countAggregator().incrementCount("connect_to_app_id");
}
@Override
@ -202,6 +203,7 @@ public class Socket {
} catch (JSONException e) {
Log.e("Failed to create JSON data object: " + e.getMessage());
}
Leanplum.countAggregator().incrementCount("send_event_socket");
}
/**
@ -232,7 +234,7 @@ public class Socket {
((BaseActionContext) context).setIsPreview(true);
context.update();
LeanplumInternal.triggerAction(context);
ActionManager.getInstance().recordMessageImpression(messageId);
Leanplum.triggerMessageDisplayed(context);
}
} catch (JSONException e) {
Log.e("Error getting action info", e);
@ -307,7 +309,7 @@ public class Socket {
return;
}
VarCache.applyVariableDiffs(
JsonConverter.mapFromJson(object), null, null, null, null, null);
JsonConverter.mapFromJson(object), null, null, null, null, null, null);
} catch (JSONException e) {
Log.e("Couldn't applyVars for preview.", e);
} catch (Throwable e) {

View File

@ -30,21 +30,24 @@ import com.leanplum.Leanplum;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.thirdparty_unused.BuildConfig;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Arrays;
import java.util.HashSet;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.methods.HttpPost;
import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient;
import ch.boye.httpclientandroidlib.impl.client.HttpClients;
// Suppressing deprecated apache dependency.
@SuppressWarnings("deprecation")
class SocketIOClient {
interface Handler {
void onConnect();
@ -70,30 +73,22 @@ class SocketIOClient {
mHandler = handler;
}
private String downloadUriAsString()
throws IOException {
URL url = new URL(this.mURL);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
private static String userAgentString() {
String appName = (Leanplum.getContext() != null) ?
Util.getApplicationName(Leanplum.getContext()) + "/" + Util.getVersionName() : "websocket";
return appName + "(" + RequestOld.appId() + "; " + Constants.CLIENT + "; "
+ Constants.LEANPLUM_VERSION + "/" + Constants.LEANPLUM_PACKAGE_IDENTIFIER + ")";
}
private static String downloadUriAsString(final HttpUriRequest req)
throws IOException {
CloseableHttpClient client = HttpClients.createMinimal();
req.setHeader("User-Agent", userAgentString());
try {
InputStream inputStream = connection.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String tempStr;
StringBuffer stringBuffer = new StringBuffer();
while ((tempStr = bufferedReader.readLine()) != null) {
stringBuffer.append(tempStr);
}
bufferedReader.close();
inputStream.close();
return stringBuffer.toString();
HttpResponse res = client.execute(req);
return readToEnd(res.getEntity().getContent());
} finally {
if (connection != null) {
connection.disconnect();
}
client.close();
}
}
@ -244,9 +239,9 @@ class SocketIOClient {
return;
new Thread() {
public void run() {
HttpPost post = new HttpPost(mURL);
try {
String line = downloadUriAsString();
String line = downloadUriAsString(post);
String[] parts = line.split(":");
mSession = parts[0];
String heartbeat = parts[1];

View File

@ -41,13 +41,13 @@ import android.support.annotation.RequiresPermission;
import android.text.TextUtils;
import android.util.TypedValue;
import com.google.android.gms.ads.identifier.AdvertisingIdClient;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumActivityHelper;
import com.leanplum.LeanplumDeviceIdMode;
import com.leanplum.LeanplumException;
import com.leanplum.internal.Constants.Methods;
import com.leanplum.internal.Constants.Params;
import com.leanplum.monitoring.ExceptionHandler;
import com.leanplum.utils.SharedPreferencesUtil;
import org.json.JSONException;
@ -203,14 +203,18 @@ public class Util {
*/
private static DeviceIdInfo getAdvertisingId(Context caller) throws Exception {
try {
AdvertisingIdClient.Info info = AdvertisingIdClient.getAdvertisingIdInfo(caller);
if (info != null) {
String advertisingId = info.getId();
String deviceId = checkDeviceId("advertising id", advertisingId);
if (deviceId != null) {
boolean limitedTracking = info.isLimitAdTrackingEnabled();
return new DeviceIdInfo(deviceId, limitedTracking);
}
// Using reflection because the app will either crash or print warnings
// if the app doesn't link to Google Play Services, even if this method is not called.
Object adInfo = Class.forName("com.google.android.gms.ads.identifier.AdvertisingIdClient")
.getMethod("getAdvertisingIdInfo", Context.class).invoke(null, caller);
String id = checkDeviceId(
"advertising id", (String) adInfo.getClass().getMethod("getId")
.invoke(adInfo));
if (id != null) {
boolean limitTracking = (Boolean) adInfo.getClass()
.getMethod("isLimitAdTrackingEnabled").invoke(adInfo);
Log.v("Using advertising device id: " + id);
return new DeviceIdInfo(id, limitTracking);
}
} catch (Throwable t) {
Log.e("Error getting advertising ID. Google Play Services are not available: ", t);
@ -450,7 +454,7 @@ public class Util {
Uri.Builder builder = new Uri.Builder();
for (Map.Entry<String, Object> pair : params.entrySet()) {
if (pair.getValue() == null) {
Log.w("Request parameter for key: " + pair.getKey() + " is null.");
Log.w("RequestOld parameter for key: " + pair.getKey() + " is null.");
continue;
}
builder.appendQueryParameter(pair.getKey(), pair.getValue().toString());
@ -550,7 +554,7 @@ public class Util {
urlConnection.setInstanceFollowRedirects(true);
Context context = Leanplum.getContext();
urlConnection.setRequestProperty("User-Agent",
getApplicationName(context) + "/" + getVersionName() + "/" + Request.appId() + "/" +
getApplicationName(context) + "/" + getVersionName() + "/" + RequestOld.appId() + "/" +
Constants.CLIENT + "/" + Constants.LEANPLUM_VERSION + "/" + getSystemName() + "/" +
getSystemVersion() + "/" + Constants.LEANPLUM_PACKAGE_IDENTIFIER);
return urlConnection;
@ -834,10 +838,19 @@ public class Util {
}
}
/**
* Initialize exception handling in the SDK.
*/
public static void initExceptionHandling(Context context) {
ExceptionHandler.getInstance().setContext(context);
}
/**
* Handles uncaught exceptions in the SDK.
*/
public static void handleException(Throwable t) {
ExceptionHandler.getInstance().reportException(t);
if (t instanceof OutOfMemoryError) {
if (Constants.isDevelopmentModeEnabled) {
throw (OutOfMemoryError) t;
@ -880,7 +893,7 @@ public class Util {
params.put("stackTrace", stringWriter.toString());
params.put(Params.VERSION_NAME, versionName);
Request.post(Methods.LOG, params).send();
RequestOld.post(Methods.LOG, params).send();
} catch (Throwable t2) {
Log.e("Unable to send error report.", t2);
}

View File

@ -87,6 +87,7 @@ public class VarCache {
private static boolean silent;
private static int contentVersion;
private static Map<String, Object> userAttributes;
private static Map<String, Object> variantDebugInfo = new HashMap<>();
private static final String NAME_COMPONENT_REGEX = "(?:[^\\.\\[.(\\\\]+|\\\\.)+";
private static final Pattern NAME_COMPONENT_PATTERN = Pattern.compile(NAME_COMPONENT_REGEX);
@ -322,25 +323,38 @@ public class VarCache {
return hasReceivedDiffs;
}
public static Map<String, Object> getVariantDebugInfo() {
return variantDebugInfo;
}
public static void setVariantDebugInfo(Map<String, Object> variantDebugInfo) {
if (variantDebugInfo != null) {
VarCache.variantDebugInfo = variantDebugInfo;
} else {
VarCache.variantDebugInfo = new HashMap<>();
}
}
public static void loadDiffs() {
if (Constants.isNoop()) {
return;
}
Context context = Leanplum.getContext();
SharedPreferences defaults = context.getSharedPreferences(LEANPLUM, Context.MODE_PRIVATE);
if (Request.token() == null) {
if (RequestOld.token() == null) {
applyVariableDiffs(
new HashMap<String, Object>(),
new HashMap<String, Object>(),
new ArrayList<Map<String, Object>>(),
new ArrayList<Map<String, Object>>(),
new HashMap<String, Object>(),
new ArrayList<Map<String, Object>>());
new ArrayList<Map<String, Object>>(),
new HashMap<String, Object>());
return;
}
try {
// Crypt functions return input text if there was a problem.
AESCrypt aesContext = new AESCrypt(Request.appId(), Request.token());
AESCrypt aesContext = new AESCrypt(RequestOld.appId(), RequestOld.token());
String variables = aesContext.decodePreference(
defaults, Constants.Defaults.VARIABLES_KEY, "{}");
String messages = aesContext.decodePreference(
@ -351,17 +365,19 @@ public class VarCache {
defaults, Constants.Defaults.EVENT_RULES_KEY, "[]");
String regions = aesContext.decodePreference(defaults, Constants.Defaults.REGIONS_KEY, "{}");
String variants = aesContext.decodePreference(defaults, Constants.Keys.VARIANTS, "[]");
String variantDebugInfo = aesContext.decodePreference(defaults, Constants.Keys.VARIANT_DEBUG_INFO, "{}");
applyVariableDiffs(
JsonConverter.fromJson(variables),
JsonConverter.fromJson(messages),
JsonConverter.<Map<String, Object>>listFromJson(new JSONArray(updateRules)),
JsonConverter.<Map<String, Object>>listFromJson(new JSONArray(eventRules)),
JsonConverter.fromJson(regions),
JsonConverter.<Map<String, Object>>listFromJson(new JSONArray(variants)));
JsonConverter.<Map<String, Object>>listFromJson(new JSONArray(variants)),
JsonConverter.fromJson(variantDebugInfo));
String deviceId = aesContext.decodePreference(defaults, Constants.Params.DEVICE_ID, null);
if (deviceId != null) {
if (Util.isValidDeviceId(deviceId)) {
Request.setDeviceId(deviceId);
RequestOld.setDeviceId(deviceId);
} else {
Log.w("Invalid stored device id found: \"" + deviceId + "\"; discarding.");
}
@ -369,7 +385,7 @@ public class VarCache {
String userId = aesContext.decodePreference(defaults, Constants.Params.USER_ID, null);
if (userId != null) {
if (Util.isValidUserId(userId)) {
Request.setUserId(userId);
RequestOld.setUserId(userId);
} else {
Log.w("Invalid stored user id found: \"" + userId + "\"; discarding.");
}
@ -383,13 +399,14 @@ public class VarCache {
Log.e("Could not load variable diffs.\n" + Log.getStackTraceString(e));
}
userAttributes();
Leanplum.countAggregator().incrementCount("load_diffs");
}
public static void saveDiffs() {
if (Constants.isNoop()) {
return;
}
if (Request.token() == null) {
if (RequestOld.token() == null) {
return;
}
Context context = Leanplum.getContext();
@ -397,7 +414,7 @@ public class VarCache {
SharedPreferences.Editor editor = defaults.edit();
// Crypt functions return input text if there was a problem.
AESCrypt aesContext = new AESCrypt(Request.appId(), Request.token());
AESCrypt aesContext = new AESCrypt(RequestOld.appId(), RequestOld.token());
String variablesCipher = aesContext.encrypt(JsonConverter.toJson(diffs));
editor.putString(Constants.Defaults.VARIABLES_KEY, variablesCipher);
@ -405,17 +422,21 @@ public class VarCache {
editor.putString(Constants.Defaults.MESSAGES_KEY, messagesCipher);
try {
String updateRulesCipher = aesContext.encrypt(
JsonConverter.listToJsonArray(updateRuleDiffs).toString());
editor.putString(Constants.Defaults.UPDATE_RULES_KEY, updateRulesCipher);
if (updateRuleDiffs != null && !updateRuleDiffs.isEmpty()) {
String updateRulesCipher = aesContext.encrypt(
JsonConverter.listToJsonArray(updateRuleDiffs).toString());
editor.putString(Constants.Defaults.UPDATE_RULES_KEY, updateRulesCipher);
}
} catch (JSONException e) {
Log.e("Error converting updateRuleDiffs to JSON", e);
}
try {
String eventRulesCipher = aesContext.encrypt(
JsonConverter.listToJsonArray(eventRuleDiffs).toString());
editor.putString(Constants.Defaults.EVENT_RULES_KEY, eventRulesCipher);
if (eventRuleDiffs != null && !eventRuleDiffs.isEmpty()) {
String eventRulesCipher = aesContext.encrypt(
JsonConverter.listToJsonArray(eventRuleDiffs).toString());
editor.putString(Constants.Defaults.EVENT_RULES_KEY, eventRulesCipher);
}
} catch (JSONException e) {
Log.e("Error converting eventRuleDiffs to JSON", e);
}
@ -424,16 +445,27 @@ public class VarCache {
editor.putString(Constants.Defaults.REGIONS_KEY, regionsCipher);
try {
String variantsJson = JsonConverter.listToJsonArray(variants).toString();
editor.putString(Constants.Keys.VARIANTS, aesContext.encrypt(variantsJson));
if (variants != null && !variants.isEmpty()) {
String variantsJson = JsonConverter.listToJsonArray(variants).toString();
editor.putString(Constants.Keys.VARIANTS, aesContext.encrypt(variantsJson));
}
} catch (JSONException e1) {
Log.e("Error converting " + variants + " to JSON.\n" + Log.getStackTraceString(e1));
}
editor.putString(Constants.Params.DEVICE_ID, aesContext.encrypt(Request.deviceId()));
editor.putString(Constants.Params.USER_ID, aesContext.encrypt(Request.userId()));
if (variantDebugInfo != null) {
editor.putString(
Constants.Keys.VARIANT_DEBUG_INFO,
aesContext.encrypt(JsonConverter.toJson(variantDebugInfo)));
}
editor.putString(Constants.Params.DEVICE_ID, aesContext.encrypt(RequestOld.deviceId()));
editor.putString(Constants.Params.USER_ID, aesContext.encrypt(RequestOld.userId()));
editor.putString(Constants.Keys.LOGGING_ENABLED,
aesContext.encrypt(String.valueOf(Constants.loggingEnabled)));
SharedPreferencesUtil.commitChanges(editor);
Leanplum.countAggregator().incrementCount("send_diffs");
}
/**
@ -467,9 +499,9 @@ public class VarCache {
}
String overrideFile = var.stringValue;
if (var.isResource && Constants.Kinds.FILE.equals(var.kind()) && overrideFile != null &&
!overrideFile.equals(var.defaultValue())) {
!overrideFile.equals(var.defaultValue())) {
Map<String, Object> variationAttributes = CollectionUtil.uncheckedCast(fileAttributes.get
(overrideFile));
(overrideFile));
InputStream stream = fileStreams.get(overrideFile);
if (variationAttributes != null && stream != null) {
var.setOverrideResId(getResIdFromPath(var.stringValue()));
@ -484,7 +516,8 @@ public class VarCache {
List<Map<String, Object>> updateRules,
List<Map<String, Object>> eventRules,
Map<String, Object> regions,
List<Map<String, Object>> variants) {
List<Map<String, Object>> variants,
Map<String, Object> variantDebugInfo) {
if (diffs != null) {
VarCache.diffs = diffs;
computeMergedDictionary();
@ -561,6 +594,10 @@ public class VarCache {
VarCache.variants = variants;
}
if (variantDebugInfo != null) {
VarCache.setVariantDebugInfo(variantDebugInfo);
}
contentVersion++;
if (!silent) {
@ -575,6 +612,7 @@ public class VarCache {
eventsUpdateBlock.updateCache();
}
}
Leanplum.countAggregator().incrementCount("apply_variable_diffs");
}
static void applyUpdateRuleDiffs(List<Map<String, Object>> updateRuleDiffs) {
@ -668,7 +706,7 @@ public class VarCache {
params.put(Constants.Params.ACTION_DEFINITIONS, JsonConverter.toJson(actionDefinitions));
}
params.put(Constants.Params.FILE_ATTRIBUTES, JsonConverter.toJson(fileAttributes));
Request.post(Constants.Methods.SET_VARS, params).sendIfConnected();
RequestOld.post(Constants.Methods.SET_VARS, params).sendIfConnected();
}
return changed;
@ -713,7 +751,7 @@ public class VarCache {
Map<String, Object> params = new HashMap<>();
params.put(Constants.Params.DATA, fileData.toString());
Request.post(Constants.Methods.UPLOAD_FILE, params).sendFilesNow(filenames,
RequestOld.post(Constants.Methods.UPLOAD_FILE, params).sendFilesNow(filenames,
streams);
filenames = new ArrayList<>();
@ -744,7 +782,7 @@ public class VarCache {
if (filenames.size() > 0) {
Map<String, Object> params = new HashMap<>();
params.put(Constants.Params.DATA, fileData.toString());
Request.post(Constants.Methods.UPLOAD_FILE, params).sendFilesNow(filenames, streams);
RequestOld.post(Constants.Methods.UPLOAD_FILE, params).sendFilesNow(filenames, streams);
}
}
@ -769,14 +807,17 @@ public class VarCache {
public static void onUpdate(CacheUpdateBlock block) {
updateBlock = block;
Leanplum.countAggregator().incrementCount("on_update_varcache");
}
public static void onInterfaceUpdate(CacheUpdateBlock block) {
interfaceUpdateBlock = block;
Leanplum.countAggregator().incrementCount("on_interface_update");
}
public static void onEventsUpdate(CacheUpdateBlock block) {
eventsUpdateBlock = block;
Leanplum.countAggregator().incrementCount("on_events_update");
}
public static List<Map<String, Object>> variants() {
@ -841,7 +882,7 @@ public class VarCache {
if (userAttributes == null) {
Context context = Leanplum.getContext();
SharedPreferences defaults = context.getSharedPreferences(LEANPLUM, Context.MODE_PRIVATE);
AESCrypt aesContext = new AESCrypt(Request.appId(), Request.token());
AESCrypt aesContext = new AESCrypt(RequestOld.appId(), RequestOld.token());
try {
userAttributes = JsonConverter.fromJson(
aesContext.decodePreference(defaults, Constants.Defaults.ATTRIBUTES_KEY, "{}"));
@ -854,7 +895,7 @@ public class VarCache {
}
public static void saveUserAttributes() {
if (Constants.isNoop() || Request.appId() == null || userAttributes == null) {
if (Constants.isNoop() || RequestOld.appId() == null || userAttributes == null) {
return;
}
Context context = Leanplum.getContext();
@ -862,9 +903,27 @@ public class VarCache {
SharedPreferences.Editor editor = defaults.edit();
// Crypt functions return input text if there was a problem.
String plaintext = JsonConverter.toJson(userAttributes);
AESCrypt aesContext = new AESCrypt(Request.appId(), Request.token());
AESCrypt aesContext = new AESCrypt(RequestOld.appId(), RequestOld.token());
editor.putString(Constants.Defaults.ATTRIBUTES_KEY, aesContext.encrypt(plaintext));
SharedPreferencesUtil.commitChanges(editor);
Leanplum.countAggregator().incrementCount("save_user_attributes");
}
public static void clearUserContent() {
vars.clear();
variants.clear();
variantDebugInfo.clear();
diffs.clear();
messageDiffs.clear();
messages = null;
userAttributes = null;
merged = null;
devModeValuesFromServer = null;
devModeFileAttributesFromServer = null;
devModeActionDefinitionsFromServer = null;
}
/**
@ -872,6 +931,7 @@ public class VarCache {
*/
public static void reset() {
vars.clear();
variantDebugInfo.clear();
fileAttributes.clear();
fileStreams.clear();
valuesFromClient.clear();

View File

@ -27,6 +27,14 @@ import android.os.HandlerThread;
import android.text.TextUtils;
import android.util.Base64;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpException;
import ch.boye.httpclientandroidlib.HttpStatus;
import ch.boye.httpclientandroidlib.NameValuePair;
import ch.boye.httpclientandroidlib.StatusLine;
import ch.boye.httpclientandroidlib.client.HttpResponseException;
import ch.boye.httpclientandroidlib.message.BasicLineParser;
import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
import java.io.EOFException;
import java.io.IOException;
@ -45,15 +53,6 @@ import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import ch.boye.httpclientandroidlib.Header;
import ch.boye.httpclientandroidlib.HttpException;
import ch.boye.httpclientandroidlib.HttpStatus;
import ch.boye.httpclientandroidlib.NameValuePair;
import ch.boye.httpclientandroidlib.StatusLine;
import ch.boye.httpclientandroidlib.client.HttpResponseException;
import ch.boye.httpclientandroidlib.message.BasicLineParser;
import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
// Suppressing deprecated apache dependency.
@SuppressWarnings("deprecation")
class WebSocketClient {

View File

@ -66,56 +66,6 @@ import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Map;
/**
* Base dialog used to display the Center Popup, Interstitial, Web Interstitial, HTML template.
*
* @author Martin Yanakiev, Anna Orlova
*/
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Typeface;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.graphics.drawable.shapes.Shape;
import android.os.Build;
import android.os.Handler;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.WindowManager;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.leanplum.ActionContext;
import com.leanplum.Leanplum;
import com.leanplum.utils.BitmapUtil;
import com.leanplum.utils.SizeUtil;
import com.leanplum.views.BackgroundImageView;
import com.leanplum.views.CloseButton;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Map;
/**
* Base dialog used to display the Center Popup, Interstitial, Web Interstitial, HTML template.
*
@ -134,7 +84,7 @@ public class BaseMessageDialog extends Dialog {
private boolean isClosing = false;
protected BaseMessageDialog(Activity activity, boolean fullscreen, BaseMessageOptions options,
WebInterstitialOptions webOptions, HTMLOptions htmlOptions) {
WebInterstitialOptions webOptions, HTMLOptions htmlOptions) {
super(activity, getTheme(activity));
SizeUtil.init(activity);
@ -150,7 +100,7 @@ public class BaseMessageDialog extends Dialog {
}
dialogView = new RelativeLayout(activity);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
dialogView.setBackgroundColor(Color.TRANSPARENT);
dialogView.setLayoutParams(layoutParams);
@ -180,9 +130,9 @@ public class BaseMessageDialog extends Dialog {
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
if (htmlOptions != null &&
MessageTemplates.Args.HTML_ALIGN_BOTTOM.equals(htmlOptions.getHtmlAlign())) {
MessageTemplates.Args.HTML_ALIGN_BOTTOM.equals(htmlOptions.getHtmlAlign())) {
dialogView.setGravity(Gravity.BOTTOM);
}
}
@ -261,7 +211,7 @@ public class BaseMessageDialog extends Dialog {
CloseButton closeButton = new CloseButton(context);
closeButton.setId(103);
RelativeLayout.LayoutParams closeLayout = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
if (fullscreen) {
closeLayout.addRule(RelativeLayout.ALIGN_PARENT_TOP, dialogView.getId());
closeLayout.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, dialogView.getId());
@ -289,13 +239,13 @@ public class BaseMessageDialog extends Dialog {
RelativeLayout.LayoutParams layoutParams;
if (fullscreen) {
layoutParams = new RelativeLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
} else if (isHtml) {
int height = SizeUtil.dpToPx(context, htmlOptions.getHtmlHeight());
HTMLOptions.Size htmlWidth = htmlOptions.getHtmlWidth();
if (htmlWidth == null || TextUtils.isEmpty(htmlWidth.type)) {
layoutParams = new RelativeLayout.LayoutParams(
LayoutParams.MATCH_PARENT, height);
LayoutParams.MATCH_PARENT, height);
} else {
int width = htmlWidth.value;
if ("%".equals(htmlWidth.type)) {
@ -361,9 +311,9 @@ public class BaseMessageDialog extends Dialog {
View message = createMessageView(context);
((RelativeLayout.LayoutParams) message.getLayoutParams())
.addRule(RelativeLayout.BELOW, title.getId());
.addRule(RelativeLayout.BELOW, title.getId());
((RelativeLayout.LayoutParams) message.getLayoutParams())
.addRule(RelativeLayout.ABOVE, button.getId());
.addRule(RelativeLayout.ABOVE, button.getId());
view.addView(message, message.getLayoutParams());
} else if (isWeb) {
WebView webView = createWebView(context);
@ -403,7 +353,7 @@ public class BaseMessageDialog extends Dialog {
view.setBackgroundDrawable(footerBackground);
}
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
view.setLayoutParams(layoutParams);
return view;
}
@ -411,7 +361,7 @@ public class BaseMessageDialog extends Dialog {
private RelativeLayout createTitleView(Context context) {
RelativeLayout view = new RelativeLayout(context);
view.setLayoutParams(new LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
TextView title = new TextView(context);
title.setPadding(0, SizeUtil.dp5, 0, SizeUtil.dp5);
@ -421,7 +371,7 @@ public class BaseMessageDialog extends Dialog {
title.setTextSize(TypedValue.COMPLEX_UNIT_SP, SizeUtil.textSize0);
title.setTypeface(null, Typeface.BOLD);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
layoutParams.addRule(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE);
layoutParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
title.setLayoutParams(layoutParams);
@ -433,7 +383,7 @@ public class BaseMessageDialog extends Dialog {
private TextView createMessageView(Context context) {
TextView view = new TextView(context);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
view.setLayoutParams(layoutParams);
view.setGravity(Gravity.CENTER);
view.setText(options.getMessageText());
@ -445,7 +395,7 @@ public class BaseMessageDialog extends Dialog {
private WebView createWebView(Context context) {
WebView view = new WebView(context);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
view.setLayoutParams(layoutParams);
view.setWebViewClient(new WebViewClient() {
@SuppressWarnings("deprecation")
@ -507,7 +457,6 @@ public class BaseMessageDialog extends Dialog {
webViewSettings.setMediaPlaybackRequiresUserGesture(false);
}
webViewSettings.setAppCacheEnabled(true);
webViewSettings.getSaveFormData();
webViewSettings.setAllowFileAccess(true);
webViewSettings.setJavaScriptEnabled(true);
webViewSettings.setDomStorageEnabled(true);
@ -525,7 +474,7 @@ public class BaseMessageDialog extends Dialog {
webViewSettings.setSupportZoom(false);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
webView.setLayoutParams(layoutParams);
final Dialog currentDialog = this;
webView.setWebChromeClient(new WebChromeClient());
@ -563,7 +512,7 @@ public class BaseMessageDialog extends Dialog {
try {
paramsMap = ActionContext.mapFromJson(new JSONObject(queryComponentsFromUrl(url,
"parameters")));
"parameters")));
} catch (Exception ignored) {
}
@ -579,7 +528,7 @@ public class BaseMessageDialog extends Dialog {
// Action URL or track action URL event.
if (url.contains(htmlOptions.getActionUrl()) ||
url.contains(htmlOptions.getTrackActionUrl())) {
url.contains(htmlOptions.getTrackActionUrl())) {
cancel();
String queryComponentsFromUrl = queryComponentsFromUrl(url, "action");
try {
@ -638,7 +587,7 @@ public class BaseMessageDialog extends Dialog {
private TextView createAcceptButton(Context context) {
TextView view = new TextView(context);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
layoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE);
layoutParams.addRule(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE);
layoutParams.setMargins(0, 0, 0, SizeUtil.dp5);
@ -650,7 +599,7 @@ public class BaseMessageDialog extends Dialog {
view.setTypeface(null, Typeface.BOLD);
BitmapUtil.stateBackgroundDarkerByPercentage(view,
options.getAcceptButtonBackgroundColor(), 30);
options.getAcceptButtonBackgroundColor(), 30);
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, SizeUtil.textSize0_1);
view.setOnClickListener(new View.OnClickListener() {
@ -667,7 +616,7 @@ public class BaseMessageDialog extends Dialog {
private static int getTheme(Activity activity) {
boolean full = (activity.getWindow().getAttributes().flags &
WindowManager.LayoutParams.FLAG_FULLSCREEN) == WindowManager.LayoutParams.FLAG_FULLSCREEN;
WindowManager.LayoutParams.FLAG_FULLSCREEN) == WindowManager.LayoutParams.FLAG_FULLSCREEN;
if (full) {
return android.R.style.Theme_Translucent_NoTitleBar_Fullscreen;
} else {

View File

@ -26,6 +26,7 @@ import android.graphics.Point;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.MotionEvent;
import com.leanplum.ActionContext;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumActivityHelper;
@ -50,32 +51,32 @@ public class HTMLTemplate extends BaseMessageDialog {
@Override
public boolean dispatchTouchEvent(@NonNull MotionEvent ev) {
if (!htmlOptions.isFullScreen()) {
Point size = SizeUtil.getDisplaySize(activity);
int dialogWidth = webView.getWidth();
int left = (size.x - dialogWidth) / 2;
int right = (size.x + dialogWidth) / 2;
int height = SizeUtil.dpToPx(Leanplum.getContext(), htmlOptions.getHtmlHeight());
int statusBarHeight = SizeUtil.getStatusBarHeight(Leanplum.getContext());
int htmlYOffset = htmlOptions.getHtmlYOffset(activity);
int top;
int bottom;
if (MessageTemplates.Args.HTML_ALIGN_BOTTOM.equals(htmlOptions.getHtmlAlign())) {
top = size.y - height - statusBarHeight - htmlYOffset;
bottom = size.y - htmlYOffset - statusBarHeight;
} else {
top = htmlYOffset + statusBarHeight;
bottom = height + statusBarHeight + htmlYOffset;
}
if (ev.getY() < top || ev.getY() > bottom || ev.getX() < left || ev.getX() > right) {
if (htmlOptions.isHtmlTabOutsideToClose()) {
cancel();
}
activity.dispatchTouchEvent(ev);
}
if (!htmlOptions.isFullScreen()) {
Point size = SizeUtil.getDisplaySize(activity);
int dialogWidth = webView.getWidth();
int left = (size.x - dialogWidth) / 2;
int right = (size.x + dialogWidth) / 2;
int height = SizeUtil.dpToPx(Leanplum.getContext(), htmlOptions.getHtmlHeight());
int statusBarHeight = SizeUtil.getStatusBarHeight(Leanplum.getContext());
int htmlYOffset = htmlOptions.getHtmlYOffset(activity);
int top;
int bottom;
if (MessageTemplates.Args.HTML_ALIGN_BOTTOM.equals(htmlOptions.getHtmlAlign())) {
top = size.y - height - statusBarHeight - htmlYOffset;
bottom = size.y - htmlYOffset - statusBarHeight;
} else {
top = htmlYOffset + statusBarHeight;
bottom = height + statusBarHeight + htmlYOffset;
}
return super.dispatchTouchEvent(ev);
if (ev.getY() < top || ev.getY() > bottom || ev.getX() < left || ev.getX() > right) {
if (htmlOptions.isHtmlTabOutsideToClose()) {
cancel();
}
activity.dispatchTouchEvent(ev);
}
}
return super.dispatchTouchEvent(ev);
}
public static void register() {

View File

@ -89,18 +89,18 @@ public class MessageTemplates {
// Open URL.
static final String DEFAULT_URL = "http://www.example.com";
static final String DEFAULT_BASE_URL = "http://leanplum/";
// Web interstitial values.
static final String DEFAULT_CLOSE_URL = "http://leanplum:close";
static final String DEFAULT_CLOSE_URL = DEFAULT_BASE_URL + "close";
static final boolean DEFAULT_HAS_DISMISS_BUTTON = true;
// HTML Template values.
public static final String FILE_PREFIX = "__file__";
public static final String HTML_TEMPLATE_PREFIX = "__file__Template";
static final String DEFAULT_OPEN_URL = "http://leanplum:loadFinished";
static final String DEFAULT_TRACK_URL = "http://leanplum:track";
static final String DEFAULT_ACTION_URL = "http://leanplum:runAction";
static final String DEFAULT_TRACK_ACTION_URL = "http://leanplum:runTrackedAction";
static final String DEFAULT_OPEN_URL = DEFAULT_BASE_URL + "loadFinished";
static final String DEFAULT_TRACK_URL = DEFAULT_BASE_URL + "track";
static final String DEFAULT_ACTION_URL = DEFAULT_BASE_URL + "runAction";
static final String DEFAULT_TRACK_ACTION_URL = DEFAULT_BASE_URL + "runTrackedAction";
}

View File

@ -80,9 +80,8 @@ class OpenURL {
for (ResolveInfo resolveInfo : resolveInfoList) {
if (resolveInfo != null && resolveInfo.activityInfo != null &&
resolveInfo.activityInfo.name != null) {
final String contextPackageName = context.getPackageName();
if (resolveInfo.activityInfo.name.contains(contextPackageName) ||
resolveInfo.activityInfo.packageName.contains(contextPackageName)) {
if (resolveInfo.activityInfo.name.contains(
context.getPackageName())) {
uriIntent.setPackage(resolveInfo.activityInfo.packageName);
}
}

View File

@ -0,0 +1,19 @@
package com.leanplum.models;
import android.support.annotation.NonNull;
import java.util.Date;
public class MessageArchiveData {
@NonNull public String messageID;
@NonNull public String messageBody;
@NonNull public String recipientUserID;
@NonNull public Date deliveryDateTime;
public MessageArchiveData(String messageID, String messageBody, String recipientUserID, Date deliveryDateTime) {
this.messageID = messageID;
this.messageBody = messageBody;
this.recipientUserID = recipientUserID;
this.deliveryDateTime = deliveryDateTime;
}
}

View File

@ -0,0 +1,51 @@
package com.leanplum.monitoring;
import android.content.Context;
import com.leanplum.Leanplum;
import com.leanplum.internal.Log;
public class ExceptionHandler {
private static final String LEANPLUM_CRASH_REPORTER_CLASS =
"com.leanplum.monitoring.internal.LeanplumExceptionReporter";
private static final ExceptionHandler instance = new ExceptionHandler();
public ExceptionReporting exceptionReporter = null;
private ExceptionHandler() {}
public static ExceptionHandler getInstance() {
return instance;
}
public void setContext(Context context) {
try {
// Class.forName runs the static initializer in LeanplumExceptionReporter
// which sets the exceptionReporter on the singleton
Class.forName(LEANPLUM_CRASH_REPORTER_CLASS);
if (exceptionReporter != null) {
try {
exceptionReporter.setContext(context);
} catch (Throwable t) {
Log.e("LeanplumExceptionHandler", t);
}
}
} catch (ClassNotFoundException t) {
Log.i("LeanplumExceptionHandler could not initialize Exception Reporting." +
"This is expected if you have not included the leanplum-monitoring module");
} catch (Throwable t) {
Log.e("LeanplumExceptionHandler", t);
}
}
public void reportException(Throwable exception) {
if (exceptionReporter != null) {
try {
exceptionReporter.reportException(exception);
} catch (Throwable t) {
Log.e("LeanplumExceptionHandler", t);
}
Leanplum.countAggregator().incrementCount("report_exception");
}
}
}

View File

@ -0,0 +1,8 @@
package com.leanplum.monitoring;
import android.content.Context;
public interface ExceptionReporting {
void setContext(Context context);
void reportException(Throwable t);
}

View File

@ -1,5 +1,3 @@
package com.leanplum.utils;
/*
* Copyright 2017, Leanplum, Inc. All rights reserved.
*
@ -20,6 +18,7 @@ package com.leanplum.utils;
* specific language governing permissions and limitations
* under the License.
*/
package com.leanplum.utils;
import android.content.Context;
import android.os.Build;
@ -30,28 +29,28 @@ import android.os.Build;
* @author Anna Orlova
*/
public class BuildUtil {
private static int targetSdk = -1;
private static int targetSdk = -1;
/**
* Whether notification channels are supported.
*
* @param context The application context.
* @return True if notification channels are supported, false otherwise.
*/
public static boolean isNotificationChannelSupported(Context context) {
return Build.VERSION.SDK_INT >= 26 && getTargetSdkVersion(context) >= 26;
}
/**
* Whether notification channels are supported.
*
* @param context The application context.
* @return True if notification channels are supported, false otherwise.
*/
public static boolean isNotificationChannelSupported(Context context) {
return Build.VERSION.SDK_INT >= 26 && getTargetSdkVersion(context) >= 26;
}
/**
* Returns target SDK version parsed from manifest.
*
* @param context The application context.
* @return Target SDK version.
*/
private static int getTargetSdkVersion(Context context) {
if (targetSdk == -1 && context != null) {
targetSdk = context.getApplicationInfo().targetSdkVersion;
}
return targetSdk;
/**
* Returns target SDK version parsed from manifest.
*
* @param context The application context.
* @return Target SDK version.
*/
private static int getTargetSdkVersion(Context context) {
if (targetSdk == -1 && context != null) {
targetSdk = context.getApplicationInfo().targetSdkVersion;
}
}
return targetSdk;
}
}