diff --git a/CHANGELOG.md b/CHANGELOG.md index e660b4265..b7ddf6ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 2018-01-03 - v4.0.3 + +#### Bugs Fixed + +* Fixed a potential crash when used in Instant Apps that don't contain a launcher Activity. +* Don't send payloads when the app is in the background. +* Don't poll for messages when the app is in the background. + # 2017-08-15 - v4.0.2 #### Bugs Fixed diff --git a/README.md b/README.md index 2eddd5c86..823cb9556 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ use your app, to talk to them at the right time, and in the right way. ##### [Release Notes](https://learn.apptentive.com/knowledge-base/android-sdk-release-notes/) -##### Binary releases are hosted for Maven [here](http://search.maven.org/#artifactdetails|com.apptentive|apptentive-android|4.0.2|aar) +##### Binary releases are hosted for Maven [here](http://search.maven.org/#artifactdetails|com.apptentive|apptentive-android|4.0.3|aar) #### Reporting Bugs diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveLogTag.java b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveLogTag.java index 69a54a7fa..29f6d06a8 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveLogTag.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveLogTag.java @@ -2,6 +2,7 @@ public enum ApptentiveLogTag { NETWORK(true), + APP_CONFIGURATION(true), CONVERSATION(true), NOTIFICATIONS(true), MESSAGES(true), diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveViewActivity.java b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveViewActivity.java index 57404af4d..f5f3a833f 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveViewActivity.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveViewActivity.java @@ -330,10 +330,17 @@ private void startLauncherActivityIfRoot() { if (isTaskRoot()) { PackageManager packageManager = getPackageManager(); Intent intent = packageManager.getLaunchIntentForPackage(getPackageName()); - ComponentName componentName = intent.getComponent(); - /** Backwards compatible method that will clear all activities in the stack. */ - Intent mainIntent = IntentCompat.makeRestartActivityTask(componentName); - startActivity(mainIntent); + /* + Make this work with Instant Apps. It is possible and even likely to create an Instant App + that doesn't have the Main Activity included in its APK. In such cases, this Intent is null, + and we can't do anything apart from exiting our Activity. + */ + if (intent != null) { + ComponentName componentName = intent.getComponent(); + // Backwards compatible method that will clear all activities in the stack. + Intent mainIntent = Intent.makeRestartActivityTask(componentName); + startActivity(mainIntent); + } } } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/comm/ApptentiveHttpClient.java b/apptentive/src/main/java/com/apptentive/android/sdk/comm/ApptentiveHttpClient.java index fe30a945c..47c90978d 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/comm/ApptentiveHttpClient.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/comm/ApptentiveHttpClient.java @@ -40,6 +40,7 @@ public class ApptentiveHttpClient implements PayloadRequestSender { // Active API private static final String ENDPOINT_CONVERSATION = "/conversation"; + private static final String ENDPOINT_CONFIGURATION = "/conversations/%s/configuration"; private static final String ENDPOINT_LEGACY_CONVERSATION = "/conversation/token"; private static final String ENDPOINT_LOG_IN_TO_EXISTING_CONVERSATION = "/conversations/%s/session"; private static final String ENDPOINT_LOG_IN_TO_NEW_CONVERSATION = "/conversations"; @@ -112,6 +113,22 @@ public HttpJsonRequest createLoginRequest(String conversationId, String token, H return request; } + public HttpJsonRequest createAppConfigurationRequest(String conversationId, String token, HttpRequest.Listener listener) { + if (StringUtils.isNullOrEmpty(conversationId)) { + throw new IllegalArgumentException("Conversation id is null or empty"); + } + + if (StringUtils.isNullOrEmpty(token)) { + throw new IllegalArgumentException("Conversation token is null or empty"); + } + + String endPoint = StringUtils.format(ENDPOINT_CONFIGURATION, conversationId); + HttpJsonRequest request = createJsonRequest(endPoint, new JSONObject(), HttpRequestMethod.GET); + request.setRequestProperty("Authorization", "Bearer " + token); + request.addListener(listener); + return request; + } + public HttpJsonRequest createFirstLoginRequest(String token, AppRelease appRelease, Sdk sdk, Device device, HttpRequest.Listener listener) { if (token == null) { throw new IllegalArgumentException("Token is null"); diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/conversation/ConversationManager.java b/apptentive/src/main/java/com/apptentive/android/sdk/conversation/ConversationManager.java index 1e68d09e3..4cca59b97 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/conversation/ConversationManager.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/conversation/ConversationManager.java @@ -16,7 +16,7 @@ import com.apptentive.android.sdk.comm.ApptentiveHttpClient; import com.apptentive.android.sdk.conversation.ConversationMetadata.Filter; import com.apptentive.android.sdk.migration.Migrator; -import com.apptentive.android.sdk.model.ConversationItem; +import com.apptentive.android.sdk.model.Configuration; import com.apptentive.android.sdk.model.ConversationTokenRequest; import com.apptentive.android.sdk.module.engagement.EngagementModule; import com.apptentive.android.sdk.network.HttpJsonRequest; @@ -35,6 +35,7 @@ import com.apptentive.android.sdk.util.Constants; import com.apptentive.android.sdk.util.Jwt; import com.apptentive.android.sdk.util.ObjectUtils; +import com.apptentive.android.sdk.util.RuntimeUtils; import com.apptentive.android.sdk.util.StringUtils; import com.apptentive.android.sdk.util.Util; import com.apptentive.android.sdk.util.threading.DispatchQueue; @@ -67,10 +68,14 @@ public class ConversationManager { protected static final String CONVERSATION_METADATA_PATH = "conversation-v1.meta"; + private static final String TAG_FETCH_CONVERSATION_TOKEN_REQUEST = "fetch_conversation_token"; + private static final String TAG_FETCH_APP_CONFIGURATION_REQUEST = "fetch_app_configuration"; private final WeakReference contextRef; + private boolean appIsInForeground; + /** * A basic directory for storing conversation-related data. */ @@ -96,17 +101,28 @@ public ConversationManager(Context context, File apptentiveConversationsStorageD @Override public void onReceiveNotification(ApptentiveNotification notification) { assertMainThread(); + appIsInForeground = true; if (activeConversation != null && activeConversation.hasActiveState()) { - ApptentiveLog.v(CONVERSATION, "App entered foreground notification received. Trying to fetch interactions..."); + ApptentiveLog.v(CONVERSATION, "App entered foreground notification received. Trying to fetch app configuration and interactions..."); final Context context = getContext(); if (context != null) { + fetchAppConfiguration(activeConversation); activeConversation.fetchInteractions(context); } else { - ApptentiveLog.w(CONVERSATION, "Can't fetch conversation interactions: context is lost"); + ApptentiveLog.w(CONVERSATION, "Can't fetch app configuration and conversation interactions: context is lost"); } } } }); + + ApptentiveNotificationCenter.defaultCenter() + .addObserver(NOTIFICATION_APP_ENTERED_BACKGROUND, new ApptentiveNotificationObserver() { + @Override + public void onReceiveNotification(ApptentiveNotification notification) { + assertMainThread(); + appIsInForeground = false; + } + }); } //region Conversations @@ -450,8 +466,15 @@ private void handleConversationStateChange(Conversation conversation) { ObjectUtils.toMap(NOTIFICATION_KEY_CONVERSATION, conversation)); if (conversation.hasActiveState()) { - conversation.fetchInteractions(getContext()); - conversation.getMessageManager().startPollingMessages(); + if (appIsInForeground) { + // ConversationManager listens to the foreground event to fetch interactions when it comes to foreground + conversation.fetchInteractions(getContext()); + // Message Manager listens to foreground/background events itself + conversation.getMessageManager().attemptToStartMessagePolling(); + } + + // Fetch app configuration + fetchAppConfiguration(conversation); // Update conversation with push configuration changes that happened while it wasn't active. SharedPreferences prefs = ApptentiveInternal.getInstance().getGlobalSharedPrefs(); @@ -469,6 +492,65 @@ private void handleConversationStateChange(Conversation conversation) { } } + private void fetchAppConfiguration(Conversation conversation) { + try { + fetchAppConfigurationGuarded(conversation); + } catch (Exception e) { + ApptentiveLog.e(e, "Exception while fetching app configuration"); + } + } + + private void fetchAppConfigurationGuarded(Conversation conversation) { + ApptentiveLog.d(APP_CONFIGURATION, "Fetching app configuration..."); + + HttpRequest existingRequest = getHttpClient().findRequest(TAG_FETCH_APP_CONFIGURATION_REQUEST); + if (existingRequest != null) { + ApptentiveLog.d(APP_CONFIGURATION, "Can't fetch app configuration: another request already pending"); + return; + } + + if (!Configuration.load().hasConfigurationCacheExpired()) { + // if configuration hasn't expired we would fetch it anyway for debug apps + boolean debuggable = RuntimeUtils.isAppDebuggable(getContext()); + if (!debuggable) { + ApptentiveLog.d(APP_CONFIGURATION, "Can't fetch app configuration: the old configuration is still valid"); + return; + } + } + + HttpJsonRequest request = getHttpClient() + .createAppConfigurationRequest(conversation.getConversationId(), conversation.getConversationToken(), + new HttpRequest.Listener() { + @Override + public void onFinish(HttpJsonRequest request) { + try { + String cacheControl = request.getResponseHeader("Cache-Control"); + Integer cacheSeconds = Util.parseCacheControlHeader(cacheControl); + if (cacheSeconds == null) { + cacheSeconds = Constants.CONFIG_DEFAULT_APP_CONFIG_EXPIRATION_DURATION_SECONDS; + } + ApptentiveLog.d(APP_CONFIGURATION, "Caching configuration for %d seconds.", cacheSeconds); + Configuration config = new Configuration(request.getResponseObject().toString()); + config.setConfigurationCacheExpirationMillis(System.currentTimeMillis() + cacheSeconds * 1000); + config.save(); + } catch (Exception e) { + ApptentiveLog.e(e, "Exception while parsing app configuration response"); + } + } + + @Override + public void onCancel(HttpJsonRequest request) { + } + + @Override + public void onFail(HttpJsonRequest request, String reason) { + ApptentiveLog.e(APP_CONFIGURATION, "App configuration request failed: %s", reason); + } + }); + request.setTag(TAG_FETCH_APP_CONFIGURATION_REQUEST); + request.start(); + } + private void updateMetadataItems(Conversation conversation) { ApptentiveLog.vv("Updating metadata: state=%s localId=%s conversationId=%s token=%s", conversation.getState(), diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/MessageManager.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/MessageManager.java index 9e6e8c820..1b8db9293 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/MessageManager.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/MessageManager.java @@ -416,8 +416,10 @@ public void destroy() { //region Polling - public void startPollingMessages() { - pollingWorker.startPolling(); + public void attemptToStartMessagePolling() { + if (conversation.isMessageCenterFeatureUsed()) { + pollingWorker.startPolling(); + } } public void stopPollingMessages() { @@ -527,6 +529,7 @@ private void setCurrentForegroundActivity(Activity activity) { } public void setMessageCenterInForeground(boolean bInForeground) { + conversation.setMessageCenterFeatureUsed(true); pollingWorker.setMessageCenterInForeground(bInForeground); } @@ -563,7 +566,9 @@ protected void execute() { private void appWentToForeground() { appInForeground.set(true); - pollingWorker.appWentToForeground(); + if (conversation.isMessageCenterFeatureUsed()) { + pollingWorker.appWentToForeground(); + } } private void appWentToBackground() { diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/MessagePollingWorker.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/MessagePollingWorker.java index 910de485f..95121ae74 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/MessagePollingWorker.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/MessagePollingWorker.java @@ -41,6 +41,7 @@ class MessagePollingWorker implements Destroyable { conf = Configuration.load(); backgroundPollingInterval = conf.getMessageCenterBgPoll() * 1000; foregroundPollingInterval = conf.getMessageCenterFgPoll() * 1000; + ApptentiveLog.vv("Message Polling Worker: bg=%d, fg=%d", backgroundPollingInterval, foregroundPollingInterval); } @Override diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/network/HttpRequest.java b/apptentive/src/main/java/com/apptentive/android/sdk/network/HttpRequest.java index cd950c1a6..604c2d04d 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/network/HttpRequest.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/network/HttpRequest.java @@ -587,6 +587,10 @@ public int getResponseCode() { return responseCode; } + public String getResponseHeader(String key) { + return responseHeaders != null ? responseHeaders.get(key) : null; + } + public boolean isAuthenticationFailure() { return responseCode == 401; } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveTaskManager.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveTaskManager.java index 705c5d6b5..858bfbb2a 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveTaskManager.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveTaskManager.java @@ -55,7 +55,7 @@ public class ApptentiveTaskManager implements PayloadStore, EventStore, Apptenti private final ThreadPoolExecutor singleThreadExecutor; // TODO: replace with a private concurrent dispatch queue private final PayloadSender payloadSender; - private boolean appInBackground; + private boolean appInBackground = true; /* * Creates an asynchronous task manager with one worker thread. This constructor must be invoked on the UI thread. diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/Constants.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/Constants.java index 3f53ae04b..60d44768c 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/util/Constants.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/Constants.java @@ -9,7 +9,7 @@ public class Constants { public static final int API_VERSION = 9; - public static final String APPTENTIVE_SDK_VERSION = "4.0.2"; + public static final String APPTENTIVE_SDK_VERSION = "4.0.3"; public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 45000; public static final int DEFAULT_READ_TIMEOUT_MILLIS = 45000;