diff --git a/.travis.yml b/.travis.yml
index 967a3f2b8..73df64e76 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -20,14 +20,16 @@ android:
- tools
- platform-tools
- tools # not a mistakenly duplicated line: used above api 25.x
- - build-tools-26.0.3
+ - build-tools-27.0.3
- android-19
- - android-26
+ - android-27
- extra-google-google_play_services
- extra-google-m2repository
- extra-android-m2repository
- addon-google_apis-google-26
- sys-img-armeabi-v7a-android-19
+before_install:
+- yes | sdkmanager "platforms;android-27"
install: true
before_script:
- echo no | android create avd --force -n test -t android-19 --abi armeabi-v7a
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 021e11b5e..064586f04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+# 2018-05-01 - v5.1.0
+
+#### Major changes
+
+* Added support for notification-based interactions.
+
+#### Improvements
+
+* Better logging for interaction criteria evaluation.
+* Better troubleshooting support.
+
# 2018-04-19 - v5.0.5
#### Bugs Fixed
diff --git a/README.md b/README.md
index c4ab03f58..ccb3cb410 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|5.0.5|aar)
+##### Binary releases are hosted for Maven [here](http://search.maven.org/#artifactdetails|com.apptentive|apptentive-android|5.1.0|aar)
#### Reporting Bugs
diff --git a/apptentive/build.gradle b/apptentive/build.gradle
index 6d275615d..4b46841fa 100644
--- a/apptentive/build.gradle
+++ b/apptentive/build.gradle
@@ -20,8 +20,8 @@ dependencies {
}
android {
- compileSdkVersion 26
- buildToolsVersion '26.0.3'
+ compileSdkVersion 27
+ buildToolsVersion '27.0.3'
defaultConfig {
minSdkVersion 14
diff --git a/apptentive/src/androidTest/java/com/apptentive/android/sdk/InstrumentationTestCaseBase.java b/apptentive/src/androidTest/java/com/apptentive/android/sdk/InstrumentationTestCaseBase.java
new file mode 100644
index 000000000..5bb4489c5
--- /dev/null
+++ b/apptentive/src/androidTest/java/com/apptentive/android/sdk/InstrumentationTestCaseBase.java
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.test.RenamingDelegatingContext;
+
+public class InstrumentationTestCaseBase extends TestCaseBase {
+ protected Context getContext() {
+ return new RenamingDelegatingContext(InstrumentationRegistry.getTargetContext(), "test_");
+ }
+}
diff --git a/apptentive/src/androidTest/java/com/apptentive/android/sdk/conversation/FileMessageStoreTest.java b/apptentive/src/androidTest/java/com/apptentive/android/sdk/conversation/FileMessageStoreTest.java
index b405f5e15..2242fc3a6 100644
--- a/apptentive/src/androidTest/java/com/apptentive/android/sdk/conversation/FileMessageStoreTest.java
+++ b/apptentive/src/androidTest/java/com/apptentive/android/sdk/conversation/FileMessageStoreTest.java
@@ -39,7 +39,7 @@ public class FileMessageStoreTest extends TestCaseBase {
public TemporaryFolder tempFolder = new TemporaryFolder();
@Before
- public void setUp() {
+ public void setUp() throws Exception {
super.setUp();
ApptentiveInternal.setInstance(new ApptentiveInternal(InstrumentationRegistry.getTargetContext()));
}
diff --git a/apptentive/src/androidTest/java/com/apptentive/android/sdk/module/engagement/InteractionLauncherTest.java b/apptentive/src/androidTest/java/com/apptentive/android/sdk/module/engagement/InteractionLauncherTest.java
new file mode 100644
index 000000000..0c98baa1e
--- /dev/null
+++ b/apptentive/src/androidTest/java/com/apptentive/android/sdk/module/engagement/InteractionLauncherTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2016, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.module.engagement;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.apptentive.android.sdk.InstrumentationTestCaseBase;
+import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction;
+import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction.DisplayType;
+import com.apptentive.android.sdk.module.engagement.interaction.model.TextModalInteraction;
+import com.apptentive.android.sdk.util.RuntimeUtils;
+
+import org.json.JSONException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static junit.framework.Assert.assertEquals;
+
+@RunWith(AndroidJUnit4.class)
+public class InteractionLauncherTest extends InstrumentationTestCaseBase {
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ RuntimeUtils.overrideStaticFinalField(EngagementModule.class, "LAUNCHER_FACTORY", new DefaultInteractionLauncherFactory() {
+ @NonNull
+ @Override
+ protected InteractionLauncher createActivityInteractionLauncher() {
+ return new MockInteractionLauncher("Activity");
+ }
+
+ @NonNull
+ @Override
+ protected InteractionLauncher createNotificationInteractionLauncher() {
+ return new MockInteractionLauncher("Notification");
+ }
+ });
+
+ // Everything should run immediately
+ overrideMainQueue(true);
+ }
+
+ @Test
+ public void testInteractionDefaultDisplayType() throws JSONException {
+ Interaction interaction = new TextModalInteraction("{\"type\":\"TextModal\"}");
+ assertEquals(interaction.getDisplayType(), DisplayType.unknown);
+ EngagementModule.launchInteraction(getContext(), interaction);
+ assertResult("Activity");
+ }
+
+ @Test
+ public void testInteractionNotificationDisplayType() throws JSONException {
+ Interaction interaction = new TextModalInteraction("{\"type\":\"TextModal\",\"display_type\":\"notification\"}");
+ assertEquals(interaction.getDisplayType(), DisplayType.notification);
+ EngagementModule.launchInteraction(getContext(), interaction);
+ assertResult("Notification");
+ }
+
+ @Test
+ public void testInteractionUnknownDisplayType() throws JSONException {
+ Interaction interaction = new TextModalInteraction("{\"type\":\"TextModal\",\"display_Type\":\"unknown\"}");
+ assertEquals(interaction.getDisplayType(), DisplayType.unknown);
+ EngagementModule.launchInteraction(getContext(), interaction);
+ assertResult("Activity");
+ }
+
+ class MockInteractionLauncher implements InteractionLauncher {
+ private final String name;
+
+ MockInteractionLauncher(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean launch(Context context, Interaction interaction) {
+ addResult(name);
+ return true;
+ }
+ }
+}
diff --git a/apptentive/src/androidTest/java/com/apptentive/android/sdk/module/engagement/InteractionNotificationBroadcastReceiverHandlerTest.java b/apptentive/src/androidTest/java/com/apptentive/android/sdk/module/engagement/InteractionNotificationBroadcastReceiverHandlerTest.java
new file mode 100644
index 000000000..4b7a9bcd3
--- /dev/null
+++ b/apptentive/src/androidTest/java/com/apptentive/android/sdk/module/engagement/InteractionNotificationBroadcastReceiverHandlerTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2016, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.module.engagement;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.apptentive.android.sdk.InstrumentationTestCaseBase;
+import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction;
+import com.apptentive.android.sdk.module.engagement.interaction.model.TextModalInteraction;
+import com.apptentive.android.sdk.module.engagement.notification.ApptentiveNotificationInteractionBroadcastReceiver;
+import com.apptentive.android.sdk.module.engagement.notification.DefaultInteractionNotificationBroadcastReceiverHandler;
+import com.apptentive.android.sdk.util.Constants;
+import com.apptentive.android.sdk.util.RuntimeUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+
+@RunWith(AndroidJUnit4.class)
+public class InteractionNotificationBroadcastReceiverHandlerTest extends InstrumentationTestCaseBase {
+
+ @Test
+ public void testNoteNotification() throws Exception {
+ final String interactionDefinition = "{\"type\":\"TextModal\",\"name\":\"TextModal Interaction displayed as Notification\",\"id\":\"textmodal_interaction_notification\",\"displayType\":\"Notification\",\"priority\":1,\"configuration\":{\"title\":\"Note Title\",\"body\":\"Note body\",\"actions\":[{\"id\":\"action_id_1\",\"label\":\"Dismiss\",\"action\":\"dismiss\"},{\"id\":\"action_id_2\",\"label\":\"Dismiss\",\"action\":\"dismiss\"}]}}";
+ Interaction interaction = new TextModalInteraction(interactionDefinition);
+
+ RuntimeUtils.overrideStaticFinalField(ApptentiveNotificationInteractionBroadcastReceiver.class, "DEFAULT_HANDLER", new DefaultInteractionNotificationBroadcastReceiverHandler() {
+ @Override
+ public void handleBroadcast(Context context, Intent intent) {
+ assertNotNull(intent);
+ assertEquals(Constants.NOTIFICATION_ACTION_DISPLAY, intent.getAction());
+ assertEquals(interactionDefinition, intent.getStringExtra(Constants.NOTIFICATION_EXTRA_INTERACTION_DEFINITION));
+ }
+ });
+ EngagementModule.launchInteraction(getContext(), interaction);
+ }
+}
diff --git a/apptentive/src/androidTest/java/com/apptentive/android/sdk/module/engagement/NoteInteractionNotificationAdapterTest.java b/apptentive/src/androidTest/java/com/apptentive/android/sdk/module/engagement/NoteInteractionNotificationAdapterTest.java
new file mode 100644
index 000000000..9f44889d9
--- /dev/null
+++ b/apptentive/src/androidTest/java/com/apptentive/android/sdk/module/engagement/NoteInteractionNotificationAdapterTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2016, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.module.engagement;
+
+import android.content.Context;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.apptentive.android.sdk.InstrumentationTestCaseBase;
+import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction;
+import com.apptentive.android.sdk.module.engagement.interaction.model.TextModalInteraction;
+import com.apptentive.android.sdk.module.engagement.notification.DefaultInteractionNotificationBroadcastReceiverHandler;
+import com.apptentive.android.sdk.module.engagement.notification.NoteInteractionNotificationAdapter;
+import com.apptentive.android.sdk.util.RuntimeUtils;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static com.apptentive.android.sdk.util.Constants.NOTIFICATION_CHANNEL_DEFAULT;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@RunWith(AndroidJUnit4.class)
+public class NoteInteractionNotificationAdapterTest extends InstrumentationTestCaseBase {
+
+ @Test
+ public void testNoteNotificationDisplay() throws Exception {
+ final String interactionDefinition = "{\"type\":\"TextModal\",\"name\":\"TextModal Interaction displayed as Notification\",\"id\":\"textmodal_interaction_notification\",\"display_type\":\"notification\",\"priority\":1,\"configuration\":{\"title\":\"Note Title\",\"body\":\"Note body\",\"actions\":[{\"id\":\"action_id_1\",\"label\":\"Dismiss\",\"action\":\"dismiss\"},{\"id\":\"action_id_2\",\"label\":\"Dismiss\",\"action\":\"dismiss\"}]}}";
+ Interaction interaction = new TextModalInteraction(interactionDefinition);
+
+ RuntimeUtils.overrideStaticFinalField(DefaultInteractionNotificationBroadcastReceiverHandler.class, "DEFAULT_ADAPTER_NOTE", new NoteInteractionNotificationAdapter() {
+ @Override
+ protected void actionDisplayNotification(Context context, String channelId, TextModalInteraction interaction) {
+ assertNotNull(context);
+ assertEquals(NOTIFICATION_CHANNEL_DEFAULT, channelId);
+ assertNotNull(interaction);
+ assertEquals(Interaction.Type.TextModal, interaction.getType());
+ assertEquals("Note Title", interaction.getTitle());
+ assertEquals("Note body", interaction.getBody());
+ }
+ });
+ EngagementModule.launchInteraction(getContext(), interaction);
+ }
+}
\ No newline at end of file
diff --git a/apptentive/src/androidTest/java/com/apptentive/android/sdk/network/HttpRequestManagerTest.java b/apptentive/src/androidTest/java/com/apptentive/android/sdk/network/HttpRequestManagerTest.java
index ff1b239a7..827490542 100644
--- a/apptentive/src/androidTest/java/com/apptentive/android/sdk/network/HttpRequestManagerTest.java
+++ b/apptentive/src/androidTest/java/com/apptentive/android/sdk/network/HttpRequestManagerTest.java
@@ -20,7 +20,7 @@ public class HttpRequestManagerTest extends TestCaseBase {
private MockDispatchQueue networkQueue;
@Before
- public void setUp() {
+ public void setUp() throws Exception {
super.setUp();
networkQueue = new MockDispatchQueue(false);
diff --git a/apptentive/src/androidTest/java/com/apptentive/android/sdk/util/threading/ConcurrentDispatchQueueTest.java b/apptentive/src/androidTest/java/com/apptentive/android/sdk/util/threading/ConcurrentDispatchQueueTest.java
index 51d9ebec7..d10fa1cdb 100644
--- a/apptentive/src/androidTest/java/com/apptentive/android/sdk/util/threading/ConcurrentDispatchQueueTest.java
+++ b/apptentive/src/androidTest/java/com/apptentive/android/sdk/util/threading/ConcurrentDispatchQueueTest.java
@@ -20,7 +20,7 @@ public class ConcurrentDispatchQueueTest extends TestCaseBase {
private DispatchQueue dispatchQueue;
@Before
- public void setUp() {
+ public void setUp() throws Exception {
super.setUp();
dispatchQueue = DispatchQueue.createBackgroundQueue("Test Queue", DispatchQueueType.Concurrent);
}
diff --git a/apptentive/src/androidTest/java/com/apptentive/android/sdk/util/threading/SerialDispatchQueueTest.java b/apptentive/src/androidTest/java/com/apptentive/android/sdk/util/threading/SerialDispatchQueueTest.java
index b37af3b25..c319bee4c 100644
--- a/apptentive/src/androidTest/java/com/apptentive/android/sdk/util/threading/SerialDispatchQueueTest.java
+++ b/apptentive/src/androidTest/java/com/apptentive/android/sdk/util/threading/SerialDispatchQueueTest.java
@@ -21,7 +21,7 @@ public class SerialDispatchQueueTest extends TestCaseBase {
private DispatchQueue dispatchQueue;
@Before
- public void setUp() {
+ public void setUp() throws Exception {
super.setUp();
dispatchQueue = DispatchQueue.createBackgroundQueue("Test Queue", DispatchQueueType.Serial);
}
diff --git a/apptentive/src/main/AndroidManifest.xml b/apptentive/src/main/AndroidManifest.xml
index 7993dedaf..c2b5924ec 100644
--- a/apptentive/src/main/AndroidManifest.xml
+++ b/apptentive/src/main/AndroidManifest.xml
@@ -25,5 +25,7 @@
android:enabled="true"
android:exported="false"/>
+
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/Apptentive.java b/apptentive/src/main/java/com/apptentive/android/sdk/Apptentive.java
index 73b9cd22b..eaa1d4086 100755
--- a/apptentive/src/main/java/com/apptentive/android/sdk/Apptentive.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/Apptentive.java
@@ -49,6 +49,9 @@
import static com.apptentive.android.sdk.ApptentiveHelper.checkConversationQueue;
import static com.apptentive.android.sdk.ApptentiveHelper.dispatchConversationTask;
import static com.apptentive.android.sdk.ApptentiveHelper.dispatchOnConversationQueue;
+import static com.apptentive.android.sdk.ApptentiveLogTag.CONVERSATION;
+import static com.apptentive.android.sdk.ApptentiveLogTag.MESSAGES;
+import static com.apptentive.android.sdk.ApptentiveLogTag.PUSH;
import static com.apptentive.android.sdk.util.StringUtils.trim;
/**
@@ -58,23 +61,68 @@ public class Apptentive {
/**
* Must be called from the {@link Application#onCreate()} method in the {@link Application} object defined in your app's manifest.
+ * Note: application key and signature would be resolved from the AndroidManifest.xml
*
* @param application The {@link Application} object for this app.
+ * @deprecated Please, use {@link #register(Application, String, String)} or {@link #register(Application, ApptentiveConfiguration)} instead
*/
+ @Deprecated
public static void register(Application application) {
- register(application, null, null);
+ if (application == null) {
+ throw new IllegalArgumentException("Application is null");
+ }
+
+ String apptentiveKey = Util.getManifestMetadataString(application, Constants.MANIFEST_KEY_APPTENTIVE_KEY);
+ if (StringUtils.isNullOrEmpty(apptentiveKey)) {
+ ApptentiveLog.e("Unable to initialize Apptentive SDK: '%s' manifest key is missing", Constants.MANIFEST_KEY_APPTENTIVE_KEY);
+ return;
+ }
+
+ String apptentiveSignature = Util.getManifestMetadataString(application, Constants.MANIFEST_KEY_APPTENTIVE_SIGNATURE);
+ if (StringUtils.isNullOrEmpty(apptentiveSignature)) {
+ ApptentiveLog.e("Unable to initialize Apptentive SDK: '%s' manifest key is missing", Constants.MANIFEST_KEY_APPTENTIVE_SIGNATURE);
+ return;
+ }
+
+ ApptentiveConfiguration configuration = new ApptentiveConfiguration(apptentiveKey, apptentiveSignature);
+
+ String logLevelString = Util.getManifestMetadataString(application, Constants.MANIFEST_KEY_APPTENTIVE_LOG_LEVEL);
+ ApptentiveLog.Level logLevel = ApptentiveLog.Level.parse(logLevelString);
+ if (logLevel != ApptentiveLog.Level.UNKNOWN) {
+ configuration.setLogLevel(logLevel);
+ }
+
+ register(application, configuration);
}
+ /**
+ * Must be called from the {@link Application#onCreate()} method in the {@link Application} object defined in your app's manifest.
+ * @param application Application object.
+ * @param apptentiveKey Apptentive Key.
+ * @param apptentiveSignature Apptentive Signature.
+ */
public static void register(Application application, String apptentiveKey, String apptentiveSignature) {
- register(application, apptentiveKey, apptentiveSignature, null);
+ register(application, new ApptentiveConfiguration(apptentiveKey, apptentiveSignature));
}
- private static void register(Application application, String apptentiveKey, String apptentiveSignature, String serverUrl) {
+ /**
+ * Must be called from the {@link Application#onCreate()} method in the {@link Application} object defined in your app's manifest.
+ * @param application Application object.
+ * @param configuration Apptentive configuration containing SDK initialization data.
+ */
+ public static void register(Application application, ApptentiveConfiguration configuration) {
+ if (application == null) {
+ throw new IllegalArgumentException("Application is null");
+ }
+
+ if (configuration == null) {
+ throw new IllegalArgumentException("Apptentive configuration is null");
+ }
+
try {
- ApptentiveLog.i("Registering Apptentive.");
- ApptentiveInternal.createInstance(application, apptentiveKey, apptentiveSignature, serverUrl);
+ ApptentiveInternal.createInstance(application, configuration);
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while registering Apptentive");
+ ApptentiveLog.e(e, "Exception while registering Apptentive SDK");
}
}
@@ -114,7 +162,7 @@ public static String getPersonEmail() {
}
}
} catch (Exception e) {
- ApptentiveLog.e("Exception while getting person email");
+ ApptentiveLog.e(CONVERSATION,"Exception while getting person email");
}
return null;
}
@@ -153,7 +201,7 @@ public static String getPersonName() {
}
}
} catch (Exception e) {
- ApptentiveLog.e("Exception while getting person name");
+ ApptentiveLog.e(CONVERSATION, "Exception while getting person name");
}
return null;
}
@@ -435,7 +483,7 @@ public static boolean isApptentivePushNotification(Intent intent) {
}
return ApptentiveInternal.getApptentivePushNotificationData(intent) != null;
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while checking for Apptentive push notification intent");
+ ApptentiveLog.e(PUSH, e, "Exception while checking for Apptentive push notification intent");
}
return false;
}
@@ -454,7 +502,7 @@ public static boolean isApptentivePushNotification(Bundle bundle) {
}
return ApptentiveInternal.getApptentivePushNotificationData(bundle) != null;
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while checking for Apptentive push notification bundle");
+ ApptentiveLog.e(PUSH, e, "Exception while checking for Apptentive push notification bundle");
}
return false;
}
@@ -472,7 +520,7 @@ public static boolean isApptentivePushNotification(Map data) {
}
return ApptentiveInternal.getApptentivePushNotificationData(data) != null;
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while checking for Apptentive push notification data");
+ ApptentiveLog.e(PUSH, e, "Exception while checking for Apptentive push notification data");
}
return false;
}
@@ -672,7 +720,7 @@ public static String getTitleFromApptentivePush(Bundle bundle) {
return uaPushBundle.getString(ApptentiveInternal.TITLE_DEFAULT);
}
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while getting title from Apptentive push");
+ ApptentiveLog.e(PUSH, e, "Exception while getting title from Apptentive push");
}
return null;
}
@@ -716,7 +764,7 @@ public static String getBodyFromApptentivePush(Bundle bundle) {
return bundle.getString(ApptentiveInternal.BODY_UA);
}
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while getting body from Apptentive push");
+ ApptentiveLog.e(PUSH, e, "Exception while getting body from Apptentive push");
}
return null;
}
@@ -740,7 +788,7 @@ public static String getTitleFromApptentivePush(Map data) {
}
return data.get(ApptentiveInternal.TITLE_DEFAULT);
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while getting title from Apptentive push");
+ ApptentiveLog.e(PUSH, e, "Exception while getting title from Apptentive push");
}
return null;
}
@@ -764,7 +812,7 @@ public static String getBodyFromApptentivePush(Map data) {
}
return data.get(ApptentiveInternal.BODY_DEFAULT);
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while getting body from Apptentive push");
+ ApptentiveLog.e(PUSH, e, "Exception while getting body from Apptentive push");
}
return null;
}
@@ -786,7 +834,7 @@ public static void setRatingProvider(IRatingProvider ratingProvider) {
ApptentiveInternal.getInstance().setRatingProvider(ratingProvider);
}
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while setting rating provider");
+ ApptentiveLog.e(CONVERSATION, e, "Exception while setting rating provider");
}
}
@@ -803,7 +851,7 @@ public static void putRatingProviderArg(String key, String value) {
ApptentiveInternal.getInstance().putRatingProviderArg(key, value);
}
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while putting rating provider arg");
+ ApptentiveLog.e(CONVERSATION, e, "Exception while putting rating provider arg");
}
}
@@ -950,7 +998,7 @@ public static int getUnreadMessageCount() {
return conversationProxy != null ? conversationProxy.getUnreadMessageCount() : 0;
}
} catch (Exception e) {
- ApptentiveLog.w(e, "Error in Apptentive.getUnreadMessageCount()");
+ ApptentiveLog.e(MESSAGES, e, "Exception while getting unread message count");
MetricModule.sendError(e, null, null);
}
return 0;
@@ -1325,7 +1373,7 @@ protected void execute() {
try {
loginGuarded(token, callback);
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while trying to login");
+ ApptentiveLog.e(CONVERSATION, e, "Exception while trying to login");
notifyFailure(callback, "Exception while trying to login");
MetricModule.sendError(e, null, null);
}
@@ -1338,7 +1386,7 @@ private static void loginGuarded(String token, final LoginCallback callback) {
final ApptentiveInstance sharedInstance = ApptentiveInternal.getInstance();
if (sharedInstance.isNull()) {
- ApptentiveLog.e("Unable to login: Apptentive instance is not properly initialized");
+ ApptentiveLog.e(CONVERSATION, "Unable to login: Apptentive instance is not properly initialized");
notifyFailure(callback, "Apptentive instance is not properly initialized");
} else {
sharedInstance.login(token, callback);
@@ -1404,7 +1452,7 @@ public static void setAuthenticationFailedListener(AuthenticationFailedListener
}
ApptentiveInternal.getInstance().setAuthenticationFailedListener(listener);
} catch (Exception e) {
- ApptentiveLog.w(e, "Error in Apptentive.setUnreadMessagesListener()");
+ ApptentiveLog.e(CONVERSATION, e, "Error in Apptentive.setUnreadMessagesListener()");
MetricModule.sendError(e, null, null);
}
}
@@ -1416,7 +1464,7 @@ public static void clearAuthenticationFailedListener() {
}
ApptentiveInternal.getInstance().setAuthenticationFailedListener(null);
} catch (Exception e) {
- ApptentiveLog.w(e, "Error in Apptentive.clearUnreadMessagesListener()");
+ ApptentiveLog.e(CONVERSATION, e, "Exception while clearing authentication listener");
MetricModule.sendError(e, null, null);
}
}
@@ -1502,7 +1550,7 @@ public static AuthenticationFailedReason parse(String errorType, String error) {
ret.error = error;
return ret;
} catch (Exception e) {
- ApptentiveLog.w("Error parsing unknown Apptentive.AuthenticationFailedReason: %s", errorType);
+ ApptentiveLog.e(CONVERSATION, "Error parsing unknown Apptentive.AuthenticationFailedReason: %s", errorType);
}
return UNKNOWN;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveConfiguration.java b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveConfiguration.java
new file mode 100644
index 000000000..03c753fac
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveConfiguration.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk;
+
+import android.support.annotation.NonNull;
+
+import com.apptentive.android.sdk.util.StringUtils;
+
+public class ApptentiveConfiguration {
+ private final String apptentiveKey;
+ private final String apptentiveSignature;
+ private String baseURL;
+ private ApptentiveLog.Level logLevel;
+ private boolean shouldSanitizeLogMessages;
+
+ public ApptentiveConfiguration(@NonNull String apptentiveKey, @NonNull String apptentiveSignature) {
+ if (StringUtils.isNullOrEmpty(apptentiveKey)) {
+ throw new IllegalArgumentException("Apptentive key is null or empty");
+ }
+
+ if (StringUtils.isNullOrEmpty(apptentiveSignature)) {
+ throw new IllegalArgumentException("Apptentive signature is null or empty");
+ }
+
+ this.apptentiveKey = apptentiveKey.trim();
+ this.apptentiveSignature = apptentiveSignature.trim();
+ this.logLevel = ApptentiveLog.Level.INFO;
+ this.shouldSanitizeLogMessages = true;
+ }
+
+ public String getApptentiveKey() {
+ return apptentiveKey;
+ }
+
+ public String getApptentiveSignature() {
+ return apptentiveSignature;
+ }
+
+ public String getBaseURL() {
+ return baseURL;
+ }
+
+ public ApptentiveConfiguration setBaseURL(String baseURL) {
+ this.baseURL = baseURL;
+ return this;
+ }
+
+ public ApptentiveLog.Level getLogLevel() {
+ return logLevel;
+ }
+
+ public ApptentiveConfiguration setLogLevel(ApptentiveLog.Level logLevel) {
+ this.logLevel = logLevel;
+ return this;
+ }
+
+ /**
+ * Returns true
is SDK should hide user sensitive information (user name, email,
+ * custom data, etc). Useful for debugging.
+ */
+ public boolean shouldSanitizeLogMessages() {
+ return shouldSanitizeLogMessages;
+ }
+
+ /**
+ * Overrides if SDK should hide sensitive information logging (user name, email,
+ * custom data, etc). Useful for debugging.
+ */
+ public void setShouldSanitizeLogMessages(boolean shouldSanitizeLogMessages) {
+ this.shouldSanitizeLogMessages = shouldSanitizeLogMessages;
+ }
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInstance.java b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInstance.java
index c4cf17286..68d9e1843 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInstance.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInstance.java
@@ -36,6 +36,7 @@ public interface ApptentiveInstance extends Nullsafe {
void onActivityStarted(Activity activity);
void onActivityResumed(Activity activity);
+ void onActivityStopped(Activity activity);
void onAppEnterForeground();
void onAppEnterBackground();
void onAppLaunch(Context context);
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInternal.java b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInternal.java
index 8dd73ccd5..d656437b2 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInternal.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInternal.java
@@ -53,6 +53,8 @@
import com.apptentive.android.sdk.storage.Sdk;
import com.apptentive.android.sdk.storage.SdkManager;
import com.apptentive.android.sdk.storage.VersionHistoryItem;
+import com.apptentive.android.sdk.util.AdvertiserManager;
+import com.apptentive.android.sdk.util.AdvertiserManager.AdvertisingIdClientInfo;
import com.apptentive.android.sdk.util.Constants;
import com.apptentive.android.sdk.util.ObjectUtils;
import com.apptentive.android.sdk.util.StringUtils;
@@ -74,22 +76,29 @@
import static com.apptentive.android.sdk.ApptentiveHelper.checkConversationQueue;
import static com.apptentive.android.sdk.ApptentiveHelper.dispatchOnConversationQueue;
import static com.apptentive.android.sdk.ApptentiveHelper.isConversationQueue;
+import static com.apptentive.android.sdk.ApptentiveLogTag.ADVERTISER_ID;
import static com.apptentive.android.sdk.ApptentiveLogTag.CONVERSATION;
+import static com.apptentive.android.sdk.ApptentiveLogTag.MESSAGES;
+import static com.apptentive.android.sdk.ApptentiveLogTag.PUSH;
import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_ACTIVITY_RESUMED;
import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_ACTIVITY_STARTED;
+import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_ACTIVITY_STOPPED;
import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_APP_ENTERED_BACKGROUND;
import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_APP_ENTERED_FOREGROUND;
import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_AUTHENTICATION_FAILED;
+import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_CONFIGURATION_FETCH_DID_FINISH;
import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_CONVERSATION_STATE_DID_CHANGE;
import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_CONVERSATION_WILL_LOGOUT;
-import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_INTERACTIONS_FETCHED;
+import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_INTERACTIONS_DID_FETCH;
import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_INTERACTIONS_SHOULD_DISMISS;
import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_INTERACTION_MANIFEST_FETCHED;
import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_KEY_ACTIVITY;
import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_KEY_AUTHENTICATION_FAILED_REASON;
+import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_KEY_CONFIGURATION;
import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_KEY_CONVERSATION;
import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_KEY_CONVERSATION_ID;
import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_KEY_MANIFEST;
+import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_KEY_SUCCESSFUL;
import static com.apptentive.android.sdk.debug.Assert.assertNotNull;
import static com.apptentive.android.sdk.debug.Assert.assertTrue;
import static com.apptentive.android.sdk.util.Constants.CONVERSATIONS_DIR;
@@ -98,7 +107,6 @@
* This class contains only internal methods. These methods should not be access directly by the host app.
*/
public class ApptentiveInternal implements ApptentiveInstance, ApptentiveNotificationObserver {
-
private final ApptentiveTaskManager taskManager;
private final ApptentiveActivityLifecycleCallbacks lifecycleCallbacks;
@@ -143,6 +151,7 @@ public class ApptentiveInternal implements ApptentiveInstance, ApptentiveNotific
private static final String PUSH_ACTION = "action";
private static final String PUSH_CONVERSATION_ID = "conversation_id";
+ private static final int LOG_HISTORY_SIZE = 2;
private enum PushAction {
pmc, // Present Message Center.
@@ -152,7 +161,7 @@ public static PushAction parse(String name) {
try {
return PushAction.valueOf(name);
} catch (IllegalArgumentException e) {
- ApptentiveLog.d("Error parsing unknown PushAction: " + name);
+ ApptentiveLog.w(PUSH, "This version of the SDK can't handle push action '%s'", name);
}
return unknown;
}
@@ -193,6 +202,7 @@ private ApptentiveInternal(Application application, String apptentiveKey, String
globalSharedPrefs = application.getSharedPreferences(Constants.PREF_NAME, Context.MODE_PRIVATE);
apptentiveHttpClient = new ApptentiveHttpClient(apptentiveKey, apptentiveSignature, getEndpointBase(globalSharedPrefs));
+
conversationManager = new ConversationManager(appContext, Util.getInternalDir(appContext, CONVERSATIONS_DIR, true));
appRelease = AppReleaseManager.generateCurrentAppRelease(application, this);
@@ -203,7 +213,9 @@ private ApptentiveInternal(Application application, String apptentiveKey, String
.addObserver(NOTIFICATION_CONVERSATION_STATE_DID_CHANGE, this)
.addObserver(NOTIFICATION_CONVERSATION_WILL_LOGOUT, this)
.addObserver(NOTIFICATION_AUTHENTICATION_FAILED, this)
- .addObserver(NOTIFICATION_INTERACTION_MANIFEST_FETCHED, this);
+ .addObserver(NOTIFICATION_INTERACTION_MANIFEST_FETCHED, this)
+ .addObserver(NOTIFICATION_APP_ENTERED_FOREGROUND, this)
+ .addObserver(NOTIFICATION_CONFIGURATION_FETCH_DID_FINISH, this);
}
public static boolean isApptentiveRegistered() {
@@ -226,40 +238,33 @@ public static boolean isConversationActive() {
*
* @param application the context of the app that is creating the instance
*/
- static void createInstance(Application application, String apptentiveKey, String apptentiveSignature, final String serverUrl) {
- if (application == null) {
- throw new IllegalArgumentException("Application is null");
- }
-
- // try initializing log monitor
- LogMonitor.tryInitialize(application.getApplicationContext(), apptentiveKey, apptentiveSignature);
+ static void createInstance(@NonNull Application application, @NonNull ApptentiveConfiguration configuration) {
+ final String apptentiveKey = configuration.getApptentiveKey();
+ final String apptentiveSignature = configuration.getApptentiveSignature();
+ final String baseURL = configuration.getBaseURL();
- synchronized (ApptentiveInternal.class) {
- if (sApptentiveInternal == null) {
+ // set log message sanitizing
+ ApptentiveLog.setShouldSanitizeLogMessages(configuration.shouldSanitizeLogMessages());
- // trim spaces
- apptentiveKey = Util.trim(apptentiveKey);
- apptentiveSignature = Util.trim(apptentiveSignature);
+ // set log level before we initialize log monitor since log monitor can override it as well
+ ApptentiveLog.overrideLogLevel(configuration.getLogLevel());
- // if App key is not defined - try loading from AndroidManifest.xml
- if (StringUtils.isNullOrEmpty(apptentiveKey)) {
- apptentiveKey = Util.getManifestMetadataString(application, Constants.MANIFEST_KEY_APPTENTIVE_KEY);
- // TODO: check if Apptentive Key is still empty
- }
+ // initialize log writer
+ ApptentiveLog.initialize(application.getApplicationContext(), LOG_HISTORY_SIZE);
- // if App signature is not defined - try loading from AndroidManifest.xml
- if (StringUtils.isNullOrEmpty(apptentiveSignature)) {
- apptentiveSignature = Util.getManifestMetadataString(application, Constants.MANIFEST_KEY_APPTENTIVE_SIGNATURE);
- // TODO: check if Apptentive Signature is still empty
- }
+ // try initializing log monitor
+ LogMonitor.startSession(application.getApplicationContext(), apptentiveKey, apptentiveSignature);
+ synchronized (ApptentiveInternal.class) {
+ if (sApptentiveInternal == null) {
try {
- ApptentiveLog.v("Initializing Apptentive instance: apptentiveKey=%s apptentiveSignature=%s", apptentiveKey, apptentiveSignature);
- sApptentiveInternal = new ApptentiveInternal(application, apptentiveKey, apptentiveSignature, serverUrl);
+ ApptentiveLog.i("Registering Apptentive Android SDK %s", Constants.getApptentiveSdkVersion());
+ ApptentiveLog.v("ApptentiveKey=%s ApptentiveSignature=%s", apptentiveKey, apptentiveSignature);
+ sApptentiveInternal = new ApptentiveInternal(application, apptentiveKey, apptentiveSignature, baseURL);
dispatchOnConversationQueue(new DispatchTask() {
@Override
protected void execute() {
- sApptentiveInternal.start(); // TODO: check the result of this call
+ sApptentiveInternal.start();
}
});
application.registerActivityLifecycleCallbacks(sApptentiveInternal.lifecycleCallbacks);
@@ -464,6 +469,16 @@ public void onActivityStarted(Activity activity) {
}
}
+ public void onActivityStopped(Activity activity) {
+ checkConversationQueue();
+
+ if (activity != null) {
+ // Post a notification
+ ApptentiveNotificationCenter.defaultCenter().postNotification(NOTIFICATION_ACTIVITY_STOPPED,
+ NOTIFICATION_KEY_ACTIVITY, activity);
+ }
+ }
+
public void onActivityResumed(Activity activity) {
checkConversationQueue();
@@ -482,7 +497,7 @@ public void onAppEnterForeground() {
checkConversationQueue();
// Try to initialize log monitor
- LogMonitor.tryInitialize(appContext, apptentiveKey, apptentiveSignature);
+ LogMonitor.startSession(appContext, apptentiveKey, apptentiveSignature);
// Post a notification
ApptentiveNotificationCenter.defaultCenter().postNotification(NOTIFICATION_APP_ENTERED_FOREGROUND);
@@ -566,35 +581,19 @@ private boolean start() {
* 3. An unreadMessageCountListener() is set up
*/
- long start = System.currentTimeMillis();
boolean conversationLoaded = conversationManager.loadActiveConversation(getApplicationContext());
- ApptentiveLog.i(CONVERSATION, "Active conversation is%s loaded. Took %d ms", conversationLoaded ? "" : " not", System.currentTimeMillis() - start);
-
- if (conversationLoaded) {
- Conversation activeConversation = conversationManager.getActiveConversation();
- // TODO: figure out if this is still necessary
- // boolean featureEverUsed = activeConversation.isMessageCenterFeatureUsed();
- // if (featureEverUsed) {
- // messageManager.init();
- // }
+ if (!conversationLoaded) {
+ ApptentiveLog.w(CONVERSATION, "There is no active conversation. The SDK will be disabled until a conversation becomes active.");
}
apptentiveToolbarTheme = appContext.getResources().newTheme();
- boolean apptentiveDebug = false;
- String logLevelOverride = null;
try {
appPackageName = appContext.getPackageName();
PackageManager packageManager = appContext.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(appPackageName, PackageManager.GET_META_DATA | PackageManager.GET_RECEIVERS);
ApplicationInfo ai = packageInfo.applicationInfo;
- Bundle metaData = ai.metaData;
- if (metaData != null) {
- logLevelOverride = Util.trim(metaData.getString(Constants.MANIFEST_KEY_APPTENTIVE_LOG_LEVEL));
- apptentiveDebug = metaData.getBoolean(Constants.MANIFEST_KEY_APPTENTIVE_DEBUG);
- }
-
// Used for application theme inheritance if the theme is an AppCompat theme.
setApplicationDefaultTheme(ai.theme);
@@ -606,7 +605,7 @@ private boolean start() {
for (ActivityInfo activityInfo : registered) {
// Throw assertion error when relict class found in manifest.
if (activityInfo.name.equals("com.apptentive.android.sdk.comm.NetworkStateReceiver")) {
- throw new AssertionError("NetworkStateReceiver has been removed from Apptentive SDK, please make sure it's also removed from manifest file");
+ throw new AssertionError("NetworkStateReceiver has been removed from Apptentive SDK, please make sure it's also removed from manifest file"); // TODO: should be IllegalStateException or similar
}
}
}
@@ -616,59 +615,13 @@ private boolean start() {
bRet = false;
}
-
- // Set debuggable and appropriate log level.
- if (apptentiveDebug) {
- ApptentiveLog.i("Apptentive debug logging set to VERBOSE.");
- setMinimumLogLevel(ApptentiveLog.Level.VERBOSE);
- } else if (logLevelOverride != null) {
- ApptentiveLog.i("Overriding log level: %s", logLevelOverride);
- setMinimumLogLevel(ApptentiveLog.Level.parse(logLevelOverride));
- } else {
- if (appRelease.isDebug()) {
- setMinimumLogLevel(ApptentiveLog.Level.VERBOSE);
- }
- }
- ApptentiveLog.i("Debug mode enabled? %b", appRelease.isDebug());
-
- // The app key can be passed in programmatically, or we can fallback to checking in the manifest.
- if (TextUtils.isEmpty(apptentiveKey) || apptentiveKey.contains(Constants.EXAMPLE_APPTENTIVE_KEY_VALUE)) {
- String errorMessage = "The Apptentive Key is not defined. You may provide your Apptentive Key in Apptentive.register(), or in as meta-data in your AndroidManifest.xml.\n" +
- "";
- if (appRelease.isDebug()) {
- throw new RuntimeException(errorMessage);
- } else {
- ApptentiveLog.e(errorMessage);
- }
- } else {
- ApptentiveLog.d("Using cached Apptentive App Key");
- }
- ApptentiveLog.d("Apptentive App Key: %s", apptentiveKey);
-
- // The app signature can be passed in programmatically, or we can fallback to checking in the manifest.
- if (TextUtils.isEmpty(apptentiveSignature) || apptentiveSignature.contains(Constants.EXAMPLE_APPTENTIVE_SIGNATURE_VALUE)) {
- String errorMessage = "The Apptentive Signature is not defined. You may provide your Apptentive Signature in Apptentive.register(), or in as meta-data in your AndroidManifest.xml.\n" +
- "";
- if (appRelease.isDebug()) {
- throw new RuntimeException(errorMessage);
- } else {
- ApptentiveLog.e(errorMessage);
- }
- } else {
- ApptentiveLog.d("Using cached Apptentive App Signature");
- }
- ApptentiveLog.d("Apptentive App Signature: %s", apptentiveSignature);
-
- // Grab app info we need to access later on.
- ApptentiveLog.d("Default Locale: %s", Locale.getDefault().toString());
+ ApptentiveLog.v("Application Info:\n\tApptentive Key: %s\n\tApptentive Key: %s\n\tDebuggable APK: %b\n\tDefault locale: %s", apptentiveKey, apptentiveSignature, appRelease.isDebug(), Locale.getDefault());
return bRet;
}
private void checkSendVersionChanges(Conversation conversation) {
if (conversation == null) {
- ApptentiveLog.e("Can't check session data changes: session data is not initialized");
+ ApptentiveLog.e(CONVERSATION, "Can't check session data changes: session data is not initialized");
return;
}
@@ -697,19 +650,19 @@ private void checkSendVersionChanges(Conversation conversation) {
// TODO: Move this into a session became active handler.
final String lastSeenSdkVersion = conversation.getLastSeenSdkVersion();
- final String currentSdkVersion = Constants.APPTENTIVE_SDK_VERSION;
+ final String currentSdkVersion = Constants.getApptentiveSdkVersion();
if (!StringUtils.equal(lastSeenSdkVersion, currentSdkVersion)) {
sdkChanged = true;
}
if (appReleaseChanged) {
- ApptentiveLog.i("Version changed: Name: %s => %s, Code: %d => %d", previousVersionName, currentVersionName, previousVersionCode, currentVersionCode);
+ ApptentiveLog.i(CONVERSATION, "Application version was changed: Name: %s => %s, Code: %d => %d", previousVersionName, currentVersionName, previousVersionCode, currentVersionCode);
conversation.getVersionHistory().updateVersionHistory(Util.currentTimeSeconds(), currentVersionCode, currentVersionName);
}
Sdk sdk = SdkManager.generateCurrentSdk(appContext);
if (sdkChanged) {
- ApptentiveLog.i("SDK version changed: %s => %s", lastSeenSdkVersion, currentSdkVersion);
+ ApptentiveLog.i(CONVERSATION, "SDK version was changed: %s => %s", lastSeenSdkVersion, currentSdkVersion);
conversation.setLastSeenSdkVersion(currentSdkVersion);
conversation.setSdk(sdk);
}
@@ -726,6 +679,8 @@ private void checkSendVersionChanges(Conversation conversation) {
* We want to make sure the app is using the latest configuration from the server if the app or sdk version changes.
*/
private void invalidateCaches(Conversation conversation) {
+ checkConversationQueue();
+
conversation.setInteractionExpiration(0L);
Configuration config = Configuration.load();
config.setConfigurationCacheExpirationMillis(System.currentTimeMillis());
@@ -803,13 +758,6 @@ protected void execute() {
}
}
- /**
- * Pass in a log level to override the default, which is {@link ApptentiveLog.Level#INFO}
- */
- private void setMinimumLogLevel(ApptentiveLog.Level level) {
- ApptentiveLog.overrideLogLevel(level);
- }
-
/**
* The key that is used to store extra data on an Apptentive push notification.
*/
@@ -826,7 +774,7 @@ private void setMinimumLogLevel(ApptentiveLog.Level level) {
static String getApptentivePushNotificationData(Intent intent) {
if (intent != null) {
- ApptentiveLog.v("Got an Intent.");
+ ApptentiveLog.v(PUSH, "Got an Intent.");
return getApptentivePushNotificationData(intent.getExtras());
}
return null;
@@ -842,36 +790,36 @@ static String getApptentivePushNotificationData(Intent intent) {
static String getApptentivePushNotificationData(Bundle pushBundle) {
if (pushBundle != null) {
if (pushBundle.containsKey(PUSH_EXTRA_KEY_PARSE)) { // Parse
- ApptentiveLog.v("Got a Parse Push.");
+ ApptentiveLog.v(PUSH, "Got a Parse Push.");
String parseDataString = pushBundle.getString(PUSH_EXTRA_KEY_PARSE);
if (parseDataString == null) {
- ApptentiveLog.e("com.parse.Data is null.");
+ ApptentiveLog.e(PUSH, "com.parse.Data is null.");
return null;
}
try {
JSONObject parseJson = new JSONObject(parseDataString);
return parseJson.optString(APPTENTIVE_PUSH_EXTRA_KEY, null);
} catch (JSONException e) {
- ApptentiveLog.e("com.parse.Data is corrupt: %s", parseDataString);
+ ApptentiveLog.e(PUSH, "com.parse.Data is corrupt: %s", parseDataString);
return null;
}
} else if (pushBundle.containsKey(PUSH_EXTRA_KEY_UA)) { // Urban Airship
- ApptentiveLog.v("Got an Urban Airship push.");
+ ApptentiveLog.v(PUSH, "Got an Urban Airship push.");
Bundle uaPushBundle = pushBundle.getBundle(PUSH_EXTRA_KEY_UA);
if (uaPushBundle == null) {
- ApptentiveLog.e("Urban Airship push extras bundle is null");
+ ApptentiveLog.e(PUSH, "Urban Airship push extras bundle is null");
return null;
}
return uaPushBundle.getString(APPTENTIVE_PUSH_EXTRA_KEY);
} else if (pushBundle.containsKey(APPTENTIVE_PUSH_EXTRA_KEY)) { // All others
// Straight FCM / GCM / SNS, or nested
- ApptentiveLog.v("Found apptentive push data.");
+ ApptentiveLog.v(PUSH, "Found apptentive push data.");
return pushBundle.getString(APPTENTIVE_PUSH_EXTRA_KEY);
} else {
- ApptentiveLog.e("Got an unrecognizable push.");
+ ApptentiveLog.e(PUSH, "Got an unrecognizable push.");
}
}
- ApptentiveLog.e("Push bundle was null.");
+ ApptentiveLog.e(PUSH, "Push bundle was null.");
return null;
}
@@ -897,7 +845,7 @@ public void showAboutInternal(Context context, boolean showBrandingBand) {
* TODO: Decouple this from Conversation and Message Manager so it can be unit tested.
*/
static PendingIntent generatePendingIntentFromApptentivePushData(Conversation conversation, String apptentivePushData) {
- ApptentiveLog.d("Generating Apptentive push PendingIntent.");
+ ApptentiveLog.d(PUSH, "Generating Apptentive push PendingIntent.");
if (!TextUtils.isEmpty(apptentivePushData)) {
try {
JSONObject pushJson = new JSONObject(apptentivePushData);
@@ -907,7 +855,7 @@ static PendingIntent generatePendingIntentFromApptentivePushData(Conversation co
if (conversationId != null) {
// is it an actual receiver?
if (!StringUtils.equal(conversation.getConversationId(), conversationId)) {
- ApptentiveLog.i("Can't generate pending intent from Apptentive push data: push conversation id doesn't match active conversation");
+ ApptentiveLog.i(PUSH, "Can't generate pending intent from Apptentive push data: push conversation id doesn't match active conversation");
return null;
}
}
@@ -927,10 +875,10 @@ static PendingIntent generatePendingIntentFromApptentivePushData(Conversation co
return ApptentiveInternal.prepareMessageCenterPendingIntent(ApptentiveInternal.getInstance().getApplicationContext(), conversation);
}
default:
- ApptentiveLog.w("Unknown Apptentive push notification action: \"%s\"", action.name());
+ ApptentiveLog.w(PUSH, "Unknown Apptentive push notification action: \"%s\"", action.name());
}
} catch (Exception e) {
- ApptentiveLog.e(e, "Error parsing JSON from push notification.");
+ ApptentiveLog.e(PUSH, e, "Error parsing JSON from push notification.");
MetricModule.sendError(e, "Parsing Apptentive Push", apptentivePushData);
}
}
@@ -954,7 +902,7 @@ public boolean showMessageCenterInternal(@NonNull Context context, Map 0){
- try{
- message = String.format(message, args);
- }catch(IllegalFormatException e){
- message = "Error formatting log message [level="+level+"]: "+message;
- level = Level.ERROR;
- }
- }
+ public static boolean shouldSanitizeLogMessages() {
+ return shouldSanitizeLogMessages;
+ }
- String extra = null;
+ public static void setShouldSanitizeLogMessages(boolean shouldSanitizeLogMessages) {
+ ApptentiveLog.shouldSanitizeLogMessages = shouldSanitizeLogMessages;
+ }
- // add thread name if logging of the UI-thread
- if (!DispatchQueue.isMainQueue()) {
- extra = '[' + Thread.currentThread().getName() + ']';
- }
+ /**
+ * Sets a log listener which gets called every time SDK logs a message
+ */
+ public static void setLogListener(LogListener logListener) {
+ ApptentiveLog.logListener = logListener;
+ }
- // custom tag
- if (tag != null) {
- if (extra == null) {
- extra = '[' + tag.toString() + ']';
- } else {
- extra += " [" + tag.toString() + ']';
- }
+ public static Object hideIfSanitized(Object value) {
+ return value != null && shouldSanitizeLogMessages ? "" : value;
+ }
+
+ private static void log(Level level, @Nullable ApptentiveLogTag tag, Throwable throwable, String message, Object... args) {
+ try {
+ logGuarded(level, tag, throwable, message, args);
+ } catch (Exception e) {
+ // we don't care if this one fails in unit test: in fact it's better if unit test fails here
+ android.util.Log.println(Log.ERROR, TAG, "Exception while trying to log a message: " + e.getMessage());
+ }
+ }
+
+ private static void logGuarded(Level level, ApptentiveLogTag tag, Throwable throwable, String message, Object... args) {
+ if (args != null && args.length > 0 && message != null && message.length() > 0) {
+ try {
+ message = String.format(message, args);
+ } catch (Exception e) {
+ message = "Error formatting log message: " + message;
+ level = Level.ERROR;
}
+ }
- if (extra != null) {
- message = extra + " " + message;
+ StringBuilder extra = null;
+
+ // add thread name if logging of the UI-thread
+ if (!DispatchQueue.isMainQueue()) {
+ extra = new StringBuilder()
+ .append('[')
+ .append(Thread.currentThread().getName())
+ .append(']');
+ }
+
+ // custom tag
+ if (tag != null) {
+ if (extra == null) {
+ extra = new StringBuilder();
+ } else {
+ extra.append(' ');
}
+ extra
+ .append('[')
+ .append(tag.toString())
+ .append(']');
+ }
+ if (extra != null) {
+ message = extra.append(' ').append(message).toString();
+ }
- LOGGER_IMPLEMENTATION.println(level.getAndroidLevel(), TAG, message);
- if(throwable != null){
- if(throwable.getMessage() != null){
- LOGGER_IMPLEMENTATION.println(level.getAndroidLevel(), TAG, throwable.getMessage());
- }
- while(throwable != null) {
- LOGGER_IMPLEMENTATION.println(level.getAndroidLevel(), TAG, LOGGER_IMPLEMENTATION.getStackTraceString(throwable));
- throwable = throwable.getCause();
- }
+ log0(level, message);
+ if (throwable != null) {
+ if (throwable.getMessage() != null) {
+ log0(level, throwable.getMessage());
+ }
+ while (throwable != null) {
+ log0(level, Util.getStackTraceString(throwable));
+ throwable = throwable.getCause();
}
}
}
- public static boolean canLog(Level level) {
- return logLevel.canLog(level);
- }
+ private static void log0(Level level, String message) {
+ try {
+ if (canLog(level)) {
+ android.util.Log.println(level.getAndroidLevel(), TAG, message);
+ }
+ } catch (Exception e) {
+ System.out.println(message); // fallback for unit-test
+ }
- public static void vv(ApptentiveLogTag tag, String message, Object... args) {
- if (tag.enabled) {
- doLog(Level.VERY_VERBOSE, tag, null, message, args);
+ if (logListener != null) {
+ logListener.onLogMessage(level, message);
}
}
- public static void vv(ApptentiveLogTag tag, Throwable throwable, String message, Object... args){
- if (tag.enabled) {
- doLog(Level.VERY_VERBOSE, tag, throwable, message, args);
+
+ public static @NonNull File getLogsDirectory(Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("Context is null");
}
+ return new File(context.getCacheDir(), "com.apptentive.logs");
}
- public static void vv(String message, Object... args){
- doLog(Level.VERY_VERBOSE, null, null, message, args);
- }
- public static void vv(Throwable throwable, String message, Object... args){
- doLog(Level.VERY_VERBOSE, null, throwable, message, args);
+
+ public static boolean canLog(Level level) {
+ return logLevel.canLog(level);
}
public static void v(ApptentiveLogTag tag, String message, Object... args) {
- if (tag.enabled) {
- doLog(Level.VERBOSE, tag, null, message, args);
- }
+ log(Level.VERBOSE, tag, null, message, args);
}
- public static void v(ApptentiveLogTag tag, Throwable throwable, String message, Object... args){
- if (tag.enabled) {
- doLog(Level.VERBOSE, tag, throwable, message, args);
- }
+
+ public static void v(ApptentiveLogTag tag, Throwable throwable, String message, Object... args) {
+ log(Level.VERBOSE, tag, throwable, message, args);
}
- public static void v(String message, Object... args){
- doLog(Level.VERBOSE, null, null, message, args);
+
+ public static void v(String message, Object... args) {
+ log(Level.VERBOSE, null, null, message, args);
}
- public static void v(Throwable throwable, String message, Object... args){
- doLog(Level.VERBOSE, null, throwable, message, args);
+
+ public static void v(Throwable throwable, String message, Object... args) {
+ log(Level.VERBOSE, null, throwable, message, args);
}
- public static void d(ApptentiveLogTag tag, String message, Object... args){
- if (tag.enabled) {
- doLog(Level.DEBUG, tag, null, message, args);
- }
+ public static void d(ApptentiveLogTag tag, String message, Object... args) {
+ log(Level.DEBUG, tag, null, message, args);
}
- public static void d(ApptentiveLogTag tag, Throwable throwable, String message, Object... args){
- if (tag.enabled) {
- doLog(Level.DEBUG, tag, throwable, message, args);
- }
+
+ public static void d(ApptentiveLogTag tag, Throwable throwable, String message, Object... args) {
+ log(Level.DEBUG, tag, throwable, message, args);
}
- public static void d(String message, Object... args){
- doLog(Level.DEBUG, null, null, message, args);
+
+ public static void d(String message, Object... args) {
+ log(Level.DEBUG, null, null, message, args);
}
- public static void d(Throwable throwable, String message, Object... args){
- doLog(Level.DEBUG, null, throwable, message, args);
+
+ public static void d(Throwable throwable, String message, Object... args) {
+ log(Level.DEBUG, null, throwable, message, args);
}
- public static void i(ApptentiveLogTag tag, String message, Object... args){
- if (tag.enabled) {
- doLog(Level.INFO, tag, null, message, args);
- }
+ public static void i(ApptentiveLogTag tag, String message, Object... args) {
+ log(Level.INFO, tag, null, message, args);
}
- public static void i(ApptentiveLogTag tag, Throwable throwable, String message, Object... args){
- if (tag.enabled) {
- doLog(Level.INFO, tag, throwable, message, args);
- }
+
+ public static void i(ApptentiveLogTag tag, Throwable throwable, String message, Object... args) {
+ log(Level.INFO, tag, throwable, message, args);
}
- public static void i(String message, Object... args){
- doLog(Level.INFO, null, null, message, args);
+
+ public static void i(String message, Object... args) {
+ log(Level.INFO, null, null, message, args);
}
- public static void i(Throwable throwable, String message, Object... args){
- doLog(Level.INFO, null, throwable, message, args);
+
+ public static void i(Throwable throwable, String message, Object... args) {
+ log(Level.INFO, null, throwable, message, args);
}
- public static void w(ApptentiveLogTag tag, String message, Object... args){
- if (tag.enabled) {
- doLog(Level.WARN, tag, null, message, args);
- }
+ public static void w(ApptentiveLogTag tag, String message, Object... args) {
+ log(Level.WARN, tag, null, message, args);
}
- public static void w(ApptentiveLogTag tag, Throwable throwable, String message, Object... args){
- if (tag.enabled) {
- doLog(Level.WARN, tag, throwable, message, args);
- }
+
+ public static void w(ApptentiveLogTag tag, Throwable throwable, String message, Object... args) {
+ log(Level.WARN, tag, throwable, message, args);
}
- public static void w(String message, Object... args){
- doLog(Level.WARN, null, null, message, args);
+
+ public static void w(String message, Object... args) {
+ log(Level.WARN, null, null, message, args);
}
- public static void w(Throwable throwable, String message, Object... args){
- doLog(Level.WARN, null, throwable, message, args);
+
+ public static void w(Throwable throwable, String message, Object... args) {
+ log(Level.WARN, null, throwable, message, args);
}
- public static void e(ApptentiveLogTag tag, String message, Object... args){
- if (tag.enabled) {
- doLog(Level.ERROR, tag, null, message, args);
- }
+ public static void e(ApptentiveLogTag tag, String message, Object... args) {
+ log(Level.ERROR, tag, null, message, args);
}
- public static void e(ApptentiveLogTag tag, Throwable throwable, String message, Object... args){
- if (tag.enabled) {
- doLog(Level.ERROR, tag, throwable, message, args);
- }
+
+ public static void e(ApptentiveLogTag tag, Throwable throwable, String message, Object... args) {
+ log(Level.ERROR, tag, throwable, message, args);
}
- public static void e(String message, Object... args){
- doLog(Level.ERROR, null, null, message, args);
+
+ public static void e(String message, Object... args) {
+ log(Level.ERROR, null, null, message, args);
}
- public static void e(Throwable throwable, String message, Object... args){
- doLog(Level.ERROR, null, throwable, message, args);
+
+ public static void e(Throwable throwable, String message, Object... args) {
+ log(Level.ERROR, null, throwable, message, args);
}
- public static void a(ApptentiveLogTag tag, String message, Object... args){
- if (tag.enabled) {
- doLog(Level.ASSERT, tag, null, message, args);
- }
+ public static void a(ApptentiveLogTag tag, String message, Object... args) {
+ log(Level.ASSERT, tag, null, message, args);
}
- public static void a(ApptentiveLogTag tag, Throwable throwable, String message, Object... args){
- if (tag.enabled) {
- doLog(Level.ASSERT, tag, throwable, message, args);
- }
+
+ public static void a(ApptentiveLogTag tag, Throwable throwable, String message, Object... args) {
+ log(Level.ASSERT, tag, throwable, message, args);
}
- public static void a(String message, Object... args){
- doLog(Level.ASSERT, null, null, message, args);
+
+ public static void a(String message, Object... args) {
+ log(Level.ASSERT, null, null, message, args);
}
- public static void a(Throwable throwable, String message, Object... args){
- doLog(Level.ASSERT, null, throwable, message, args);
+
+ public static void a(Throwable throwable, String message, Object... args) {
+ log(Level.ASSERT, null, throwable, message, args);
}
public enum Level {
- VERY_VERBOSE(1, Log.VERBOSE),
- VERBOSE(Log.VERBOSE, Log.VERBOSE),
- DEBUG(Log.DEBUG, Log.DEBUG),
- INFO(Log.INFO, Log.INFO),
- WARN(Log.WARN, Log.WARN),
- ERROR(Log.ERROR, Log.ERROR),
- ASSERT(Log.ASSERT, Log.ASSERT),
- DEFAULT(Log.INFO, Log.INFO);
-
- private int level;
- private int androidLevel;
-
- private Level(int level, int androidLevel) {
+ VERBOSE("V", Log.VERBOSE, Log.VERBOSE),
+ DEBUG("D", Log.DEBUG, Log.DEBUG),
+ INFO("I", Log.INFO, Log.INFO),
+ WARN("W", Log.WARN, Log.WARN),
+ ERROR("E", Log.ERROR, Log.ERROR),
+ ASSERT("A", Log.ASSERT, Log.ASSERT),
+ UNKNOWN("?", -1, -1);
+
+ private final int level;
+ private final int androidLevel;
+ private final String shortName;
+
+ Level(String shortName, int level, int androidLevel) {
+ this.shortName = shortName;
this.level = level;
this.androidLevel = androidLevel;
}
+ public String getShortName() {
+ return shortName;
+ }
+
public int getAndroidLevel() {
return androidLevel;
}
@@ -232,16 +270,18 @@ public int getLevel() {
}
public static Level parse(String level) {
- try {
- return Level.valueOf(level);
- } catch (IllegalArgumentException e) {
- LOGGER_IMPLEMENTATION.println(Log.WARN, TAG, "Error parsing unknown ApptentiveLog.Level: " + level);
+ if (!StringUtils.isNullOrEmpty(level)) {
+ try {
+ return Level.valueOf(level);
+ } catch (Exception ignored) {
+ }
}
- return DEFAULT;
+ return UNKNOWN;
}
/**
* If this object is the current level, returns true if the Level passed in is of a sufficient level to be logged.
+ *
* @return true if "level" can be logged.
*/
public boolean canLog(Level level) {
@@ -249,8 +289,7 @@ public boolean canLog(Level level) {
}
}
- interface LoggerImplementation {
- void println(int priority, String tag, String msg);
- String getStackTraceString(Throwable throwable);
+ public interface LogListener {
+ void onLogMessage(@NonNull Level level, @NonNull String message);
}
}
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 29f6d06a8..d8cc1af45 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveLogTag.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveLogTag.java
@@ -1,18 +1,18 @@
package com.apptentive.android.sdk;
public enum ApptentiveLogTag {
- NETWORK(true),
- APP_CONFIGURATION(true),
- CONVERSATION(true),
- NOTIFICATIONS(true),
- MESSAGES(true),
- DATABASE(true),
- PAYLOADS(true),
- TESTER_COMMANDS(true);
-
- ApptentiveLogTag(boolean enabled) {
- this.enabled = enabled;
- }
-
- public boolean enabled;
+ NETWORK,
+ APP_CONFIGURATION,
+ CONVERSATION,
+ INTERACTIONS,
+ NOTIFICATIONS,
+ MESSAGES,
+ DATABASE,
+ PAYLOADS,
+ TESTER_COMMANDS,
+ NOTIFICATION_INTERACTIONS,
+ PUSH,
+ UTIL,
+ TROUBLESHOOT,
+ ADVERTISER_ID,
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveNotifications.java b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveNotifications.java
index 6aa27b596..4439578e5 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveNotifications.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveNotifications.java
@@ -8,6 +8,11 @@
public class ApptentiveNotifications {
+ /**
+ * Sent when conversation loading is finished
+ */
+ public static final String NOTIFICATION_CONVERSATION_LOAD_DID_FINISH = "CONVERSATION_LOAD_DID_FINISH"; // { conversation : Conversation, successful: Boolean }
+
/**
* Sent when conversation data changes
*/
@@ -18,55 +23,85 @@ public class ApptentiveNotifications {
*/
public static final String NOTIFICATION_CONVERSATION_STATE_DID_CHANGE = "CONVERSATION_STATE_DID_CHANGE"; // { conversation : Conversation }
+ /**
+ * Sent when conversation token starts fetching
+ */
+ public static final String NOTIFICATION_CONVERSATION_TOKEN_WILL_FETCH = "CONVERSATION_TOKEN_WILL_FETCH"; // { conversation : Conversation }
+
+ /**
+ * Sent when conversation token fetch completes
+ */
+ public static final String NOTIFICATION_CONVERSATION_TOKEN_DID_FETCH = "CONVERSATION_TOKEN_DID_FETCH"; // { conversation : Conversation, successful: Boolean }
+
/**
* Sent when conversation is about to be logged out, to allow necessary tasks to be completed within the ending conversation.
*/
public static final String NOTIFICATION_CONVERSATION_WILL_LOGOUT = "CONVERSATION_WILL_LOGOUT"; // { conversation : Conversation }
+ /**
+ * Sent when message polling is started
+ */
+ public static final String NOTIFICATION_MESSAGES_STARTED_POLLING = "MESSAGES_STARTED_POLLING"; // { interval: Long }
+
+ /**
+ * Sent when message polling is stopped
+ */
+ public static final String NOTIFICATION_MESSAGES_STOPPED_POLLING = "MESSAGES_STOPPED_POLLING";
+
/**
* Sent if a new activity is started.
*/
- public static final String NOTIFICATION_ACTIVITY_STARTED = "NOTIFICATION_ACTIVITY_STARTED"; // { activity : Activity }
+ public static final String NOTIFICATION_ACTIVITY_STARTED = "ACTIVITY_STARTED"; // { activity : Activity }
+
+ /**
+ * Sent if a new activity is stopped.
+ */
+ public static final String NOTIFICATION_ACTIVITY_STOPPED = "ACTIVITY_STOPPED"; // { activity : Activity }
/**
* Sent if activity is resumed.
*/
- public static final String NOTIFICATION_ACTIVITY_RESUMED = "NOTIFICATION_ACTIVITY_RESUMED"; // { activity : Activity }
+ public static final String NOTIFICATION_ACTIVITY_RESUMED = "ACTIVITY_RESUMED"; // { activity : Activity }
/**
* Sent if app entered foreground
*/
- public static final String NOTIFICATION_APP_ENTERED_FOREGROUND = "NOTIFICATION_APP_ENTERED_FOREGROUND";
+ public static final String NOTIFICATION_APP_ENTERED_FOREGROUND = "APP_ENTERED_FOREGROUND";
/**
* Sent if app entered background
*/
- public static final String NOTIFICATION_APP_ENTERED_BACKGROUND = "NOTIFICATION_APP_ENTERED_BACKGROUND";
+ public static final String NOTIFICATION_APP_ENTERED_BACKGROUND = "APP_ENTERED_BACKGROUND";
+
+ /**
+ * Event is generated
+ */
+ public static final String NOTIFICATION_EVENT_GENERATED = "EVENT_GENERATED"; // { event: EventPayload }
/**
* Sent before payload request is sent to the server
*/
- public static final String NOTIFICATION_PAYLOAD_WILL_START_SEND = "NOTIFICATION_PAYLOAD_WILL_START_SEND"; // { payload: PayloadData }
+ public static final String NOTIFICATION_PAYLOAD_WILL_START_SEND = "PAYLOAD_WILL_START_SEND"; // { payload: PayloadData }
/**
* Sent after payload sending if finished (might be successful or not)
*/
- public static final String NOTIFICATION_PAYLOAD_DID_FINISH_SEND = "NOTIFICATION_PAYLOAD_DID_FINISH_SEND"; // { successful : boolean, payload: PayloadData, responseCode: int, responseData: JSONObject }
+ public static final String NOTIFICATION_PAYLOAD_DID_FINISH_SEND = "PAYLOAD_DID_FINISH_SEND"; // { successful : boolean, payload: PayloadData, responseCode: int, responseData: JSONObject }
/**
* Sent if user requested to close all interactions.
*/
- public static final String NOTIFICATION_INTERACTIONS_SHOULD_DISMISS = "NOTIFICATION_INTERACTIONS_SHOULD_DISMISS";
+ public static final String NOTIFICATION_INTERACTIONS_SHOULD_DISMISS = "INTERACTIONS_SHOULD_DISMISS";
/**
* Sent when a request to the server fails with a 401, and external code needs to be notified.
*/
- public static final String NOTIFICATION_AUTHENTICATION_FAILED = "NOTIFICATION_AUTHENTICATION_FAILED"; // { conversationId : String, authenticationFailedReason: AuthenticationFailedReason }
+ public static final String NOTIFICATION_AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED"; // { conversationId : String, authenticationFailedReason: AuthenticationFailedReason }
/**
* Sent when interactions are fetched for any conversation. Used right now so espresso tests know when they can run.
*/
- public static final String NOTIFICATION_INTERACTIONS_FETCHED = "NOTIFICATION_INTERACTIONS_FETCHED";
+ public static final String NOTIFICATION_INTERACTIONS_DID_FETCH = "INTERACTIONS_DID_FETCH"; // { successful: Boolean }
/**
* Sent when interaction manifest data is fetched for any conversation. Used by the log monitor.
@@ -78,25 +113,39 @@ public class ApptentiveNotifications {
*/
public static final String NOTIFICATION_MESSAGE_STORE_DID_CHANGE = "MESSAGE_STORE_DID_CHANGE"; // { messageStore: MessageStore }
+ /**
+ * Sent when advertiser id was resolved.
+ */
+ public static final String NOTIFICATION_ADVERTISER_ID_DID_RESOLVE = "ADVERTISER_ID_DID_RESOLVE"; // { successful: Boolean, clientInfo: AdvertisingIdClientInfo }
+
+ /**
+ * Sent when advertiser id was resolved.
+ */
+ public static final String NOTIFICATION_CONFIGURATION_FETCH_DID_FINISH = "CONFIGURATION_FETCH_DID_FINISH"; // { configuration: Configuration, conversation: Conversation }
+
/**
* Sent when log monitor starts capturing and storing logs.
*/
- public static final String NOTIFICATION_LOG_MONITOR_STARTED = "NOTIFICATION_LOG_MONITOR_STARTED";
+ public static final String NOTIFICATION_LOG_MONITOR_STARTED = "LOG_MONITOR_STARTED";
/**
* Sent when log monitor stops capturing and storing logs.
*/
- public static final String NOTIFICATION_LOG_MONITOR_STOPPED = "NOTIFICATION_LOG_MONITOR_STOPPED";
+ public static final String NOTIFICATION_LOG_MONITOR_STOPPED = "LOG_MONITOR_STOPPED";
// keys
public static final String NOTIFICATION_KEY_SUCCESSFUL = "successful";
public static final String NOTIFICATION_KEY_ACTIVITY = "activity";
public static final String NOTIFICATION_KEY_CONVERSATION = "conversation";
public static final String NOTIFICATION_KEY_CONVERSATION_ID = "conversationId";
+ public static final String NOTIFICATION_KEY_EVENT = "event";
public static final String NOTIFICATION_KEY_AUTHENTICATION_FAILED_REASON = "authenticationFailedReason";// type: AuthenticationFailedReason
public static final String NOTIFICATION_KEY_PAYLOAD = "payload";
+ public static final String NOTIFICATION_KEY_CONFIGURATION = "configuration";
public static final String NOTIFICATION_KEY_RESPONSE_CODE = "responseCode";
public static final String NOTIFICATION_KEY_RESPONSE_DATA = "responseData";
public static final String NOTIFICATION_KEY_MANIFEST = "manifest";
public static final String NOTIFICATION_KEY_MESSAGE_STORE = "messageStore";
+ public static final String NOTIFICATION_KEY_INTERVAL = "interval";
+ public static final String NOTIFICATION_KEY_ADVERTISER_CLIENT_INFO = "clientInfo";
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveNullInstance.java b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveNullInstance.java
index c94b9842f..f0b12769c 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveNullInstance.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveNullInstance.java
@@ -57,6 +57,11 @@ public void onActivityResumed(Activity activity) {
failMethodCall("onActivityResumed");
}
+ @Override
+ public void onActivityStopped(Activity activity) {
+ failMethodCall("onActivityStopped");
+ }
+
@Override
public void onAppEnterForeground() {
failMethodCall("onAppEnterForeground");
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/comm/ApptentiveClient.java b/apptentive/src/main/java/com/apptentive/android/sdk/comm/ApptentiveClient.java
index ca855cfb9..ca0101c66 100755
--- a/apptentive/src/main/java/com/apptentive/android/sdk/comm/ApptentiveClient.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/comm/ApptentiveClient.java
@@ -19,7 +19,7 @@ public class ApptentiveClient {
private static final String USER_AGENT_STRING = "Apptentive/%s (Android)"; // Format with SDK version string.
public static String getUserAgentString() {
- return String.format(USER_AGENT_STRING, Constants.APPTENTIVE_SDK_VERSION);
+ return String.format(USER_AGENT_STRING, Constants.getApptentiveSdkVersion());
}
/**
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 2dfe81746..f2c8507d6 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
@@ -6,7 +6,6 @@
package com.apptentive.android.sdk.comm;
-import com.apptentive.android.sdk.conversation.Conversation;
import com.apptentive.android.sdk.model.ConversationTokenRequest;
import com.apptentive.android.sdk.model.PayloadData;
import com.apptentive.android.sdk.network.HttpJsonRequest;
@@ -36,8 +35,8 @@ public class ApptentiveHttpClient implements PayloadRequestSender {
public static final String USER_AGENT_STRING = "Apptentive/%s (Android)"; // Format with SDK version string.
- public static final int DEFAULT_HTTP_CONNECT_TIMEOUT = 45000;
- public static final int DEFAULT_HTTP_SOCKET_TIMEOUT = 45000;
+ private static final int DEFAULT_HTTP_CONNECT_TIMEOUT = 45000;
+ private static final int DEFAULT_HTTP_SOCKET_TIMEOUT = 45000;
// Active API
private static final String ENDPOINT_CONVERSATION = "/conversation";
@@ -52,7 +51,6 @@ public class ApptentiveHttpClient implements PayloadRequestSender {
private final String apptentiveSignature;
private final String serverURL;
private final String userAgentString;
- private final HttpRequestManager httpRequestManager;
public ApptentiveHttpClient(String apptentiveKey, String apptentiveSignature, String serverURL) {
if (StringUtils.isNullOrEmpty(apptentiveKey)) {
@@ -67,11 +65,10 @@ public ApptentiveHttpClient(String apptentiveKey, String apptentiveSignature, St
throw new IllegalArgumentException("Illegal server URL: '" + serverURL + "'");
}
- this.httpRequestManager = new HttpRequestManager();
this.apptentiveKey = apptentiveKey;
this.apptentiveSignature = apptentiveSignature;
this.serverURL = serverURL;
- this.userAgentString = String.format(USER_AGENT_STRING, Constants.APPTENTIVE_SDK_VERSION);
+ this.userAgentString = String.format(USER_AGENT_STRING, Constants.getApptentiveSdkVersion());
}
//region API Requests
@@ -188,7 +185,7 @@ public HttpJsonRequest createFirstLoginRequest(String token, AppRelease appRelea
* Returns the first request with a given tag or null
is not found
*/
public HttpRequest findRequest(String tag) {
- return httpRequestManager.findRequest(tag);
+ return HttpRequestManager.sharedManager().findRequest(tag);
}
//endregion
@@ -272,7 +269,7 @@ private RawHttpRequest createRawRequest(String endpoint, byte[] data, HttpReques
}
private void setupRequestDefaults(HttpRequest request) {
- request.setRequestManager(httpRequestManager);
+ request.setRequestManager(HttpRequestManager.sharedManager());
request.setRequestProperty("User-Agent", userAgentString);
request.setRequestProperty("Connection", "Keep-Alive");
request.setRequestProperty("Accept-Encoding", "gzip");
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/conversation/Conversation.java b/apptentive/src/main/java/com/apptentive/android/sdk/conversation/Conversation.java
index 26a161b18..044a3e1b2 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/conversation/Conversation.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/conversation/Conversation.java
@@ -15,6 +15,7 @@
import com.apptentive.android.sdk.comm.ApptentiveHttpClient;
import com.apptentive.android.sdk.debug.Assert;
import com.apptentive.android.sdk.model.DevicePayload;
+import com.apptentive.android.sdk.model.EventPayload;
import com.apptentive.android.sdk.model.Payload;
import com.apptentive.android.sdk.model.PersonPayload;
import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction;
@@ -120,7 +121,7 @@ protected void execute() {
try {
saveConversationData();
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while saving conversation data");
+ ApptentiveLog.e(CONVERSATION, e, "Exception while saving conversation data");
}
}
};
@@ -151,6 +152,11 @@ public void startListeningForChanges() {
//region Payloads
public void addPayload(Payload payload) {
+ // TODO: figure out a better way of detecting new events
+ if (payload instanceof EventPayload) {
+ notifyEventGenerated((EventPayload) payload);
+ }
+
payload.setLocalConversationIdentifier(notNull(getLocalIdentifier()));
payload.setConversationId(getConversationId());
payload.setToken(getConversationToken());
@@ -160,6 +166,11 @@ public void addPayload(Payload payload) {
ApptentiveInternal.getInstance().getApptentiveTaskManager().addPayload(payload);
}
+ private void notifyEventGenerated(EventPayload payload) {
+ ApptentiveNotificationCenter.defaultCenter()
+ .postNotification(NOTIFICATION_EVENT_GENERATED, NOTIFICATION_KEY_EVENT, payload);
+ }
+
//endregion
//region Interactions
@@ -167,12 +178,12 @@ public void addPayload(Payload payload) {
/**
* Returns an Interaction for eventLabel
if there is one that can be displayed.
*/
- public Interaction getApplicableInteraction(String eventLabel) {
+ public Interaction getApplicableInteraction(String eventLabel, boolean verbose) {
String targetsString = getTargets();
if (targetsString != null) {
try {
Targets targets = new Targets(getTargets());
- String interactionId = targets.getApplicableInteraction(eventLabel);
+ String interactionId = targets.getApplicableInteraction(eventLabel, verbose);
if (interactionId != null) {
String interactionsString = getInteractions();
if (interactionsString != null) {
@@ -181,13 +192,17 @@ public Interaction getApplicableInteraction(String eventLabel) {
}
}
} catch (JSONException e) {
- ApptentiveLog.e(e, "Exception while getting applicable interaction: %s", eventLabel);
+ ApptentiveLog.e(INTERACTIONS, e, "Exception while getting applicable interaction: %s", eventLabel);
}
}
return null;
}
- void fetchInteractions(Context context) {
+ public void fetchInteractions(Context context) {
+ if (!isPollForInteractions()) {
+ ApptentiveLog.d(CONVERSATION, "Interaction polling is turned off. Skipping fetch.");
+ return;
+ }
boolean cacheExpired = getInteractionExpiration() < Util.currentTimeSeconds();
if (cacheExpired || RuntimeUtils.isAppDebuggable(context)) {
ApptentiveHttpClient httpClient = ApptentiveInternal.getInstance().getApptentiveHttpClient();
@@ -218,7 +233,7 @@ public void onFinish(HttpJsonRequest request) {
ApptentiveLog.e(CONVERSATION, "Unable to save interactionManifest.");
}
} catch (JSONException e) {
- ApptentiveLog.e(e, "Invalid InteractionManifest received.");
+ ApptentiveLog.e(CONVERSATION, e, "Invalid InteractionManifest received.");
}
ApptentiveLog.v(CONVERSATION, "Fetching new Interactions task finished");
@@ -291,10 +306,10 @@ public void storeInteractionManifest(String interactionManifest) {
setTargets(targets.toString());
setInteractions(interactions.toString());
} else {
- ApptentiveLog.e("Unable to save InteractionManifest.");
+ ApptentiveLog.e(CONVERSATION, "Unable to save InteractionManifest.");
}
} catch (JSONException e) {
- ApptentiveLog.w("Invalid InteractionManifest received.");
+ ApptentiveLog.w(CONVERSATION, "Invalid InteractionManifest received.");
}
}
@@ -305,7 +320,7 @@ public void storeInteractionManifest(String interactionManifest) {
public void scheduleSaveConversationData() {
boolean scheduled = DispatchQueue.backgroundQueue().dispatchAsyncOnce(saveConversationTask, 100L);
if (scheduled) {
- ApptentiveLog.d(CONVERSATION, "Scheduling conversation save.");
+ ApptentiveLog.v(CONVERSATION, "Scheduling conversation save.");
} else {
ApptentiveLog.d(CONVERSATION, "Conversation save already scheduled.");
}
@@ -316,10 +331,10 @@ public void scheduleSaveConversationData() {
* if succeed.
*/
private synchronized void saveConversationData() throws SerializerException {
- if (ApptentiveLog.canLog(ApptentiveLog.Level.VERY_VERBOSE)) {
- ApptentiveLog.vv(CONVERSATION, "Saving %sconversation data...", hasState(LOGGED_IN) ? "encrypted " : "");
- ApptentiveLog.vv(CONVERSATION, "EventData: %s", getEventData().toString());
- ApptentiveLog.vv(CONVERSATION, "Messages: %s", messageManager.getMessageStore().toString());
+ if (ApptentiveLog.canLog(ApptentiveLog.Level.VERBOSE)) {
+ ApptentiveLog.v(CONVERSATION, "Saving %sconversation data...", hasState(LOGGED_IN) ? "encrypted " : "");
+ ApptentiveLog.v(CONVERSATION, "EventData: %s", getEventData().toString());
+ ApptentiveLog.v(CONVERSATION, "Messages: %s", messageManager.getMessageStore().toString());
}
long start = System.currentTimeMillis();
@@ -333,7 +348,7 @@ private synchronized void saveConversationData() throws SerializerException {
}
serializer.serialize(conversationData);
- ApptentiveLog.vv(CONVERSATION, "Conversation data saved (took %d ms)", System.currentTimeMillis() - start);
+ ApptentiveLog.v(CONVERSATION, "Conversation data saved (took %d ms)", System.currentTimeMillis() - start);
}
synchronized void loadConversationData() throws SerializerException {
@@ -637,11 +652,11 @@ public MessageManager getMessageManager() {
return messageManager;
}
- synchronized File getConversationDataFile() {
+ public synchronized File getConversationDataFile() {
return conversationDataFile;
}
- synchronized File getConversationMessagesFile() {
+ public synchronized File getConversationMessagesFile() {
return conversationMessagesFile;
}
@@ -653,7 +668,7 @@ void setEncryptionKey(String encryptionKey) {
this.encryptionKey = encryptionKey;
}
- String getUserId() {
+ public String getUserId() {
return userId;
}
@@ -680,7 +695,7 @@ public void setPushIntegration(int pushProvider, String token) {
integrationConfig.setAmazonAwsSns(item);
break;
default:
- ApptentiveLog.e("Invalid pushProvider: %d", pushProvider);
+ ApptentiveLog.e(CONVERSATION, "Invalid pushProvider: %d", pushProvider);
break;
}
}
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 22dd0533e..93c84f29d 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
@@ -12,7 +12,6 @@
import com.apptentive.android.sdk.Apptentive;
import com.apptentive.android.sdk.Apptentive.LoginCallback;
-import com.apptentive.android.sdk.ApptentiveHelper;
import com.apptentive.android.sdk.ApptentiveInternal;
import com.apptentive.android.sdk.ApptentiveLog;
import com.apptentive.android.sdk.comm.ApptentiveHttpClient;
@@ -40,8 +39,6 @@
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;
-import com.apptentive.android.sdk.util.threading.DispatchTask;
import org.json.JSONObject;
@@ -52,13 +49,13 @@
import static com.apptentive.android.sdk.ApptentiveHelper.checkConversationQueue;
import static com.apptentive.android.sdk.ApptentiveHelper.conversationQueue;
-import static com.apptentive.android.sdk.ApptentiveLog.Level.VERY_VERBOSE;
+import static com.apptentive.android.sdk.ApptentiveLog.Level.VERBOSE;
+import static com.apptentive.android.sdk.ApptentiveLog.hideIfSanitized;
import static com.apptentive.android.sdk.ApptentiveLogTag.*;
import static com.apptentive.android.sdk.ApptentiveNotifications.*;
import static com.apptentive.android.sdk.conversation.ConversationState.*;
import static com.apptentive.android.sdk.debug.Assert.*;
-import static com.apptentive.android.sdk.debug.Tester.*;
-import static com.apptentive.android.sdk.debug.TesterEvent.*;
+import static com.apptentive.android.sdk.util.Constants.CONVERSATION_METADATA_FILE;
import static com.apptentive.android.sdk.util.StringUtils.isNullOrEmpty;
/**
@@ -70,9 +67,6 @@
*
*/
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";
@@ -145,85 +139,126 @@ public boolean loadActiveConversation(Context context) {
try {
// resolving metadata
- ApptentiveLog.vv(CONVERSATION, "Resolving metadata...");
+ ApptentiveLog.v(CONVERSATION, "Resolving metadata...");
conversationMetadata = resolveMetadata();
- if (ApptentiveLog.canLog(VERY_VERBOSE)) {
+ if (ApptentiveLog.canLog(VERBOSE)) {
printMetadata(conversationMetadata, "Loaded Metadata");
}
// attempt to load existing conversation
- ApptentiveLog.vv(CONVERSATION, "Loading active conversation...");
+ ApptentiveLog.v(CONVERSATION, "Loading active conversation...");
setActiveConversation(loadActiveConversationGuarded());
if (activeConversation != null) {
+ ApptentiveNotificationCenter.defaultCenter()
+ .postNotification(NOTIFICATION_CONVERSATION_LOAD_DID_FINISH,
+ NOTIFICATION_KEY_CONVERSATION, activeConversation,
+ NOTIFICATION_KEY_SUCCESSFUL, true);
+
activeConversation.startListeningForChanges();
activeConversation.scheduleSaveConversationData();
- dispatchDebugEvent(EVT_CONVERSATION_LOAD,
- "successful", Boolean.TRUE,
- "conversation_state", activeConversation.getState().toString(),
- "conversation_identifier", activeConversation.getConversationId());
-
handleConversationStateChange(activeConversation);
return true;
}
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while loading active conversation");
+ ApptentiveLog.e(CONVERSATION, e, "Exception while loading active conversation");
}
- dispatchDebugEvent(EVT_CONVERSATION_LOAD, "successful", Boolean.FALSE);
+ ApptentiveNotificationCenter.defaultCenter()
+ .postNotification(NOTIFICATION_CONVERSATION_LOAD_DID_FINISH,
+ NOTIFICATION_KEY_SUCCESSFUL, false);
+
return false;
}
- private Conversation loadActiveConversationGuarded() throws IOException, SerializerException {
+ private @Nullable Conversation loadActiveConversationGuarded() throws IOException {
+ // try to load an active conversation from metadata first
+ try {
+ if (conversationMetadata.hasItems()) {
+ return loadConversationFromMetadata(conversationMetadata);
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while loading conversation");
+ }
+
+ // no active conversations: create a new one
+ ApptentiveLog.i(CONVERSATION, "Creating 'anonymous' conversation...");
+ File dataFile = new File(apptentiveConversationsStorageDir, "conversation-" + Util.generateRandomFilename());
+ File messagesFile = new File(apptentiveConversationsStorageDir, "messages-" + Util.generateRandomFilename());
+ Conversation conversation = new Conversation(dataFile, messagesFile);
+
+ // attempt to migrate a legacy conversation (if any)
+ if (migrateLegacyConversation(conversation)) {
+ return conversation;
+ }
+
+ // if there is no Legacy Conversation, then just connect it to the server.
+ conversation.setState(ANONYMOUS_PENDING);
+ fetchConversationToken(conversation);
+ return conversation;
+ }
+
+ /**
+ * Attempts to load an existing conversation based on metadata file
+ * @return null
is only logged out conversations available
+ */
+ private @Nullable Conversation loadConversationFromMetadata(ConversationMetadata metadata) throws SerializerException {
// we're going to scan metadata in attempt to find existing conversations
ConversationMetadataItem item;
// if the user was logged in previously - we should have an active conversation
- item = conversationMetadata.findItem(LOGGED_IN);
+ item = metadata.findItem(LOGGED_IN);
if (item != null) {
- ApptentiveLog.v(CONVERSATION, "Loading logged-in conversation...");
+ ApptentiveLog.i(CONVERSATION, "Loading 'logged-in' conversation...");
return loadConversation(item);
}
// if no users were logged in previously - we might have an anonymous conversation
- item = conversationMetadata.findItem(ANONYMOUS);
+ item = metadata.findItem(ANONYMOUS);
if (item != null) {
- ApptentiveLog.v(CONVERSATION, "Loading anonymous conversation...");
+ ApptentiveLog.i(CONVERSATION, "Loading 'anonymous' conversation...");
return loadConversation(item);
}
// check if we have a 'pending' anonymous conversation
- item = conversationMetadata.findItem(ANONYMOUS_PENDING);
+ item = metadata.findItem(ANONYMOUS_PENDING);
if (item != null) {
- ApptentiveLog.v(CONVERSATION, "Loading anonymous pending conversation...");
+ ApptentiveLog.i(CONVERSATION, "Loading 'anonymous pending' conversation...");
final Conversation conversation = loadConversation(item);
fetchConversationToken(conversation);
return conversation;
}
// check if we have a 'legacy pending' conversation
- item = conversationMetadata.findItem(LEGACY_PENDING);
+ item = metadata.findItem(LEGACY_PENDING);
if (item != null) {
- ApptentiveLog.v(CONVERSATION, "Loading legacy pending conversation...");
+ ApptentiveLog.i(CONVERSATION, "Loading 'legacy pending' conversation...");
final Conversation conversation = loadConversation(item);
fetchLegacyConversation(conversation);
return conversation;
}
- // Check for only LOGGED_OUT Conversations
- if (conversationMetadata.hasItems()) {
- ApptentiveLog.v(CONVERSATION, "Can't load conversation: only 'logged-out' conversations available");
- return null;
- }
+ // we only have LOGGED_OUT conversations
+ ApptentiveLog.i(CONVERSATION, "No active conversations to load: only 'logged-out' conversations available");
+ return null;
+ }
- // No conversation exists: Create a new one
- ApptentiveLog.v(CONVERSATION, "Can't load conversation: creating anonymous conversation...");
- File dataFile = new File(apptentiveConversationsStorageDir, "conversation-" + Util.generateRandomFilename());
- File messagesFile = new File(apptentiveConversationsStorageDir, "messages-" + Util.generateRandomFilename());
- Conversation anonymousConversation = new Conversation(dataFile, messagesFile);
+ /**
+ * Attempts to migrate a legacy conversation
+ * @return true
is succeed
+ */
+ private boolean migrateLegacyConversation(Conversation conversation) {
+ try {
+ return migrateLegacyConversationGuarded(conversation);
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while migrating legacy conversation");
+ }
+ return false;
+ }
+ private boolean migrateLegacyConversationGuarded(Conversation conversation) {
// If there is a Legacy Conversation, migrate it into the new Conversation object.
// Check whether migration is needed.
// No Conversations exist in the meta-data.
@@ -231,38 +266,38 @@ private Conversation loadActiveConversationGuarded() throws IOException, Seriali
final SharedPreferences prefs = ApptentiveInternal.getInstance().getGlobalSharedPrefs();
String legacyConversationToken = prefs.getString(Constants.PREF_KEY_CONVERSATION_TOKEN, null);
if (!isNullOrEmpty(legacyConversationToken)) {
+ ApptentiveLog.i(CONVERSATION, "Migrating an existing legacy conversation to the new format...");
+
String lastSeenVersionString = prefs.getString(Constants.PREF_KEY_LAST_SEEN_SDK_VERSION, null);
Apptentive.Version version4 = new Apptentive.Version();
version4.setVersion("4.0.0");
Apptentive.Version lastSeenVersion = new Apptentive.Version();
lastSeenVersion.setVersion(lastSeenVersionString);
if (lastSeenVersionString != null && lastSeenVersion.compareTo(version4) < 0) {
+ conversation.setState(LEGACY_PENDING);
+ conversation.setConversationToken(legacyConversationToken);
- anonymousConversation.setState(LEGACY_PENDING);
- anonymousConversation.setConversationToken(legacyConversationToken);
-
- Migrator migrator = new Migrator(getContext(), prefs, anonymousConversation);
+ Migrator migrator = new Migrator(getContext(), prefs, conversation);
migrator.migrate();
- ApptentiveLog.v("Fetching legacy conversation...");
- fetchLegacyConversation(anonymousConversation)
- // remove legacy key when request is finished
- .addListener(new HttpRequest.Adapter() {
- @Override
- public void onFinish(HttpRequest request) {
- prefs.edit()
- .remove(Constants.PREF_KEY_CONVERSATION_TOKEN)
- .apply();
- }
- });
- return anonymousConversation;
+ ApptentiveLog.v(CONVERSATION, "Fetching legacy conversation...");
+ fetchLegacyConversation(conversation)
+ // remove legacy key when request is finished
+ .addListener(new HttpRequest.Adapter() {
+ @Override
+ public void onFinish(HttpRequest request) {
+ prefs.edit()
+ .remove(Constants.PREF_KEY_CONVERSATION_TOKEN)
+ .apply();
+ }
+ });
+ return true;
}
+
+ ApptentiveLog.w(CONVERSATION, "Unable to migrate legacy conversation: data format is outdated!");
}
- // If there is no Legacy Conversation, then just connect it to the server.
- anonymousConversation.setState(ANONYMOUS_PENDING);
- fetchConversationToken(anonymousConversation);
- return anonymousConversation;
+ return false;
}
private HttpRequest fetchLegacyConversation(final Conversation conversation) {
@@ -315,7 +350,7 @@ public void onFinish(HttpJsonRequest request) {
// handle state change
handleConversationStateChange(conversation);
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while handling legacy conversation id");
+ ApptentiveLog.e(CONVERSATION, e, "Exception while handling legacy conversation id");
}
}
@@ -325,7 +360,7 @@ public void onCancel(HttpJsonRequest request) {
@Override
public void onFail(HttpJsonRequest request, String reason) {
- ApptentiveLog.e("Failed to fetch legacy conversation id: %s", reason);
+ ApptentiveLog.e(CONVERSATION, "Failed to fetch legacy conversation id: %s", reason);
}
});
@@ -363,10 +398,14 @@ private Conversation loadConversation(ConversationMetadataItem item) throws Seri
private HttpRequest fetchConversationToken(final Conversation conversation) {
checkConversationQueue();
+ // post notification
+ notifyFetchStarted(conversation);
+
// check if context is lost
final Context context = getContext();
if (context == null) {
ApptentiveLog.w(CONVERSATION, "Unable to fetch conversation token: context reference is lost");
+ notifyFetchFinished(conversation, false);
return null;
}
@@ -377,8 +416,7 @@ private HttpRequest fetchConversationToken(final Conversation conversation) {
return existingRequest;
}
- ApptentiveLog.i(CONVERSATION, "Fetching Configuration token task started.");
- dispatchDebugEvent(EVT_CONVERSATION_WILL_FETCH_TOKEN);
+ ApptentiveLog.d(CONVERSATION, "Started fetching conversation token...");
// Try to fetch a new one from the server.
ConversationTokenRequest conversationTokenRequest = new ConversationTokenRequest();
@@ -400,19 +438,19 @@ public void onFinish(HttpJsonRequest request) {
try {
JSONObject root = request.getResponseObject();
String conversationToken = root.getString("token");
- ApptentiveLog.d(CONVERSATION, "ConversationToken: " + conversationToken);
+ ApptentiveLog.d(CONVERSATION, "ConversationToken: " + hideIfSanitized(conversationToken));
String conversationId = root.getString("id");
ApptentiveLog.d(CONVERSATION, "New Conversation id: %s", conversationId);
if (isNullOrEmpty(conversationToken)) {
ApptentiveLog.e(CONVERSATION, "Can't fetch conversation: missing 'token'");
- dispatchDebugEvent(EVT_CONVERSATION_DID_FETCH_TOKEN, false);
+ notifyFetchFinished(conversation, false);
return;
}
if (isNullOrEmpty(conversationId)) {
ApptentiveLog.e(CONVERSATION, "Can't fetch conversation: missing 'id'");
- dispatchDebugEvent(EVT_CONVERSATION_DID_FETCH_TOKEN, false);
+ notifyFetchFinished(conversation, false);
return;
}
@@ -430,24 +468,24 @@ public void onFinish(HttpJsonRequest request) {
ApptentiveLog.d(CONVERSATION, "PersonId: " + personId);
conversation.getPerson().setId(personId);
- dispatchDebugEvent(EVT_CONVERSATION_DID_FETCH_TOKEN, true);
+ notifyFetchFinished(conversation, true);
handleConversationStateChange(conversation);
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while handling conversation token");
- dispatchDebugEvent(EVT_CONVERSATION_DID_FETCH_TOKEN, false);
+ ApptentiveLog.e(CONVERSATION, e, "Exception while handling conversation token");
+ notifyFetchFinished(conversation, false);
}
}
@Override
public void onCancel(HttpJsonRequest request) {
- dispatchDebugEvent(EVT_CONVERSATION_DID_FETCH_TOKEN, false);
+ notifyFetchFinished(conversation, false);
}
@Override
public void onFail(HttpJsonRequest request, String reason) {
- ApptentiveLog.w("Failed to fetch conversation token: %s", reason);
- dispatchDebugEvent(EVT_CONVERSATION_DID_FETCH_TOKEN, false);
+ ApptentiveLog.w(CONVERSATION, "Failed to fetch conversation token: %s", reason);
+ notifyFetchFinished(conversation, false);
}
});
@@ -457,23 +495,34 @@ public void onFail(HttpJsonRequest request, String reason) {
return request;
}
+ private void notifyFetchStarted(Conversation conversation) {
+ ApptentiveNotificationCenter.defaultCenter()
+ .postNotification(NOTIFICATION_CONVERSATION_TOKEN_WILL_FETCH,
+ NOTIFICATION_KEY_CONVERSATION, conversation);
+ }
+
+ private void notifyFetchFinished(Conversation conversation, boolean successful) {
+ ApptentiveNotificationCenter.defaultCenter()
+ .postNotification(NOTIFICATION_CONVERSATION_TOKEN_DID_FETCH,
+ NOTIFICATION_KEY_CONVERSATION, conversation,
+ NOTIFICATION_KEY_SUCCESSFUL, successful ? Boolean.TRUE : Boolean.FALSE);
+ }
+
//endregion
//region Conversation fetching
private void handleConversationStateChange(Conversation conversation) {
+ ApptentiveLog.d(CONVERSATION, "Conversation state changed: %s", conversation);
checkConversationQueue();
assertTrue(conversation != null && !conversation.hasState(UNDEFINED));
if (conversation != null && !conversation.hasState(UNDEFINED)) {
- dispatchDebugEvent(EVT_CONVERSATION_STATE_CHANGE,
- "conversation_state", conversation.getState().toString(),
- "conversation_identifier", conversation.getConversationId());
ApptentiveNotificationCenter.defaultCenter()
.postNotification(NOTIFICATION_CONVERSATION_STATE_DID_CHANGE,
- ObjectUtils.toMap(NOTIFICATION_KEY_CONVERSATION, conversation));
+ NOTIFICATION_KEY_CONVERSATION, conversation);
if (conversation.hasActiveState()) {
if (appIsInForeground) {
@@ -496,7 +545,7 @@ private void handleConversationStateChange(Conversation conversation) {
}
updateMetadataItems(conversation);
- if (ApptentiveLog.canLog(VERY_VERBOSE)) {
+ if (ApptentiveLog.canLog(VERBOSE)) {
printMetadata(conversationMetadata, "Updated Metadata");
}
}
@@ -507,11 +556,11 @@ private void fetchAppConfiguration(Conversation conversation) {
try {
fetchAppConfigurationGuarded(conversation);
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while fetching app configuration");
+ ApptentiveLog.e(CONVERSATION, e, "Exception while fetching app configuration");
}
}
- private void fetchAppConfigurationGuarded(Conversation conversation) {
+ private void fetchAppConfigurationGuarded(final Conversation conversation) {
ApptentiveLog.d(APP_CONFIGURATION, "Fetching app configuration...");
HttpRequest existingRequest = getHttpClient().findRequest(TAG_FETCH_APP_CONFIGURATION_REQUEST);
@@ -544,8 +593,16 @@ public void onFinish(HttpJsonRequest request) {
Configuration config = new Configuration(request.getResponseObject().toString());
config.setConfigurationCacheExpirationMillis(System.currentTimeMillis() + cacheSeconds * 1000);
config.save();
+
+ ApptentiveNotificationCenter.defaultCenter()
+ .postNotification(NOTIFICATION_CONFIGURATION_FETCH_DID_FINISH,
+ NOTIFICATION_KEY_CONFIGURATION, config,
+ NOTIFICATION_KEY_CONVERSATION, conversation);
+
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while parsing app configuration response");
+ ApptentiveLog.e(CONVERSATION, e, "Exception while parsing app configuration response");
+ ApptentiveNotificationCenter.defaultCenter()
+ .postNotification(NOTIFICATION_CONFIGURATION_FETCH_DID_FINISH, NOTIFICATION_KEY_CONFIGURATION, null);
}
}
@@ -566,11 +623,11 @@ public void onFail(HttpJsonRequest request, String reason) {
private void updateMetadataItems(Conversation conversation) {
checkConversationQueue();
- ApptentiveLog.vv("Updating metadata: state=%s localId=%s conversationId=%s token=%s",
+ ApptentiveLog.v(CONVERSATION, "Updating metadata: state=%s localId=%s conversationId=%s token=%s",
conversation.getState(),
conversation.getLocalIdentifier(),
conversation.getConversationId(),
- conversation.getConversationToken());
+ hideIfSanitized(conversation.getConversationToken()));
// if the conversation is 'logged-in' we should not have any other 'logged-in' items in metadata
if (conversation.hasState(LOGGED_IN)) {
@@ -620,20 +677,17 @@ private ConversationMetadata resolveMetadata() {
checkConversationQueue();
try {
- File metaFile = new File(apptentiveConversationsStorageDir, CONVERSATION_METADATA_PATH);
+ File metaFile = new File(apptentiveConversationsStorageDir, CONVERSATION_METADATA_FILE);
if (metaFile.exists()) {
- ApptentiveLog.v(CONVERSATION, "Loading meta file: " + metaFile);
- final ConversationMetadata metadata = ObjectSerialization.deserialize(metaFile, ConversationMetadata.class);
- dispatchDebugEvent(EVT_CONVERSATION_METADATA_LOAD, true);
- return metadata;
+ ApptentiveLog.v(CONVERSATION, "Loading metadata file: %s", metaFile);
+ return ObjectSerialization.deserialize(metaFile, ConversationMetadata.class);
} else {
- ApptentiveLog.v(CONVERSATION, "Meta file does not exist: " + metaFile);
+ ApptentiveLog.v(CONVERSATION, "Metadata file not found: %s", metaFile);
}
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while loading conversation metadata");
+ ApptentiveLog.e(CONVERSATION, e, "Exception while loading conversation metadata");
}
- dispatchDebugEvent(EVT_CONVERSATION_METADATA_LOAD, false);
return new ConversationMetadata();
}
@@ -641,11 +695,11 @@ private void saveMetadata() {
checkConversationQueue();
try {
- if (ApptentiveLog.canLog(VERY_VERBOSE)) {
- ApptentiveLog.vv(CONVERSATION, "Saving metadata: ", conversationMetadata.toString());
+ if (ApptentiveLog.canLog(VERBOSE)) {
+ ApptentiveLog.v(CONVERSATION, "Saving metadata: ", conversationMetadata.toString());
}
long start = System.currentTimeMillis();
- File metaFile = new File(apptentiveConversationsStorageDir, CONVERSATION_METADATA_PATH);
+ File metaFile = new File(apptentiveConversationsStorageDir, CONVERSATION_METADATA_FILE);
ObjectSerialization.serialize(metaFile, conversationMetadata);
ApptentiveLog.v(CONVERSATION, "Saved metadata (took %d ms)", System.currentTimeMillis() - start);
} catch (Exception e) {
@@ -683,13 +737,13 @@ private void requestLoggedInConversation(final String token, final LoginCallback
final Jwt jwt = Jwt.decode(token);
userId = jwt.getPayload().optString("sub");
if (StringUtils.isNullOrEmpty(userId)) {
- ApptentiveLog.e("Error while extracting user id: Missing field \"sub\"");
+ ApptentiveLog.e(CONVERSATION, "Error while extracting user id: Missing field \"sub\"");
callback.onLoginFail("Error while extracting user id: Missing field \"sub\"");
return;
}
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while extracting user id");
+ ApptentiveLog.e(CONVERSATION, e, "Exception while extracting user id");
callback.onLoginFail("Exception while extracting user id");
return;
}
@@ -707,7 +761,7 @@ public boolean accept(ConversationMetadataItem item) {
});
if (conversationItem == null) {
- ApptentiveLog.w("No conversation found matching user: '%s'. Logging in as new user.", userId);
+ ApptentiveLog.w(CONVERSATION, "No conversation found matching user: '%s'. Logging in as new user.", userId);
sendFirstLoginRequest(userId, token, callback);
return;
}
@@ -763,7 +817,7 @@ public void onFail(HttpRequest request, String reason) {
break;
case LOGGED_IN:
if (StringUtils.equal(activeConversation.getUserId(), userId)) {
- ApptentiveLog.w("Already logged in as \"%s\"", userId);
+ ApptentiveLog.w(CONVERSATION, "Already logged in as \"%s\"", userId);
callback.onLoginFinish();
return;
}
@@ -787,7 +841,7 @@ public void onFinish(HttpJsonRequest request) {
final String incomingConversationId = responseObject.getString("id");
handleLoginFinished(incomingConversationId, userId, token, encryptionKey);
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while parsing login response");
+ ApptentiveLog.e(CONVERSATION, e, "Exception while parsing login response");
handleLoginFailed("Internal error");
}
}
@@ -849,7 +903,7 @@ public boolean accept(ConversationMetadataItem item) {
// notify delegate
callback.onLoginFinish();
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while creating logged-in conversation");
+ ApptentiveLog.e(CONVERSATION, e, "Exception while creating logged-in conversation");
handleLoginFailed("Internal error");
}
}
@@ -878,7 +932,7 @@ public void onFinish(HttpJsonRequest request) {
final String incomingConversationId = responseObject.getString("id");
handleLoginFinished(incomingConversationId, userId, token, encryptionKey);
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while parsing login response");
+ ApptentiveLog.e(CONVERSATION, e, "Exception while parsing login response");
handleLoginFailed("Internal error");
}
}
@@ -937,7 +991,7 @@ public boolean accept(ConversationMetadataItem item) {
// notify delegate
callback.onLoginFinish();
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while creating logged-in conversation");
+ ApptentiveLog.e(CONVERSATION, e, "Exception while creating logged-in conversation");
handleLoginFailed("Internal error");
}
}
@@ -955,7 +1009,7 @@ public void logout() {
if (activeConversation != null) {
switch (activeConversation.getState()) {
case LOGGED_IN:
- ApptentiveLog.d("Ending active conversation.");
+ ApptentiveLog.d(CONVERSATION, "Ending active conversation.");
EngagementModule.engageInternal(getContext(), activeConversation, "logout");
// Post synchronously to ensure logout payload can be sent before destroying the logged in conversation.
ApptentiveNotificationCenter.defaultCenter().postNotification(NOTIFICATION_CONVERSATION_WILL_LOGOUT, ObjectUtils.toMap(NOTIFICATION_KEY_CONVERSATION, activeConversation));
@@ -972,7 +1026,7 @@ public void logout() {
} else {
ApptentiveLog.w(CONVERSATION, "Attempted to logout(), but there was no Active Conversation.");
}
- dispatchDebugEvent(EVT_LOGOUT);
+
}
//endregion
@@ -982,7 +1036,7 @@ public void logout() {
private void printMetadata(ConversationMetadata metadata, String title) {
List items = metadata.getItems();
if (items.isEmpty()) {
- ApptentiveLog.vv(CONVERSATION, "%s (%d item(s))", title, items.size());
+ ApptentiveLog.v(CONVERSATION, "%s (%d item(s))", title, items.size());
return;
}
@@ -1004,26 +1058,27 @@ private void printMetadata(ConversationMetadata metadata, String title) {
item.localConversationId,
item.conversationId,
item.userId,
- item.dataFile,
- item.messagesFile,
- item.conversationToken,
- item.encryptionKey
+ hideIfSanitized(item.dataFile),
+ hideIfSanitized(item.messagesFile),
+ hideIfSanitized(item.conversationToken),
+ hideIfSanitized(item.encryptionKey)
};
}
- ApptentiveLog.vv(CONVERSATION, "%s (%d item(s))\n%s", title, items.size(), StringUtils.table(rows));
+ ApptentiveLog.v(CONVERSATION, "%s (%d item(s))\n%s", title, items.size(), StringUtils.table(rows));
}
//endregion
//region Getters/Setters
- public Conversation getActiveConversation() {
+ public @Nullable Conversation getActiveConversation() {
checkConversationQueue(); // we should only access the conversation on a dedicated queue
return activeConversation;
}
- private synchronized void setActiveConversation(Conversation conversation) {
+ private void setActiveConversation(@Nullable Conversation conversation) {
+ checkConversationQueue(); // we should only access the conversation on a dedicated queue
this.activeConversation = conversation;
this.activeConversationProxy = conversation != null ? new ConversationProxy(conversation) : null;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/conversation/FileMessageStore.java b/apptentive/src/main/java/com/apptentive/android/sdk/conversation/FileMessageStore.java
index c9f542555..d112e74ce 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/conversation/FileMessageStore.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/conversation/FileMessageStore.java
@@ -6,6 +6,8 @@
package com.apptentive.android.sdk.conversation;
+import android.support.v4.util.AtomicFile;
+
import com.apptentive.android.sdk.ApptentiveLog;
import com.apptentive.android.sdk.debug.Assert;
import com.apptentive.android.sdk.model.ApptentiveMessage;
@@ -26,6 +28,7 @@
import java.util.ArrayList;
import java.util.List;
+import static com.apptentive.android.sdk.ApptentiveLogTag.MESSAGES;
import static com.apptentive.android.sdk.util.Util.readNullableBoolean;
import static com.apptentive.android.sdk.util.Util.readNullableDouble;
import static com.apptentive.android.sdk.util.Util.readNullableUTF;
@@ -107,7 +110,7 @@ public synchronized List getAllMessages() throws Exception {
for (MessageEntry entry : messageEntries) {
ApptentiveMessage apptentiveMessage = MessageFactory.fromJson(entry.json);
if (apptentiveMessage == null) {
- ApptentiveLog.e("Error parsing Record json from database: %s", entry.json);
+ ApptentiveLog.e(MESSAGES, "Error parsing Record json from database: %s", entry.json);
continue;
}
apptentiveMessage.setState(ApptentiveMessage.State.parse(entry.state));
@@ -196,7 +199,7 @@ private synchronized void readFromFile() {
messageEntries.addAll(entries);
}
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while reading entries");
+ ApptentiveLog.e(MESSAGES, e, "Exception while reading entries");
}
}
@@ -223,22 +226,26 @@ private synchronized void writeToFile() {
try {
writeToFileGuarded();
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while saving messages");
+ ApptentiveLog.e(MESSAGES, e, "Exception while saving messages");
}
shouldFetchFromFile = false; // mark it as not shouldFetchFromFile to keep a memory version
}
private void writeToFileGuarded() throws IOException {
- DataOutputStream dos = null;
+ AtomicFile atomicFile = new AtomicFile(file);
+ FileOutputStream stream = null;
try {
- dos = new DataOutputStream(new FileOutputStream(file));
+ stream = atomicFile.startWrite();
+ DataOutputStream dos = new DataOutputStream(stream);
dos.writeByte(VERSION);
dos.writeInt(messageEntries.size());
for (MessageEntry entry : messageEntries) {
entry.writeExternal(dos);
}
- } finally {
- Util.ensureClosed(dos);
+ atomicFile.finishWrite(stream);
+ } catch (Exception e) {
+ atomicFile.failWrite(stream);
+ throw new IOException(e);
}
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/debug/ApptentiveAttachmentFileProvider.java b/apptentive/src/main/java/com/apptentive/android/sdk/debug/ApptentiveAttachmentFileProvider.java
index cc2aa0c21..4fa162f00 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/debug/ApptentiveAttachmentFileProvider.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/debug/ApptentiveAttachmentFileProvider.java
@@ -15,6 +15,8 @@
import android.os.ParcelFileDescriptor;
import android.util.Log;
+import com.apptentive.android.sdk.ApptentiveLog;
+
import java.io.File;
import java.io.FileNotFoundException;
@@ -57,7 +59,7 @@ public ParcelFileDescriptor openFile(Uri uri, String mode)
// E.g.
// 'content://com.apptentive.android.sdk.debug.ApptentiveAttachmentFileProvider/log.txt'
// Take this and build the path to the file
- String fileLocation = getContext().getCacheDir() + File.separator
+ String fileLocation = ApptentiveLog.getLogsDirectory(getContext()) + File.separator
+ uri.getLastPathSegment();
// Create & return a ParcelFileDescriptor pointing to the file
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/debug/Assert.java b/apptentive/src/main/java/com/apptentive/android/sdk/debug/Assert.java
index 578c7ce8b..14864d1fb 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/debug/Assert.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/debug/Assert.java
@@ -21,7 +21,7 @@ public class Assert {
private static AssertImp imp = new AssertImp() {
@Override
public void assertFailed(String message) {
- ApptentiveLog.e("Assertion failed: " + message + "\n" + getStackTrace(6));
+ ApptentiveLog.a("Assertion failed: " + message + "\n" + getStackTrace(6));
}
};
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/debug/AsyncLogWriter.java b/apptentive/src/main/java/com/apptentive/android/sdk/debug/AsyncLogWriter.java
new file mode 100644
index 000000000..bddead32b
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/debug/AsyncLogWriter.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.debug;
+
+import android.support.annotation.NonNull;
+
+import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.util.Util;
+import com.apptentive.android.sdk.util.threading.DispatchQueue;
+import com.apptentive.android.sdk.util.threading.DispatchQueueType;
+import com.apptentive.android.sdk.util.threading.DispatchTask;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+import static com.apptentive.android.sdk.ApptentiveLogTag.UTIL;
+import static com.apptentive.android.sdk.util.Constants.LOG_FILE_EXT;
+import static com.apptentive.android.sdk.util.Constants.LOG_FILE_PREFIX;
+
+public class AsyncLogWriter implements ApptentiveLog.LogListener {
+ /**
+ * Initial buffer size for queued messages
+ */
+ private static final int MESSAGE_QUEUE_SIZE = 256;
+
+ /** Mutex object for operation synchronization */
+ private final Object mutex = new Object();
+
+ /**
+ * Dest directory for storing log files
+ */
+ private final File destDir;
+
+ /**
+ * How many log files should we keep
+ */
+ private final int logHistorySize;
+
+ /**
+ * Stores pending log message before they written to a log file
+ * NOTE: this field should only be accessed withing a synchronized context
+ */
+ private final List pendingMessages;
+
+ /**
+ * Dispatch queue for writing logs in the background
+ */
+ private final DispatchQueue writeQueue;
+
+ /**
+ * Dispatch task for writing messages to a log file
+ */
+ private final DispatchTask writeQueueTask;
+
+ public AsyncLogWriter(File destDir, int logHistorySize) {
+ this(destDir, DispatchQueue.createBackgroundQueue("Apptentive Log Queue", DispatchQueueType.Serial), logHistorySize);
+ }
+
+ AsyncLogWriter(File destDir, DispatchQueue writeQueue, int logHistorySize) {
+ if (destDir == null) {
+ throw new IllegalArgumentException("Dest dir is null");
+ }
+
+ if (writeQueue == null) {
+ throw new IllegalArgumentException("Write queue is null");
+ }
+
+ if (logHistorySize < 1) {
+ throw new IllegalArgumentException("Illegal log history size: " + logHistorySize);
+ }
+
+ this.destDir = destDir;
+ this.logHistorySize = logHistorySize;
+ this.writeQueue = writeQueue;
+
+ pendingMessages = new ArrayList<>(MESSAGE_QUEUE_SIZE);
+
+ File logFile = new File(destDir, createLogFilename());
+ ApptentiveLog.v(UTIL, "Log file: " + logFile);
+ writeQueueTask = new LogFileWriteTask(logFile);
+
+ // run initialization as the first task on the write queue
+ writeQueue.dispatchAsync(createInitializationTask());
+ }
+
+ // for unit-testing
+ @NonNull
+ protected String createLogFilename() {
+ return Util.currentDateAsFilename(LOG_FILE_PREFIX, LOG_FILE_EXT);
+ }
+
+ private DispatchTask createInitializationTask() {
+ return new DispatchTask() {
+ @Override
+ protected void execute() {
+ // list existing log files
+ File[] files = destDir.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.endsWith(LOG_FILE_EXT);
+ }
+ });
+
+ // anything to clear?
+ if (files == null || files.length == 0) {
+ return;
+ }
+
+ // sort existing log files by modification date (newest come first)
+ Arrays.sort(files, new Comparator() {
+ @Override
+ public int compare(File o1, File o2) {
+ // first we try to compare modification dates
+ int cmp = (int) (o2.lastModified() - o1.lastModified());
+ if (cmp != 0) {
+ return cmp;
+ }
+
+ // if for any reason they are the same - compare filenames
+ return o2.getName().compareTo(o1.getName());
+ }
+ });
+
+ // delete oldest files if the total count exceed the log history size
+ for (int i = logHistorySize - 1; i < files.length; ++i) {
+ files[i].delete();
+ }
+ }
+ };
+ }
+
+ @Override
+ public void onLogMessage(@NonNull ApptentiveLog.Level level, @NonNull String message) {
+ synchronized (mutex) {
+ pendingMessages.add(level.getShortName() + ": " + message);
+ writeQueue.dispatchAsyncOnce(writeQueueTask);
+ }
+ }
+
+ private class LogFileWriteTask extends DispatchTask {
+ private final File file;
+ private final List queuedMessagesTemp;
+
+ private LogFileWriteTask(File file) {
+ if (file == null) {
+ throw new IllegalArgumentException("File is null");
+ }
+ this.file = file;
+ queuedMessagesTemp = new ArrayList<>(MESSAGE_QUEUE_SIZE);
+ }
+
+ @Override
+ protected void execute() {
+ // we don't want to acquire the mutex for too long so just copy pending messages
+ // to the temp list which would be used in a blocking IO
+ synchronized (mutex) {
+ queuedMessagesTemp.addAll(pendingMessages);
+ pendingMessages.clear();
+ }
+
+ try {
+ Util.writeText(file, queuedMessagesTemp, true);
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while writing log file: " + destDir);
+ // TODO: disable writing all together?
+ }
+ queuedMessagesTemp.clear();
+ }
+ }
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogBroadcastReceiver.java b/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogBroadcastReceiver.java
index f0b76b6ad..8a23709cc 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogBroadcastReceiver.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogBroadcastReceiver.java
@@ -17,14 +17,7 @@
import java.io.File;
import java.util.ArrayList;
-import static com.apptentive.android.sdk.debug.TroubleshootingNotification.ACTION_ABORT;
-import static com.apptentive.android.sdk.debug.TroubleshootingNotification.ACTION_SEND_LOGS;
-import static com.apptentive.android.sdk.debug.TroubleshootingNotification.EXTRA_EMAIL_RECIPIENTS;
-import static com.apptentive.android.sdk.debug.TroubleshootingNotification.EXTRA_INFO;
-import static com.apptentive.android.sdk.debug.TroubleshootingNotification.EXTRA_LOG_FILE;
-import static com.apptentive.android.sdk.debug.TroubleshootingNotification.EXTRA_MANIFEST_FILE;
-import static com.apptentive.android.sdk.debug.TroubleshootingNotification.EXTRA_SUBJECT;
-import static com.apptentive.android.sdk.debug.TroubleshootingNotification.NOTIFICATION_ID_KEY;
+import static com.apptentive.android.sdk.debug.TroubleshootingNotificationBuilder.*;
public class LogBroadcastReceiver extends BroadcastReceiver {
@@ -42,16 +35,8 @@ public void onReceive(Context context, Intent intent) {
Intent it = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
context.sendBroadcast(it);
- // TODO: The app can be killed and this should still work. But it will be null if the app is killed when a user interacts with the notification.
- LogMonitor instance = LogMonitor.sharedInstance();
- if (instance == null) {
- ApptentiveLog.e("LogMonitor was null");
- return;
- }
-
- // stop collection logs and close the file
- instance.stopWritingLogs();
-
+ // Stop log monitor
+ LogMonitor.stopSession(context);
// handle action
String action = intent.getAction();
@@ -63,15 +48,13 @@ public void onReceive(Context context, Intent intent) {
email.putExtra(Intent.EXTRA_SUBJECT, intent.getStringExtra(EXTRA_SUBJECT));
email.putExtra(Intent.EXTRA_TEXT, intent.getStringExtra(EXTRA_INFO));
- File logFile = (File) intent.getExtras().get(EXTRA_LOG_FILE);
- File manifestFile = (File) intent.getExtras().get(EXTRA_MANIFEST_FILE);
+ File[] files = (File[]) intent.getExtras().get(EXTRA_ATTACHMENTS);
ArrayList attachments = new ArrayList<>();
- if (logFile != null && logFile.exists()) {
- attachments.add(Uri.parse("content://" + ApptentiveAttachmentFileProvider.getAuthority(context) + "/" + logFile.getName()));
- }
- if (manifestFile != null && manifestFile.exists()) {
- attachments.add(Uri.parse("content://" + ApptentiveAttachmentFileProvider.getAuthority(context) + "/" + manifestFile.getName()));
+ for (File file : files) {
+ if (file.exists()) {
+ attachments.add(Uri.parse("content://" + ApptentiveAttachmentFileProvider.getAuthority(context) + "/" + file.getName()));
+ }
}
email.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
@@ -84,7 +67,5 @@ public void onReceive(Context context, Intent intent) {
} else {
ApptentiveLog.e("Unexpected action: %s", action);
}
-
- instance.destroy();
}
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogMonitor.java b/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogMonitor.java
index c2a6d554a..7c0d45d23 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogMonitor.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogMonitor.java
@@ -6,218 +6,160 @@
package com.apptentive.android.sdk.debug;
-import android.app.Notification;
-import android.app.NotificationManager;
import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.os.Build;
-import android.util.Log;
+import android.support.annotation.Nullable;
+import android.widget.Toast;
import com.apptentive.android.sdk.ApptentiveLog;
-import com.apptentive.android.sdk.comm.ApptentiveHttpClient;
-import com.apptentive.android.sdk.notifications.ApptentiveNotificationCenter;
+import com.apptentive.android.sdk.network.HttpJsonRequest;
+import com.apptentive.android.sdk.network.HttpRequest;
+import com.apptentive.android.sdk.network.HttpRequestManager;
+import com.apptentive.android.sdk.network.HttpRequestMethod;
+import com.apptentive.android.sdk.network.HttpRequestRetryPolicy;
+import com.apptentive.android.sdk.network.HttpRequestRetryPolicyDefault;
import com.apptentive.android.sdk.util.Constants;
-import com.apptentive.android.sdk.util.Destroyable;
-import com.apptentive.android.sdk.util.Jwt;
import com.apptentive.android.sdk.util.StringUtils;
import com.apptentive.android.sdk.util.Util;
+import com.apptentive.android.sdk.util.threading.DispatchTask;
-import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.lang.ref.WeakReference;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-
-import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_LOG_MONITOR_STARTED;
-import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_LOG_MONITOR_STOPPED;
+import static com.apptentive.android.sdk.ApptentiveHelper.*;
+import static com.apptentive.android.sdk.ApptentiveLogTag.*;
import static com.apptentive.android.sdk.comm.ApptentiveHttpClient.USER_AGENT_STRING;
-public class LogMonitor implements Destroyable {
-
- // TODO: Replace with a better unique number
- public static final int NOTIFICATION_ID = 1;
-
- private static final String TAG = "LogMonitor";
-
- private static final String PREFS_NAME = "com.apptentive.debug";
- private static final String PREFS_KEY_EMAIL_RECIPIENTS = "com.apptentive.debug.EmailRecipients";
- private static final String PREFS_KEY_LOG_LEVEL = "com.apptentive.debug.LogLevel";
- private static final String PREFS_KEY_FILTER_PID = "com.apptentive.debug.FilterPID";
-
+public final class LogMonitor {
+ /**
+ * Text prefix for a valid access token
+ */
private static final String DEBUG_TEXT_HEADER = "com.apptentive.debug:";
- private static LogMonitor instance;
-
- private final WeakReference contextRef;
- private final ApptentiveLog.Level logLevel;
- private final String[] emailRecipients;
-
- private final LogWriter logWriter;
-
- private ApptentiveLog.Level oldLogLevel;
+ /**
+ * Access token verification request tag
+ */
+ private static final String TAG_VERIFICATION_REQUEST = "VERIFICATION_REQUEST";
- private LogMonitor(Context context, Configuration configuration) {
- if (context == null) {
- throw new IllegalArgumentException("Context is null");
- }
- if (configuration == null) {
- throw new IllegalArgumentException("Configuration is null");
- }
+ /**
+ * Holds current session instance (if any).
+ * NOTE: This field should only be accessed on the conversation queue.
+ */
+ private static @Nullable LogMonitorSession currentSession;
- this.contextRef = new WeakReference<>(context);
- this.logLevel = configuration.logLevel;
- this.emailRecipients = configuration.emailRecipients;
- this.logWriter = new LogWriter(getLogFile(context), configuration.restored, configuration.filterByPID);
+ // no instancing or subclassing
+ private LogMonitor() {
}
- //region Initialization
+ //region Session
/**
- * Attempts to initialize an instance. Returns true
if succeed.
+ * Attempts to start a new troubleshooting session. First the SDK will check if there is
+ * an existing session stored in the persistent storage and then check if the clipboard
+ * contains a valid access token.
+ * This call is async and returns immediately.
*/
- public static boolean tryInitialize(Context context, String apptentiveApiKey, String apptentiveApiSignature) {
- if (instance != null) {
- ApptentiveLog.i("Log Monitor already initialized");
- return false;
- }
-
- try {
- Configuration configuration = readConfigurationFromPersistentStorage(context);
- if (configuration != null) {
- ApptentiveLog.i("Read log monitor configuration from persistent storage: " + configuration);
- } else {
- String accessToken = readAccessTokenFromClipboard(context);
-
- // No access token was supplied
- if (accessToken == null) {
- return false;
- }
-
- // The access token was invalid, or expired, or the server could be reached to verify it
- if (!syncVerifyAccessToken(apptentiveApiKey, apptentiveApiSignature, accessToken)) {
- ApptentiveLog.i("Can't start log monitor: access token verification failed");
- return false;
- }
-
- configuration = readConfigurationFromToken(accessToken);
- if (configuration != null) {
- ApptentiveLog.i("Read log monitor configuration from clipboard: " + configuration);
- Util.setClipboardText(context, ""); // clear the clipboard contents after the data is parsed
-
- // store the configuration to make sure we can resume the current session
- saveConfigurationFromPersistentStorage(context, configuration);
+ public static void startSession(final Context context, final String appKey, final String appSignature) {
+ dispatchOnConversationQueue(new DispatchTask() {
+ @Override
+ protected void execute() {
+ try {
+ startSessionGuarded(context, appKey, appSignature);
+ } catch (Exception e) {
+ ApptentiveLog.e(TROUBLESHOOT, e, "Unable to start Apptentive Log Monitor");
}
}
-
- if (configuration != null) {
- ApptentiveLog.i("Entering Apptentive Troubleshooting mode.");
- instance = new LogMonitor(context, configuration);
- instance.start(context);
- return true;
- }
-
- } catch (Exception e) {
- ApptentiveLog.i("Exception while initializing Apptentive Log Monitor", e);
- }
-
- return false;
+ });
}
- private static Configuration readConfigurationFromToken(String accessToken) {
- try {
- final Jwt jwt = Jwt.decode(accessToken);
- JSONObject payload = jwt.getPayload();
+ private static void startSessionGuarded(final Context context, String appKey, String appSignature) {
+ checkConversationQueue();
- Configuration config = new Configuration();
-
- // log level
- String logLevelStr = payload.optString("level");
- if (!StringUtils.isNullOrEmpty(logLevelStr)) {
- config.logLevel = ApptentiveLog.Level.parse(logLevelStr);
- }
-
- // recipients
- JSONArray recipientsJson = payload.optJSONArray("recipients");
- if (recipientsJson != null) {
- String[] recipients = new String[recipientsJson.length()];
- for (int i = 0; i < recipientsJson.length(); ++i) {
- recipients[i] = recipientsJson.optString(i);
- }
+ // check if another session is currently active
+ if (currentSession != null) {
+ return;
+ }
- config.emailRecipients = recipients;
- }
+ // attempt to load an existing session
+ final LogMonitorSession existingSession = LogMonitorSessionIO.readCurrentSession(context);
+ if (existingSession != null) {
+ ApptentiveLog.i(TROUBLESHOOT, "Previous Apptentive Log Monitor session loaded from persistent storage: %s", existingSession);
+ startSession(context, existingSession);
+ return;
+ }
- // should we filter by PID
- config.filterByPID = payload.optBoolean("filter_app_process", config.filterByPID);
+ // attempt to create a new session based on the clipboard content
+ final String accessToken = readAccessTokenFromClipboard(context);
- return config;
- } catch (Exception e) {
- ApptentiveLog.e(e, "Exception while parsing access token: '%s'", accessToken);
- return null;
+ // no access token was found
+ if (accessToken == null) {
+ ApptentiveLog.v(TROUBLESHOOT, "No access token found in clipboard");
+ return;
}
- }
- /**
- * Attempts to read a log monitor configuration stored in the last session. Returns null
if failed
- */
- private static Configuration readConfigurationFromPersistentStorage(Context context) {
- if (context == null) {
- throw new IllegalArgumentException("Context is null");
- }
- SharedPreferences prefs = getPrefs(context);
- if (!prefs.contains(PREFS_KEY_EMAIL_RECIPIENTS)) {
- return null;
+ // clear the clipboard
+ Util.setClipboardText(context, ""); // clear the clipboard contents after the data is parsed
+
+ // check if access token
+ HttpRequest existingRequest = HttpRequestManager.sharedManager().findRequest(TAG_VERIFICATION_REQUEST);
+ if (existingRequest != null) {
+ ApptentiveLog.v(TROUBLESHOOT, "Another access token verification request is running");
+ return;
}
- Configuration configuration = new Configuration();
- configuration.restored = true;
+ // create and send a token verification request
+ HttpRequest verificationRequest = createTokenVerificationRequest(appKey, appSignature, accessToken, new HttpRequest.Adapter() {
+ @Override
+ public void onFinish(HttpJsonRequest request) {
+ checkConversationQueue();
+
+ JSONObject response = request.getResponseObject();
+ boolean tokenValid = response.optBoolean("valid", false);
+ if (!tokenValid) {
+ ApptentiveLog.w(TROUBLESHOOT, "Unable to start Apptentive Log Monitor: the access token was rejected on the server (%s)", accessToken);
+ Util.showToast(context, "Token rejected", Toast.LENGTH_LONG);
+ return;
+ }
- String emailRecipients = prefs.getString(PREFS_KEY_EMAIL_RECIPIENTS, null);
- if (!StringUtils.isNullOrEmpty(emailRecipients)) {
- configuration.emailRecipients = emailRecipients.split(",");
- }
+ LogMonitorSession session = LogMonitorSessionIO.readSessionFromJWT(accessToken);
+ if (session == null) {
+ ApptentiveLog.w(TROUBLESHOOT, "Unable to start Apptentive Log Monitor: failed to parse the access token (%s)", accessToken);
+ Util.showToast(context, "Token invalid", Toast.LENGTH_LONG);
+ return;
+ }
- String logLevel = prefs.getString(PREFS_KEY_LOG_LEVEL, null);
- if (!StringUtils.isNullOrEmpty(logLevel)) {
- configuration.logLevel = ApptentiveLog.Level.parse(logLevel);
- }
+ // store the current session to make sure we can resume log monitoring on the next application start
+ LogMonitorSessionIO.saveCurrentSession(context, session);
- configuration.filterByPID = prefs.getBoolean(PREFS_KEY_FILTER_PID, configuration.filterByPID);
+ // start the session
+ startSession(context, session);
+ }
- return configuration;
+ @Override
+ public void onFail(HttpJsonRequest request, String reason) {
+ ApptentiveLog.e(TROUBLESHOOT, "Unable to start Apptentive Log Monitor: failed to verify the access token (%s)\n%s", accessToken, reason);
+ Util.showToast(context, "Can't verify token", Toast.LENGTH_LONG);
+ }
+ });
+ verificationRequest.setCallbackQueue(conversationQueue());
+ verificationRequest.start();
}
- /** Saves the configuration into SharedPreferences */
- private static void saveConfigurationFromPersistentStorage(Context context, Configuration configuration) {
- SharedPreferences prefs = getPrefs(context);
- SharedPreferences.Editor editor = prefs.edit();
- editor.putString(PREFS_KEY_EMAIL_RECIPIENTS, StringUtils.join(configuration.emailRecipients));
- editor.putString(PREFS_KEY_LOG_LEVEL, configuration.logLevel.toString());
- editor.putBoolean(PREFS_KEY_FILTER_PID, configuration.filterByPID);
- editor.apply();
+ private static void startSession(Context context, LogMonitorSession session) {
+ currentSession = session;
+ session.start(context);
}
- private static void deleteConfigurationFromPersistentStorage(Context context) {
- SharedPreferences.Editor editor = getPrefs(context).edit();
- editor.remove(PREFS_KEY_EMAIL_RECIPIENTS);
- editor.remove(PREFS_KEY_LOG_LEVEL);
- editor.remove(PREFS_KEY_FILTER_PID);
- editor.apply();
+ static void stopSession(final Context context) {
+ dispatchOnConversationQueue(new DispatchTask() {
+ @Override
+ protected void execute() {
+ if (currentSession != null) {
+ currentSession.stop();
+ currentSession = null;
+ }
+ LogMonitorSessionIO.deleteCurrentSession(context);
+ }
+ });
}
//endregion
@@ -227,7 +169,7 @@ private static void deleteConfigurationFromPersistentStorage(Context context) {
/**
* Attempts to read access token from the clipboard
*/
- private static String readAccessTokenFromClipboard(Context context) {
+ private static @Nullable String readAccessTokenFromClipboard(Context context) {
String text = Util.getClipboardText(context);
if (StringUtils.isNullOrEmpty(text)) {
@@ -242,292 +184,50 @@ private static String readAccessTokenFromClipboard(Context context) {
}
// Remove the header
- text = text.substring(DEBUG_TEXT_HEADER.length());
- return text;
- }
-
- /**
- * Send a sync URL request to the backend to verify the access token. This method would block
- */
- private static boolean syncVerifyAccessToken(String apptentiveApiKey, String apptentiveApiSignature, String accessToken) {
- return verifyToken(apptentiveApiKey, apptentiveApiSignature, accessToken);
- }
-
- //endregion
-
- //region Lifecycle
-
- private void start(Context context) {
- Log.i(TAG, "Overriding log level: " + logLevel);
- oldLogLevel = ApptentiveLog.getLogLevel();
- ApptentiveLog.overrideLogLevel(logLevel);
-
- showDebugNotification(context);
-
- logWriter.start();
-
- // post a notification
- ApptentiveNotificationCenter.defaultCenter()
- .postNotification(NOTIFICATION_LOG_MONITOR_STARTED);
- }
-
- public void stopWritingLogs() {
- try {
- logWriter.stopAndWait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
-
- // post a notification
- ApptentiveNotificationCenter.defaultCenter()
- .postNotification(NOTIFICATION_LOG_MONITOR_STOPPED);
- }
-
- private void showDebugNotification(Context context) {
- NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- Notification notification = new TroubleshootingNotification().buildNotification(context, getSubject(context), getSystemInfo(context), getLogFile(context), getManifestFile(context), emailRecipients);
- notificationManager.notify(NOTIFICATION_ID, notification);
- }
-
- private String getSubject(Context context) {
- String subject = String.format("%s (Android)", context.getPackageName());
- try {
- ApplicationInfo ai = context.getApplicationInfo();
- subject = String.format("%s (Android)", ai.loadLabel(context.getPackageManager()).toString());
- } catch (Exception e) {
- ApptentiveLog.e(e, "Unable to load troubleshooting email status line");
- }
- return subject;
- }
-
- private String getSystemInfo(Context context) {
- String versionName = "";
- int versionCode = -1;
- try {
- PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
- // TODO: list activities, permissions, etc
- versionName = packageInfo.versionName;
- versionCode = packageInfo.versionCode;
- } catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
- }
-
- Object[] info = {
- "App Package Name", context.getPackageName(),
- "App Version Name", versionName,
- "App Version Code", versionCode,
- "Apptentive SDK", com.apptentive.android.sdk.util.Constants.APPTENTIVE_SDK_VERSION,
- "Device Model", Build.MODEL,
- "Android OS Version", Build.VERSION.RELEASE,
- "Android OS API Level", Build.VERSION.SDK_INT,
- "Locale", Locale.getDefault().getDisplayName()
- };
-
- StringBuilder result = new StringBuilder();
- result.append("This email may contain sensitive content. Please review before sending.\n\n");
- for (int i = 0; i < info.length; i += 2) {
- if (result.length() > 0) {
- result.append("\n");
- }
- result.append(info[i]);
- result.append(": ");
- result.append(info[i + 1]);
- }
- return result.toString();
- }
-
- //endregion
-
- //region Destroyable
-
- @Override
- public void destroy() {
- // restoring old log level
- if (oldLogLevel != null) {
- ApptentiveLog.overrideLogLevel(oldLogLevel);
- }
-
- Context context = getContext();
- if (context != null) {
- // deleting saved session
- deleteConfigurationFromPersistentStorage(context);
- instance = null;
- } else {
- Log.e(TAG, "Unable to destroy session: context is lost");
- }
- }
-
- //endregion
-
- //region Helpers
-
- private static SharedPreferences getPrefs(Context context) {
- return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
- }
-
- private static File getLogFile(Context context) {
- return new File(context.getCacheDir(), Constants.FILE_APPTENTIVE_LOG_FILE);
- }
-
- private static File getManifestFile(Context context) {
- return new File(context.getCacheDir(), Constants.FILE_APPTENTIVE_ENGAGEMENT_MANIFEST);
- }
-
- //endregion
-
- //region Getters/Setters
-
- public static LogMonitor sharedInstance() {
- return instance;
- }
-
- private Context getContext() {
- return contextRef.get();
- }
-
- //endregion
-
- //region Configuration
-
- private static class Configuration {
- /** Email recipients for the log email */
- String[] emailRecipients = { "support@apptentive.com" };
-
- /** New log level */
- ApptentiveLog.Level logLevel = ApptentiveLog.Level.VERY_VERBOSE;
-
- /** True if logcat output should be filtered by the process id */
- boolean filterByPID = true;
-
- /** True if configuration was restored from the persistent storage */
- boolean restored;
-
- @Override
- public String toString() {
- return String.format("logLevel=%s recipients=%s filterPID=%s restored=%s",
- logLevel, Arrays.toString(emailRecipients), Boolean.toString(filterByPID),
- Boolean.toString(restored));
- }
+ return text.substring(DEBUG_TEXT_HEADER.length());
}
//endregion
//region Token Verification
- private static boolean verifyToken(String apptentiveAppKey, String apptentiveAppSignature, String token) {
-
- final Map headers = new HashMap<>();
- headers.put("X-API-Version", String.valueOf(Constants.API_VERSION));
- headers.put("APPTENTIVE-KEY", apptentiveAppKey);
- headers.put("APPTENTIVE-SIGNATURE", apptentiveAppSignature);
- headers.put("Content-Type", "application/json");
- headers.put("Accept", "application/json");
- headers.put("User-Agent", String.format(USER_AGENT_STRING, Constants.APPTENTIVE_SDK_VERSION));
-
- JSONObject postBodyJson;
+ private static HttpRequest createTokenVerificationRequest(String apptentiveAppKey, String apptentiveAppSignature, String token, HttpRequest.Listener listener) {
+ // TODO: move this logic to ApptentiveHttpClient
+ String URL = Constants.CONFIG_DEFAULT_SERVER_URL + "/debug_token/verify";
+ HttpRequest request = new HttpJsonRequest(URL, createVerityRequestObject(token));
+ request.setTag(TAG_VERIFICATION_REQUEST);
+ request.setMethod(HttpRequestMethod.POST);
+ request.setRequestManager(HttpRequestManager.sharedManager());
+ request.setRequestProperty("X-API-Version", Constants.API_VERSION);
+ request.setRequestProperty("APPTENTIVE-KEY", apptentiveAppKey);
+ request.setRequestProperty("APPTENTIVE-SIGNATURE", apptentiveAppSignature);
+ request.setRequestProperty("Content-Type", "application/json");
+ request.setRequestProperty("Accept", "application/json");
+ request.setRequestProperty("User-Agent", String.format(USER_AGENT_STRING, Constants.getApptentiveSdkVersion()));
+ request.setRetryPolicy(createVerityRequestRetryPolicy());
+ request.addListener(listener);
+ return request;
+ }
+
+ private static JSONObject createVerityRequestObject(String token) {
try {
- postBodyJson = new JSONObject();
+ JSONObject postBodyJson = new JSONObject();
postBodyJson.put("debug_token", token);
+ return postBodyJson;
} catch (JSONException e) {
- // Can't happen
- throw new RuntimeException(e);
- }
-
- String response = loadFromURL(Constants.CONFIG_DEFAULT_SERVER_URL + "/debug_token/verify", headers, postBodyJson.toString());
- if (!StringUtils.isNullOrEmpty(response)) {
- try {
- JSONObject debugTokenResponse = new JSONObject(response);
- if (!debugTokenResponse.isNull("valid")) {
- return debugTokenResponse.optBoolean("valid", false);
- }
- ApptentiveLog.e("Debug token response was missing \"valid\" field.");
- } catch (Exception e) {
- ApptentiveLog.e("Error parsing debug token validation response.");
- return false;
- }
- return true;
+ // should not happen but it's better to throw an exception
+ throw new IllegalArgumentException("Token is invalid:" + token, e);
}
- return false;
}
- private static String loadFromURL(final String urlString, final Map headers, final String body) {
- final StringBuilder responseString = new StringBuilder();
- Thread networkThread = new Thread(new Runnable() {
+ private static HttpRequestRetryPolicy createVerityRequestRetryPolicy() {
+ return new HttpRequestRetryPolicyDefault() {
@Override
- public void run() {
- try {
- loadFromURL(urlString, headers, responseString);
- } catch (Exception e) {
- ApptentiveLog.e("Error performing debug token validation request: %s", e.getMessage());
- }
- }
-
- private void loadFromURL(String urlString, Map headers, StringBuilder responseString) throws IOException {
- ApptentiveLog.i("Performing debug token verification request: \"%s\"", body);
- URL url = new URL(urlString);
- BufferedReader reader = null;
- try {
- HttpURLConnection connection = (HttpURLConnection) url.openConnection();
- connection.setRequestMethod("POST");
- connection.setConnectTimeout(ApptentiveHttpClient.DEFAULT_HTTP_CONNECT_TIMEOUT);
- connection.setReadTimeout(ApptentiveHttpClient.DEFAULT_HTTP_SOCKET_TIMEOUT);
-
- for (String key : headers.keySet()) {
- connection.setRequestProperty(key, headers.get(key));
- }
-
- OutputStream outputStream = null;
- try {
- outputStream = connection.getOutputStream();
- outputStream.write(body.getBytes());
- } finally {
- Util.ensureClosed(outputStream);
- }
-
- int responseCode = connection.getResponseCode();
- ApptentiveLog.vv("Response code: %d", responseCode);
-
- InputStream is;
- StringBuilder buffer;
- boolean successful;
- // If successful, read the message into the response String. If not, read it into a buffer and throw it in an exception.
- if (responseCode >= HttpURLConnection.HTTP_OK && responseCode < HttpURLConnection.HTTP_MULT_CHOICE) {
- successful = true;
- is = connection.getInputStream();
- buffer = responseString;
- } else {
- successful = false;
- is = connection.getErrorStream();
- buffer = new StringBuilder();
- }
-
- reader = new BufferedReader(new InputStreamReader(is));
- String line;
- while ((line = reader.readLine()) != null) {
- if (buffer.length() > 0) {
- buffer.append('\n');
- }
- buffer.append(line);
- }
- ApptentiveLog.v("Debug Token verification response: %s", buffer);
- if (!successful) {
- throw new IOException(buffer.toString());
- }
- } finally {
- Util.ensureClosed(reader);
- }
+ public boolean shouldRetryRequest(int responseCode, int retryAttempt) {
+ return false; // fail fast: do not retry
}
- });
- networkThread.start();
- try {
- networkThread.join();
- } catch (InterruptedException e) {
- ApptentiveLog.e("Debug token validation thread interrupted.");
- return null;
- }
- return responseString.toString();
+ };
}
//endregion
-
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogMonitorSession.java b/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogMonitorSession.java
new file mode 100644
index 000000000..07215daf6
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogMonitorSession.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.debug;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.util.Constants;
+import com.apptentive.android.sdk.util.StringUtils;
+import com.apptentive.android.sdk.util.threading.DispatchQueue;
+import com.apptentive.android.sdk.util.threading.DispatchTask;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+import static com.apptentive.android.sdk.ApptentiveHelper.checkConversationQueue;
+import static com.apptentive.android.sdk.ApptentiveLog.Level.VERBOSE;
+import static com.apptentive.android.sdk.ApptentiveLogTag.TROUBLESHOOT;
+import static com.apptentive.android.sdk.debug.Assert.assertNotNull;
+import static com.apptentive.android.sdk.util.Constants.LOG_FILE_EXT;
+import static com.apptentive.android.sdk.util.Constants.LOG_FILE_PREFIX;
+
+class LogMonitorSession {
+ // TODO: Replace with a better unique number
+ private static final int NOTIFICATION_ID = 1;
+
+ /**
+ * Email recipients for the log email
+ */
+ String[] emailRecipients = {"support@apptentive.com"};
+
+ /**
+ * True if configuration was restored from the persistent storage
+ */
+ boolean restored;
+
+ private ApptentiveLog.Level oldLogLevel;
+
+ //region Lifecycle
+
+ void start(final Context context) {
+ checkConversationQueue();
+
+ ApptentiveLog.i(TROUBLESHOOT, "Overriding log level: " + VERBOSE);
+ oldLogLevel = ApptentiveLog.getLogLevel();
+ ApptentiveLog.overrideLogLevel(VERBOSE);
+
+ // show debug notification
+ showDebugNotification(context);
+ }
+
+ public void stop() {
+ assertNotNull(oldLogLevel);
+ if (oldLogLevel != null) {
+ ApptentiveLog.overrideLogLevel(oldLogLevel);
+ }
+ }
+
+ private void showDebugNotification(final Context context) {
+ final String subject = getSubject(context);
+ final File[] attachments = listAttachments(context);
+
+ DispatchQueue.mainQueue().dispatchAsync(new DispatchTask() {
+ @Override
+ protected void execute() {
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ Notification notification = TroubleshootingNotificationBuilder.buildNotification(context, subject, getSystemInfo(context), attachments, emailRecipients);
+ Assert.assertNotNull(notification, "Failed to create troubleshooting notification");
+ if (notificationManager != null) {
+ notificationManager.notify(NOTIFICATION_ID, notification);
+ }
+ }
+ });
+ }
+
+ private String getSubject(Context context) {
+ String subject = String.format("%s (Android)", context.getPackageName());
+ try {
+ ApplicationInfo ai = context.getApplicationInfo();
+ subject = String.format("%s (Android)", ai.loadLabel(context.getPackageManager()).toString());
+ } catch (Exception e) {
+ ApptentiveLog.e(TROUBLESHOOT, e, "Unable to load troubleshooting email status line");
+ }
+ return subject;
+ }
+
+ private String getSystemInfo(Context context) {
+ String versionName = "";
+ int versionCode = -1;
+ try {
+ PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ // TODO: list activities, permissions, etc
+ versionName = packageInfo.versionName;
+ versionCode = packageInfo.versionCode;
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+
+ Object[] info = {
+ "App Package Name", context.getPackageName(),
+ "App Version Name", versionName,
+ "App Version Code", versionCode,
+ "Apptentive SDK", com.apptentive.android.sdk.util.Constants.getApptentiveSdkVersion(),
+ "Device Model", Build.MODEL,
+ "Android OS Version", Build.VERSION.RELEASE,
+ "Android OS API Level", Build.VERSION.SDK_INT,
+ "Locale", Locale.getDefault().getDisplayName()
+ };
+
+ StringBuilder result = new StringBuilder();
+ result.append("This email may contain sensitive content. Please review before sending.\n\n");
+ for (int i = 0; i < info.length; i += 2) {
+ if (result.length() > 0) {
+ result.append("\n");
+ }
+ result.append(info[i]);
+ result.append(": ");
+ result.append(info[i + 1]);
+ }
+ return result.toString();
+ }
+
+ private static File[] listAttachments(Context context) {
+ List attachments = new ArrayList<>();
+
+ // manifest
+ File manifestFile = new File(ApptentiveLog.getLogsDirectory(context), Constants.FILE_APPTENTIVE_ENGAGEMENT_MANIFEST);
+ attachments.add(manifestFile);
+
+ // logs
+ File logsDirectory = ApptentiveLog.getLogsDirectory(context);
+ File[] logFiles = logsDirectory.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.endsWith(LOG_FILE_EXT) && name.startsWith(LOG_FILE_PREFIX);
+ }
+ });
+ if (logFiles != null && logFiles.length > 0) {
+ attachments.addAll(Arrays.asList(logFiles));
+ }
+
+ return attachments.toArray(new File[attachments.size()]);
+ }
+
+ //endregion
+
+ @Override
+ public String toString() {
+ return StringUtils.format("recipients=%s restored=%s",
+ Arrays.toString(emailRecipients),
+ Boolean.toString(restored));
+ }
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogMonitorSessionIO.java b/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogMonitorSessionIO.java
new file mode 100644
index 000000000..7fca2b738
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogMonitorSessionIO.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.debug;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.support.annotation.Nullable;
+
+import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.util.Jwt;
+import com.apptentive.android.sdk.util.StringUtils;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+class LogMonitorSessionIO {
+ private static final String PREFS_NAME = "com.apptentive.debug";
+ private static final String PREFS_KEY_EMAIL_RECIPIENTS = "com.apptentive.debug.EmailRecipients";
+ private static final String PREFS_KEY_FILTER_PID = "com.apptentive.debug.FilterPID";
+
+ /**
+ * Attempts to read an existing session from the persistent storage.
+ * Returns null
i
+ */
+ static @Nullable LogMonitorSession readCurrentSession(Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("Context is null");
+ }
+
+ SharedPreferences prefs = getPrefs(context);
+ if (!prefs.contains(PREFS_KEY_EMAIL_RECIPIENTS)) {
+ return null;
+ }
+
+ LogMonitorSession session = new LogMonitorSession();
+ session.restored = true;
+
+ String emailRecipients = prefs.getString(PREFS_KEY_EMAIL_RECIPIENTS, null);
+ if (!StringUtils.isNullOrEmpty(emailRecipients)) {
+ session.emailRecipients = emailRecipients.split(",");
+ }
+
+ return session;
+ }
+
+ /**
+ * Saves current session to the persistent storage
+ */
+ static void saveCurrentSession(Context context, LogMonitorSession session) {
+ if (context == null) {
+ throw new IllegalArgumentException("Context is null");
+ }
+
+ if (session == null) {
+ throw new IllegalArgumentException("Session is null");
+ }
+
+ SharedPreferences prefs = getPrefs(context);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(PREFS_KEY_EMAIL_RECIPIENTS, StringUtils.join(session.emailRecipients));
+ editor.apply();
+ }
+
+ /**
+ * Deletes current session from the persistent storage
+ */
+ static void deleteCurrentSession(Context context) {
+ SharedPreferences.Editor editor = getPrefs(context).edit();
+ editor.remove(PREFS_KEY_EMAIL_RECIPIENTS);
+ editor.remove(PREFS_KEY_FILTER_PID);
+ editor.apply();
+ }
+
+ /**
+ * Reads session from JWT-token.
+ * Returns null
if fails.
+ */
+ static @Nullable LogMonitorSession readSessionFromJWT(String token) {
+ try {
+ final Jwt jwt = Jwt.decode(token);
+ JSONObject payload = jwt.getPayload();
+
+ LogMonitorSession config = new LogMonitorSession();
+
+ // recipients
+ JSONArray recipientsJson = payload.optJSONArray("recipients");
+ if (recipientsJson != null) {
+ String[] recipients = new String[recipientsJson.length()];
+ for (int i = 0; i < recipientsJson.length(); ++i) {
+ recipients[i] = recipientsJson.optString(i);
+ }
+
+ config.emailRecipients = recipients;
+ }
+
+ return config;
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while parsing access token: '%s'", token);
+ return null;
+ }
+ }
+
+ private static SharedPreferences getPrefs(Context context) {
+ return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
+ }
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogWriter.java b/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogWriter.java
deleted file mode 100644
index 30ba77746..000000000
--- a/apptentive/src/main/java/com/apptentive/android/sdk/debug/LogWriter.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Copyright (c) 2017, Apptentive, Inc. All Rights Reserved.
- * Please refer to the LICENSE file for the terms and conditions
- * under which redistribution and use of this file is permitted.
- */
-
-package com.apptentive.android.sdk.debug;
-
-import android.util.Log;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.InterruptedIOException;
-import java.util.ArrayList;
-import java.util.List;
-
-public class LogWriter {
- private static final String TAG = LogWriter.class.getSimpleName();
- private final File file;
- private final boolean append;
- private final boolean filterByPID;
-
- private Thread thread;
- private Process process;
-
- public LogWriter(File file, boolean append, boolean filterByPID) {
- if (file == null) {
- throw new IllegalArgumentException("File is null");
- }
- this.file = file;
- this.append = append;
- this.filterByPID = filterByPID;
- }
-
- //region Lifecycle
-
- public void start() {
- if (thread != null) {
- throw new IllegalStateException("Already started");
- }
-
- thread = new Thread(new Runnable() {
- @Override
- public void run() {
- writeLogs();
- }
- }, "Apptentive Logcat Writer");
- thread.start();
- }
-
- public void stopAndWait() throws InterruptedException {
- if (thread != null) {
- process.destroy();
- thread.interrupt();
- thread.join();
- thread = null;
- }
- }
-
- //endregion
-
- //region Logs
-
- private void writeLogs() {
- try {
- writeLogsGuarded();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- private void writeLogsGuarded() throws IOException {
- BufferedReader reader = null;
- FileWriter writer = null;
-
- try {
- List cmd = new ArrayList<>();
- cmd.add("logcat");
- cmd.add("-v");
- cmd.add("tag");
- if (filterByPID && logcatCanFilterByProcess()) {
- cmd.add("--pid=" + android.os.Process.myPid());
- }
-
- process = Runtime.getRuntime().exec(cmd.toArray(new String[cmd.size()]));
- reader = new BufferedReader(new InputStreamReader(process.getInputStream()), 8192);
- writer = new FileWriter(file, append);
-
- String line;
- while ((line = reader.readLine()) != null) {
- writer.write(line);
- writer.write('\n');
- }
- } catch (InterruptedIOException e) {
- Log.i(TAG, "Apptentive log writing interrupted");
- } finally {
- if (reader != null) {
- reader.close();
- }
-
- if (writer != null) {
- writer.close();
- }
-
- if (process != null) {
- process.destroy();
- }
- }
- }
-
- /**
- * Returns true if --pid=
option is supported
- */
- private boolean logcatCanFilterByProcess() {
- // This is a bit hacky but we run `logcat --help` and analyze the output to see if
- // --pid= option is there
-
- try {
- Process process = Runtime.getRuntime().exec(new String[]{"logcat", "--help"});
- BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()), 1024);
-
- String line;
- while ((line = reader.readLine()) != null) {
- if (line.contains("--pid=")) {
- return true;
- }
- }
- } catch (Exception e) {
- Log.e(TAG, "Exception while trying to figure out if logcat can filter by process id");
- }
- return false;
- }
-
- //endregion
-}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/debug/Tester.java b/apptentive/src/main/java/com/apptentive/android/sdk/debug/Tester.java
deleted file mode 100644
index d1c80d394..000000000
--- a/apptentive/src/main/java/com/apptentive/android/sdk/debug/Tester.java
+++ /dev/null
@@ -1,91 +0,0 @@
-package com.apptentive.android.sdk.debug;
-
-import com.apptentive.android.sdk.ApptentiveLog;
-import com.apptentive.android.sdk.util.ObjectUtils;
-
-import java.util.Map;
-
-import static com.apptentive.android.sdk.debug.TesterEvent.*;
-
-public class Tester {
- private static Tester instance;
-
- private TesterEventListener listener;
-
- private Tester(TesterEventListener listener) {
- this.listener = listener;
- }
-
- ////////////////////////////////////////////////////////////////
- // Instance
-
- public static void init(TesterEventListener delegate) {
- instance = new Tester(delegate);
- }
-
- public static void destroy() {
- instance = null;
- }
-
- ////////////////////////////////////////////////////////////////
- // Events
-
- public static boolean isListeningForDebugEvents() {
- return instance != null && instance.listener != null;
- }
-
- public static void dispatchDebugEvent(String name) {
- if (isListeningForDebugEvents()) {
- notifyEvent(name, null);
- }
- }
-
- public static void dispatchDebugEvent(String name, boolean successful) {
- if (isListeningForDebugEvents()) {
- notifyEvent(name, ObjectUtils.toMap(EVT_KEY_SUCCESSFUL, successful));
- }
- }
-
- public static void dispatchDebugEvent(String name, String key, Object value) {
- if (isListeningForDebugEvents()) {
- notifyEvent(name, ObjectUtils.toMap(key, value));
- }
- }
-
- public static void dispatchDebugEvent(String name, String key1, Object value1, String key2, Object value2) {
- if (isListeningForDebugEvents()) {
- notifyEvent(name, ObjectUtils.toMap(key1, value1, key2, value2));
- }
- }
-
- public static void dispatchDebugEvent(String name, String key1, Object value1, String key2, Object value2, String key3, Object value3) {
- if (isListeningForDebugEvents()) {
- notifyEvent(name, ObjectUtils.toMap(key1, value1, key2, value2, key3, value3));
- }
- }
-
- public static void dispatchException(Throwable e) {
- if (isListeningForDebugEvents() && e != null) {
- StringBuilder stackTrace = new StringBuilder();
- StackTraceElement[] elements = e.getStackTrace();
- for (int i = 0; i < elements.length; ++i) {
- stackTrace.append(elements[i]);
- if (i < elements.length - 1) {
- stackTrace.append('\n');
- }
- }
- notifyEvent(EVT_EXCEPTION, ObjectUtils.toMap(
- "class,", e.getClass().getName(),
- "message", e.getMessage(),
- "stacktrace", stackTrace.toString()));
- }
- }
-
- private static void notifyEvent(String name, Map userInfo) {
- try {
- instance.listener.onDebugEvent(name, userInfo);
- } catch (Exception e) {
- ApptentiveLog.e(e, "Error while dispatching debug event: %s", name);
- }
- }
-}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/debug/TesterEvent.java b/apptentive/src/main/java/com/apptentive/android/sdk/debug/TesterEvent.java
deleted file mode 100644
index fcf0f0431..000000000
--- a/apptentive/src/main/java/com/apptentive/android/sdk/debug/TesterEvent.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.apptentive.android.sdk.debug;
-
-public class TesterEvent {
-
- /** An active conversation loading finished (might be either successful or failed) */
- public static final String EVT_CONVERSATION_LOAD = "conversation_load"; // { successful:boolean, conversation_state:String, conversation_identifier:String }
-
- /** An active conversation state changed */
- public static final String EVT_CONVERSATION_STATE_CHANGE = "conversation_state_change"; // { conversation_state:String, conversation_identifier:String }
-
- /** Conversation metadata loading finished (might be either successful or failed) */
- public static final String EVT_CONVERSATION_METADATA_LOAD = "conversation_metadata_load"; // { successful:boolean }
-
- /** Conversation token fetch request started */
- public static final String EVT_CONVERSATION_WILL_FETCH_TOKEN = "conversation_will_fetch_token";
-
- /** Conversation token fetch request finished (might be either successful or failed) */
- public static final String EVT_CONVERSATION_DID_FETCH_TOKEN = "conversation_did_fetch_token"; // { successful:boolean }
-
- /** Conversation interactions fetch request finished (might be either successful or failed) */
- public static final String EVT_CONVERSATION_FETCH_INTERACTIONS = "conversation_fetch_interactions"; // { successful:boolean }
-
- /** There was an unexpected runtime exception */
- public static final String EVT_EXCEPTION = "exception"; // { class:String, message:String, stackTrace:String }
-
- /** Apptentive event was sent */
- public static final String EVT_APPTENTIVE_EVENT = "apptentive_event"; // { eventLabel:String }
- public static final String EVT_APPTENTIVE_EVENT_KEY_EVENT_LABEL = "eventLabel";
-
- /** The login attempt finished (might be either successful or failed) */
- public static final String EVT_LOGIN_FINISHED = "login_finished";
- public static final String EVT_LOGIN_FINISHED_ERROR_MESSAGE = "error_message";
-
- /** The logout call finished */
- public static final String EVT_LOGOUT = "logout";
-
- // Common event keys
- public static final String EVT_KEY_SUCCESSFUL = "successful";
-}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/debug/TesterEventListener.java b/apptentive/src/main/java/com/apptentive/android/sdk/debug/TesterEventListener.java
deleted file mode 100644
index 13dc553aa..000000000
--- a/apptentive/src/main/java/com/apptentive/android/sdk/debug/TesterEventListener.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.apptentive.android.sdk.debug;
-
-import java.util.Map;
-
-public interface TesterEventListener
-{
- void onDebugEvent(String name, Map userInfo);
-}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/debug/TroubleshootingNotification.java b/apptentive/src/main/java/com/apptentive/android/sdk/debug/TroubleshootingNotificationBuilder.java
similarity index 76%
rename from apptentive/src/main/java/com/apptentive/android/sdk/debug/TroubleshootingNotification.java
rename to apptentive/src/main/java/com/apptentive/android/sdk/debug/TroubleshootingNotificationBuilder.java
index 07495bf08..87a43b050 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/debug/TroubleshootingNotification.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/debug/TroubleshootingNotificationBuilder.java
@@ -24,25 +24,26 @@
import static android.content.Context.NOTIFICATION_SERVICE;
-public class TroubleshootingNotification {
+final class TroubleshootingNotificationBuilder {
- public static final String NOTIFICATION_CHANNEL_ID = "com.apptentive.debug.NOTIFICATION_CHANNEL_TROUBLESHOOTING";
- public static final String NOTIFICATION_CHANNEL_NAME = "Apptentive Notifications";
- public static final String NOTIFICATION_CHANNEL_DESCRIPTION = "Used for SDK troubleshooting";
- public static final String NOTIFICATION_ID_KEY = "com.apptentive.debug.NOTIFICATION_ID";
- public static final int APPTENTIVE_NOTIFICATION_ID = 1;
+ private static final String NOTIFICATION_CHANNEL_ID = "com.apptentive.debug.NOTIFICATION_CHANNEL_TROUBLESHOOTING";
+ private static final String NOTIFICATION_CHANNEL_NAME = "Apptentive Notifications";
+ private static final String NOTIFICATION_CHANNEL_DESCRIPTION = "Used for SDK troubleshooting";
+ static final String NOTIFICATION_ID_KEY = "com.apptentive.debug.NOTIFICATION_ID";
+ private static final int APPTENTIVE_NOTIFICATION_ID = 1;
- public static final String ACTION_ABORT = "com.apptentive.debug.ACTION_ABORT";
- public static final String ACTION_SEND_LOGS = "com.apptentive.debug.ACTION_SEND_LOGS";
+ static final String ACTION_ABORT = "com.apptentive.debug.ACTION_ABORT";
+ static final String ACTION_SEND_LOGS = "com.apptentive.debug.ACTION_SEND_LOGS";
- public static final String EXTRA_EMAIL_RECIPIENTS = "EMAIL_RECIPIENTS";
- public static final String EXTRA_SUBJECT = "SUBJECT";
- public static final String EXTRA_INFO = "INFO";
- public static final String EXTRA_LOG_FILE = "LOG_FILE";
- public static final String EXTRA_MANIFEST_FILE = "MANIFEST_FILE";
+ static final String EXTRA_EMAIL_RECIPIENTS = "EMAIL_RECIPIENTS";
+ static final String EXTRA_SUBJECT = "SUBJECT";
+ static final String EXTRA_INFO = "INFO";
+ static final String EXTRA_ATTACHMENTS = "ATTACHMENTS";
+ private TroubleshootingNotificationBuilder() {
+ }
- public Notification buildNotification(@NonNull Context context, String subject, String systemInfo, File logFile, File manifestFile, String[] emailRecipients) {
+ static Notification buildNotification(@NonNull Context context, String subject, String systemInfo, File[] attachments, String[] emailRecipients) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
@@ -59,8 +60,7 @@ public Notification buildNotification(@NonNull Context context, String subject,
sendLogsIntent.putExtra(EXTRA_EMAIL_RECIPIENTS, emailRecipients);
sendLogsIntent.putExtra(EXTRA_SUBJECT, subject);
sendLogsIntent.putExtra(EXTRA_INFO, systemInfo);
- sendLogsIntent.putExtra(EXTRA_LOG_FILE, logFile);
- sendLogsIntent.putExtra(EXTRA_MANIFEST_FILE, manifestFile);
+ sendLogsIntent.putExtra(EXTRA_ATTACHMENTS, attachments);
PendingIntent sendLogsPendingIntent = PendingIntent.getBroadcast(context, 0, sendLogsIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action sendLogsAction = new NotificationCompat.Action.Builder(0, "Send Report", sendLogsPendingIntent).build();
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/lifecycle/ApptentiveActivityLifecycleCallbacks.java b/apptentive/src/main/java/com/apptentive/android/sdk/lifecycle/ApptentiveActivityLifecycleCallbacks.java
index 42853483f..382e53f4a 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/lifecycle/ApptentiveActivityLifecycleCallbacks.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/lifecycle/ApptentiveActivityLifecycleCallbacks.java
@@ -97,7 +97,7 @@ public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
* @param activity
*/
@Override
- public void onActivityStopped(Activity activity) {
+ public void onActivityStopped(final Activity activity) {
if (foregroundActivities.decrementAndGet() < 0) {
ApptentiveLog.e("Incorrect number of foreground Activities encountered. Resetting to 0.");
foregroundActivities.set(0);
@@ -125,6 +125,12 @@ public void run() {
}
}, CHECK_DELAY_SHORT);
+ dispatchOnConversationQueue(new DispatchTask() {
+ @Override
+ protected void execute() {
+ ApptentiveInternal.getInstance().onActivityStopped(activity);
+ }
+ });
}
@Override
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/migration/Migrator.java b/apptentive/src/main/java/com/apptentive/android/sdk/migration/Migrator.java
index 7f4efc3c8..fc5ab9c7c 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/migration/Migrator.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/migration/Migrator.java
@@ -11,6 +11,7 @@
import com.apptentive.android.sdk.Apptentive;
import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.ApptentiveLogTag;
import com.apptentive.android.sdk.conversation.Conversation;
import com.apptentive.android.sdk.migration.v4_0_0.CodePointStore;
import com.apptentive.android.sdk.migration.v4_0_0.VersionHistoryEntry;
@@ -36,6 +37,8 @@
import java.util.Iterator;
import java.util.Map;
+import static com.apptentive.android.sdk.ApptentiveLogTag.CONVERSATION;
+
public class Migrator {
private Conversation conversation;
@@ -145,7 +148,7 @@ private void migrateDevice() {
conversation.setDevice(device);
}
} catch (Exception e) {
- ApptentiveLog.e(e, "Error migrating Device.");
+ ApptentiveLog.e(CONVERSATION, e, "Error migrating Device.");
}
}
@@ -164,7 +167,7 @@ private void migrateSdk() {
sdk.setAuthorEmail(sdkOld.getAuthorEmail());
conversation.setSdk(sdk);
} catch (Exception e) {
- ApptentiveLog.e(e, "Error migrating Sdk.");
+ ApptentiveLog.e(CONVERSATION, e, "Error migrating Sdk.");
}
}
}
@@ -186,7 +189,7 @@ private void migrateAppRelease() {
appRelease.setVersionName(appReleaseOld.getVersionName());
conversation.setAppRelease(appRelease);
} catch (Exception e) {
- ApptentiveLog.e(e, "Error migrating AppRelease.");
+ ApptentiveLog.e(CONVERSATION, e, "Error migrating AppRelease.");
}
}
}
@@ -226,7 +229,7 @@ private void migratePerson() {
}
conversation.setPerson(person);
} catch (Exception e) {
- ApptentiveLog.e(e, "Error migrating Person.");
+ ApptentiveLog.e(CONVERSATION, e, "Error migrating Person.");
}
}
}
@@ -250,7 +253,7 @@ private void migrateVersionHistory() {
}
}
} catch (Exception e) {
- ApptentiveLog.w(e, "Error migrating VersionHistory entries V2 to V3.");
+ ApptentiveLog.w(CONVERSATION, e, "Error migrating VersionHistory entries V2 to V3.");
}
}
@@ -268,7 +271,7 @@ private void migrateEventData() {
eventData.setInteractions(migratedInteractions);
}
} catch (Exception e) {
- ApptentiveLog.w(e, "Error migrating Event Data.");
+ ApptentiveLog.w(CONVERSATION, e, "Error migrating Event Data.");
}
}
@@ -286,7 +289,7 @@ private static Serializable jsonObjectToSerializableType(JSONObject input) {
}
}
} catch (JSONException e) {
- ApptentiveLog.e(e, "Error migrating JSONObject.");
+ ApptentiveLog.e(CONVERSATION, e, "Error migrating JSONObject.");
}
return null;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/AppRelease.java b/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/AppRelease.java
index e6a7ffcad..e37c1e667 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/AppRelease.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/AppRelease.java
@@ -11,6 +11,8 @@
import org.json.JSONException;
import org.json.JSONObject;
+import static com.apptentive.android.sdk.ApptentiveLogTag.CONVERSATION;
+
public class AppRelease extends JSONObject {
private static final String KEY_TYPE = "type";
@@ -38,7 +40,7 @@ public void setType(String type) {
try {
put(KEY_TYPE, type);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to AppRelease.", KEY_TYPE);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to AppRelease.", KEY_TYPE);
}
}
@@ -53,7 +55,7 @@ public void setVersionName(String versionName) {
try {
put(KEY_VERSION_NAME, versionName);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to AppRelease.", KEY_VERSION_NAME);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to AppRelease.", KEY_VERSION_NAME);
}
}
@@ -68,7 +70,7 @@ public void setVersionCode(int versionCode) {
try {
put(KEY_VERSION_CODE, versionCode);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to AppRelease.", KEY_VERSION_CODE);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to AppRelease.", KEY_VERSION_CODE);
}
}
@@ -83,7 +85,7 @@ public void setIdentifier(String identifier) {
try {
put(KEY_IDENTIFIER, identifier);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to AppRelease.", KEY_IDENTIFIER);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to AppRelease.", KEY_IDENTIFIER);
}
}
@@ -98,7 +100,7 @@ public void setTargetSdkVersion(String targetSdkVersion) {
try {
put(KEY_TARGET_SDK_VERSION, targetSdkVersion);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to AppRelease.", KEY_TARGET_SDK_VERSION);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to AppRelease.", KEY_TARGET_SDK_VERSION);
}
}
@@ -113,7 +115,7 @@ public void setAppStore(String appStore) {
try {
put(KEY_APP_STORE, appStore);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to AppRelease.", KEY_APP_STORE);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to AppRelease.", KEY_APP_STORE);
}
}
@@ -126,7 +128,7 @@ public void setInheritStyle(boolean inheritStyle) {
try {
put(KEY_STYLE_INHERIT, inheritStyle);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to AppRelease.", KEY_STYLE_INHERIT);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to AppRelease.", KEY_STYLE_INHERIT);
}
}
@@ -139,7 +141,7 @@ public void setOverrideStyle(boolean overrideStyle) {
try {
put(KEY_STYLE_OVERRIDE, overrideStyle);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to AppRelease.", KEY_STYLE_OVERRIDE);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to AppRelease.", KEY_STYLE_OVERRIDE);
}
}
@@ -151,7 +153,7 @@ public void setDebug(boolean debug) {
try {
put(KEY_DEBUG, debug);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to AppRelease.", KEY_DEBUG);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to AppRelease.", KEY_DEBUG);
}
}
}
\ No newline at end of file
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/Device.java b/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/Device.java
index 57d568396..5c2fe9fb0 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/Device.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/Device.java
@@ -11,6 +11,8 @@
import org.json.JSONException;
import org.json.JSONObject;
+import static com.apptentive.android.sdk.ApptentiveLogTag.CONVERSATION;
+
public class Device extends JSONObject {
private static final String KEY_UUID = "uuid";
@@ -58,7 +60,7 @@ public void setUuid(String uuid) {
try {
put(KEY_UUID, uuid);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_UUID);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_UUID);
}
}
@@ -77,7 +79,7 @@ public void setOsName(String osName) {
try {
put(KEY_OS_NAME, osName);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_OS_NAME);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_OS_NAME);
}
}
@@ -96,7 +98,7 @@ public void setOsVersion(String osVersion) {
try {
put(KEY_OS_VERSION, osVersion);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_OS_VERSION);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_OS_VERSION);
}
}
@@ -115,7 +117,7 @@ public void setOsBuild(String osBuild) {
try {
put(KEY_OS_BUILD, osBuild);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_OS_BUILD);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_OS_BUILD);
}
}
@@ -134,7 +136,7 @@ public void setOsApiLevel(String osApiLevel) {
try {
put(KEY_OS_API_LEVEL, osApiLevel);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_OS_API_LEVEL);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_OS_API_LEVEL);
}
}
@@ -153,7 +155,7 @@ public void setManufacturer(String manufacturer) {
try {
put(KEY_MANUFACTURER, manufacturer);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_MANUFACTURER);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_MANUFACTURER);
}
}
@@ -172,7 +174,7 @@ public void setModel(String model) {
try {
put(KEY_MODEL, model);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_MODEL);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_MODEL);
}
}
@@ -191,7 +193,7 @@ public void setBoard(String board) {
try {
put(KEY_BOARD, board);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_BOARD);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_BOARD);
}
}
@@ -210,7 +212,7 @@ public void setProduct(String product) {
try {
put(KEY_PRODUCT, product);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_PRODUCT);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_PRODUCT);
}
}
@@ -229,7 +231,7 @@ public void setBrand(String brand) {
try {
put(KEY_BRAND, brand);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_BRAND);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_BRAND);
}
}
@@ -248,7 +250,7 @@ public void setCpu(String cpu) {
try {
put(KEY_CPU, cpu);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_CPU);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_CPU);
}
}
@@ -267,7 +269,7 @@ public void setDevice(String device) {
try {
put(KEY_DEVICE, device);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_DEVICE);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_DEVICE);
}
}
@@ -286,7 +288,7 @@ public void setCarrier(String carrier) {
try {
put(KEY_CARRIER, carrier);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_CARRIER);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_CARRIER);
}
}
@@ -305,7 +307,7 @@ public void setCurrentCarrier(String currentCarrier) {
try {
put(KEY_CURRENT_CARRIER, currentCarrier);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_CURRENT_CARRIER);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_CURRENT_CARRIER);
}
}
@@ -324,7 +326,7 @@ public void setNetworkType(String networkType) {
try {
put(KEY_NETWORK_TYPE, networkType);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_NETWORK_TYPE);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_NETWORK_TYPE);
}
}
@@ -343,7 +345,7 @@ public void setBuildType(String buildType) {
try {
put(KEY_BUILD_TYPE, buildType);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_BUILD_TYPE);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_BUILD_TYPE);
}
}
@@ -362,7 +364,7 @@ public void setBuildId(String buildId) {
try {
put(KEY_BUILD_ID, buildId);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_BUILD_ID);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_BUILD_ID);
}
}
@@ -381,7 +383,7 @@ public void setBootloaderVersion(String bootloaderVersion) {
try {
put(KEY_BOOTLOADER_VERSION, bootloaderVersion);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_BOOTLOADER_VERSION);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_BOOTLOADER_VERSION);
}
}
@@ -400,7 +402,7 @@ public void setRadioVersion(String radioVersion) {
try {
put(KEY_RADIO_VERSION, radioVersion);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_RADIO_VERSION);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_RADIO_VERSION);
}
}
@@ -420,7 +422,7 @@ public void setCustomData(JSONObject customData) {
try {
put(KEY_CUSTOM_DATA, customData);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_CUSTOM_DATA);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_CUSTOM_DATA);
}
}
@@ -440,7 +442,7 @@ public void setIntegrationConfig(JSONObject integrationConfig) {
try {
put(KEY_INTEGRATION_CONFIG, integrationConfig);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_INTEGRATION_CONFIG);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_INTEGRATION_CONFIG);
}
}
@@ -459,7 +461,7 @@ public void setLocaleCountryCode(String localeCountryCode) {
try {
put(KEY_LOCALE_COUNTRY_CODE, localeCountryCode);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_LOCALE_COUNTRY_CODE);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_LOCALE_COUNTRY_CODE);
}
}
@@ -478,7 +480,7 @@ public void setLocaleLanguageCode(String localeLanguageCode) {
try {
put(KEY_LOCALE_LANGUAGE_CODE, localeLanguageCode);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_LOCALE_LANGUAGE_CODE);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_LOCALE_LANGUAGE_CODE);
}
}
@@ -497,7 +499,7 @@ public void setLocaleRaw(String localeRaw) {
try {
put(KEY_LOCALE_RAW, localeRaw);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_LOCALE_RAW);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_LOCALE_RAW);
}
}
@@ -516,7 +518,7 @@ public void setUtcOffset(String utcOffset) {
try {
put(KEY_UTC_OFFSET, utcOffset);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Device.", KEY_UTC_OFFSET);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Device.", KEY_UTC_OFFSET);
}
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/Sdk.java b/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/Sdk.java
index eef52f973..4823fdb66 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/Sdk.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/Sdk.java
@@ -10,6 +10,8 @@
import org.json.JSONException;
import org.json.JSONObject;
+import static com.apptentive.android.sdk.ApptentiveLogTag.CONVERSATION;
+
public class Sdk extends JSONObject {
private static final String KEY_VERSION = "version";
@@ -39,7 +41,7 @@ public void setVersion(String version) {
try {
put(KEY_VERSION, version);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Sdk.", KEY_VERSION);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Sdk.", KEY_VERSION);
}
}
@@ -58,7 +60,7 @@ public void setProgrammingLanguage(String programmingLanguage) {
try {
put(KEY_PROGRAMMING_LANGUAGE, programmingLanguage);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Sdk.", KEY_PROGRAMMING_LANGUAGE);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Sdk.", KEY_PROGRAMMING_LANGUAGE);
}
}
@@ -77,7 +79,7 @@ public void setAuthorName(String authorName) {
try {
put(KEY_AUTHOR_NAME, authorName);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Sdk.", KEY_AUTHOR_NAME);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Sdk.", KEY_AUTHOR_NAME);
}
}
@@ -96,7 +98,7 @@ public void setAuthorEmail(String authorEmail) {
try {
put(KEY_AUTHOR_EMAIL, authorEmail);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Sdk.", KEY_AUTHOR_EMAIL);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Sdk.", KEY_AUTHOR_EMAIL);
}
}
@@ -115,7 +117,7 @@ public void setPlatform(String platform) {
try {
put(KEY_PLATFORM, platform);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Sdk.", KEY_PLATFORM);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Sdk.", KEY_PLATFORM);
}
}
@@ -134,7 +136,7 @@ public void setDistribution(String distribution) {
try {
put(KEY_DISTRIBUTION, distribution);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Sdk.", KEY_DISTRIBUTION);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Sdk.", KEY_DISTRIBUTION);
}
}
@@ -153,7 +155,7 @@ public void setDistributionVersion(String distributionVersion) {
try {
put(KEY_DISTRIBUTION_VERSION, distributionVersion);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Sdk.", KEY_DISTRIBUTION_VERSION);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Sdk.", KEY_DISTRIBUTION_VERSION);
}
}
}
\ No newline at end of file
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/VersionHistoryStore.java b/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/VersionHistoryStore.java
index 85bebd09e..9fc3a8406 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/VersionHistoryStore.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/VersionHistoryStore.java
@@ -12,6 +12,7 @@
import com.apptentive.android.sdk.ApptentiveInternal;
import com.apptentive.android.sdk.ApptentiveLog;
import com.apptentive.android.sdk.util.Constants;
+import com.apptentive.android.sdk.util.RuntimeUtils;
import com.apptentive.android.sdk.util.Util;
import org.json.JSONArray;
@@ -21,6 +22,8 @@
import java.util.List;
import java.util.Set;
+import static com.apptentive.android.sdk.ApptentiveLogTag.CONVERSATION;
+
/**
* Stores version history in JSON, in SharedPreferences.
*/
@@ -56,7 +59,7 @@ private static void ensureLoaded() {
versionHistoryEntries.add(entry);
}
} catch (Exception e) {
- ApptentiveLog.w(e, "Error loading VersionHistoryStore.");
+ ApptentiveLog.w(CONVERSATION, e, "Error loading VersionHistoryStore.");
}
}
}
@@ -84,12 +87,12 @@ public static synchronized void updateVersionHistory(Integer newVersionCode, Str
// Only modify the store if the version hasn't been seen.
if (!exists) {
VersionHistoryEntry entry = new VersionHistoryEntry(newVersionCode, newVersionName, date);
- ApptentiveLog.d("Adding Version History entry: %s", entry);
+ ApptentiveLog.v(CONVERSATION, "Adding Version History entry: %s", entry);
versionHistoryEntries.add(new VersionHistoryEntry(newVersionCode, newVersionName, date));
save();
}
} catch (Exception e) {
- ApptentiveLog.w(e, "Error updating VersionHistoryStore.");
+ ApptentiveLog.w(CONVERSATION, e, "Error updating VersionHistoryStore.");
}
}
@@ -108,7 +111,7 @@ public static synchronized Apptentive.DateTime getTimeAtInstall(Selector selecto
// Since the list is ordered, this will be the first and oldest entry.
return new Apptentive.DateTime(entry.getTimestamp());
case version_code:
- if (entry.getVersionCode() == Util.getAppVersionCode(ApptentiveInternal.getInstance().getApplicationContext())) {
+ if (entry.getVersionCode() == RuntimeUtils.getAppVersionCode(ApptentiveInternal.getInstance().getApplicationContext())) {
return new Apptentive.DateTime(entry.getTimestamp());
}
break;
@@ -116,7 +119,7 @@ public static synchronized Apptentive.DateTime getTimeAtInstall(Selector selecto
Apptentive.Version entryVersionName = new Apptentive.Version();
Apptentive.Version currentVersionName = new Apptentive.Version();
entryVersionName.setVersion(entry.getVersionName());
- currentVersionName.setVersion(Util.getAppVersionName(ApptentiveInternal.getInstance().getApplicationContext()));
+ currentVersionName.setVersion(RuntimeUtils.getAppVersionName(ApptentiveInternal.getInstance().getApplicationContext()));
if (entryVersionName.equals(currentVersionName)) {
return new Apptentive.DateTime(entry.getTimestamp());
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/VersionHistoryStoreMigrator.java b/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/VersionHistoryStoreMigrator.java
index cc95b01bb..6d1fd03f2 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/VersionHistoryStoreMigrator.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/migration/v4_0_0/VersionHistoryStoreMigrator.java
@@ -12,6 +12,8 @@
import com.apptentive.android.sdk.ApptentiveLog;
import com.apptentive.android.sdk.util.Constants;
+import static com.apptentive.android.sdk.ApptentiveLogTag.CONVERSATION;
+
public class VersionHistoryStoreMigrator {
private static final String OLD_ENTRY_SEP = "__";
@@ -23,8 +25,8 @@ public class VersionHistoryStoreMigrator {
private static boolean migrated_to_v2;
public static void migrateV1ToV2(String oldFormat) {
- ApptentiveLog.i("Migrating VersionHistoryStore V1 to V2.");
- ApptentiveLog.i("V1: %s", oldFormat);
+ ApptentiveLog.i(CONVERSATION, "Migrating VersionHistoryStore V1 to V2.");
+ ApptentiveLog.i(CONVERSATION, "V1: %s", oldFormat);
try {
String[] entriesOld = oldFormat.split(OLD_ENTRY_SEP);
for (String entryOld : entriesOld) {
@@ -36,12 +38,12 @@ public static void migrateV1ToV2(String oldFormat) {
Double.parseDouble(entryPartsOld[OLD_POSITION_TIMESTAMP])
);
} catch (Exception e) {
- ApptentiveLog.w("Error migrating old version history entry: %s", entryOld);
+ ApptentiveLog.w(CONVERSATION, "Error migrating old version history entry: %s", entryOld);
}
}
- ApptentiveLog.i("V2: %s", VersionHistoryStore.getBaseArray().toString());
+ ApptentiveLog.i(CONVERSATION, "V2: %s", VersionHistoryStore.getBaseArray().toString());
} catch (Exception e) {
- ApptentiveLog.w("Error migrating old version history entries: %s", oldFormat);
+ ApptentiveLog.w(CONVERSATION, "Error migrating old version history entries: %s", oldFormat);
}
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/ApptentiveMessage.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/ApptentiveMessage.java
index 876e46336..8157810c3 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/model/ApptentiveMessage.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/ApptentiveMessage.java
@@ -14,6 +14,8 @@
import java.util.Map;
+import static com.apptentive.android.sdk.ApptentiveLogTag.MESSAGES;
+
public abstract class ApptentiveMessage extends ConversationItem implements MessageCenterListItem {
public static final String KEY_ID = "id";
@@ -22,12 +24,12 @@ public abstract class ApptentiveMessage extends ConversationItem implements Mess
public static final String KEY_HIDDEN = "hidden";
/** inbound here means inbound to the server. When this is true, the message is outgoing */
public static final String KEY_INBOUND = "inbound";
- public static final String KEY_CUSTOM_DATA = "custom_data";
+ @SensitiveDataKey public static final String KEY_CUSTOM_DATA = "custom_data";
public static final String KEY_AUTOMATED = "automated";
public static final String KEY_SENDER = "sender";
public static final String KEY_SENDER_ID = "id";
- private static final String KEY_SENDER_NAME = "name";
- private static final String KEY_SENDER_PROFILE_PHOTO = "profile_photo";
+ @SensitiveDataKey private static final String KEY_SENDER_NAME = "name";
+ @SensitiveDataKey private static final String KEY_SENDER_PROFILE_PHOTO = "profile_photo";
// State and Read are not stored in JSON, only in DB.
private State state = State.unknown;
@@ -36,7 +38,7 @@ public abstract class ApptentiveMessage extends ConversationItem implements Mess
// datestamp is only stored in memory, due to how we selectively apply date labeling in the view.
private String datestamp;
-
+ // this an abstract class so we don't need to register it's sensitive keys (subclasses will do)
protected ApptentiveMessage() {
super(PayloadType.message);
@@ -232,7 +234,7 @@ public static Type parse(String rawType) {
try {
return Type.valueOf(rawType);
} catch (IllegalArgumentException e) {
- ApptentiveLog.v("Error parsing unknown ApptentiveMessage.Type: " + rawType);
+ ApptentiveLog.v(MESSAGES, "Error parsing unknown ApptentiveMessage.Type: " + rawType);
}
return unknown;
}
@@ -248,7 +250,7 @@ public static State parse(String state) {
try {
return State.valueOf(state);
} catch (IllegalArgumentException e) {
- ApptentiveLog.v("Error parsing unknown ApptentiveMessage.State: " + state);
+ ApptentiveLog.v(MESSAGES, "Error parsing unknown ApptentiveMessage.State: " + state);
}
return unknown;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/CompoundMessage.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/CompoundMessage.java
index 1d4071574..87abd4c81 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/model/CompoundMessage.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/CompoundMessage.java
@@ -26,16 +26,16 @@
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
+import static com.apptentive.android.sdk.ApptentiveLogTag.MESSAGES;
import static com.apptentive.android.sdk.ApptentiveLogTag.PAYLOADS;
public class CompoundMessage extends ApptentiveMessage implements MessageCenterUtil.CompoundMessageCommonInterface {
- private static final String KEY_BODY = "body";
+ @SensitiveDataKey private static final String KEY_BODY = "body";
public static final String KEY_TEXT_ONLY = "text_only";
- private static final String KEY_TITLE = "title";
+ @SensitiveDataKey private static final String KEY_TITLE = "title";
private static final String KEY_ATTACHMENTS = "attachments";
private boolean isLast;
@@ -50,6 +50,10 @@ public class CompoundMessage extends ApptentiveMessage implements MessageCenterU
*/
private ArrayList remoteAttachmentStoredFiles;
+ static {
+ registerSensitiveKeys(CompoundMessage.class);
+ }
+
// Default constructor will only be called when the message is created from local, a.k.a outgoing
public CompoundMessage() {
super();
@@ -151,7 +155,7 @@ public boolean setAssociatedImages(List attachedImages) {
Future future = ApptentiveInternal.getInstance().getApptentiveTaskManager().addCompoundMessageFiles(attachmentStoredFiles);
bRet = future.get();
} catch (Exception e) {
- ApptentiveLog.e("Unable to set associated images in worker thread");
+ ApptentiveLog.e(MESSAGES, "Unable to set associated images in worker thread");
} finally {
return bRet;
}
@@ -174,7 +178,7 @@ public boolean setAssociatedFiles(List attachedFiles) {
Future future = ApptentiveInternal.getInstance().getApptentiveTaskManager().addCompoundMessageFiles(attachedFiles);
bRet = future.get();
} catch (Exception e) {
- ApptentiveLog.e("Unable to set associated files in worker thread");
+ ApptentiveLog.e(MESSAGES, "Unable to set associated files in worker thread");
} finally {
return bRet;
}
@@ -189,7 +193,7 @@ public List getAssociatedFiles() {
Future> future = ApptentiveInternal.getInstance().getApptentiveTaskManager().getAssociatedFiles(getNonce());
associatedFiles = future.get();
} catch (Exception e) {
- ApptentiveLog.e("Unable to get associated files in worker thread");
+ ApptentiveLog.e(MESSAGES, "Unable to get associated files in worker thread");
} finally {
return associatedFiles;
}
@@ -211,7 +215,7 @@ public void deleteAssociatedFiles() {
// Delete records from db
ApptentiveInternal.getInstance().getApptentiveTaskManager().deleteAssociatedFiles(getNonce());
} catch (Exception e) {
- ApptentiveLog.e("Unable to delete associated files in worker thread");
+ ApptentiveLog.e(MESSAGES, "Unable to delete associated files in worker thread");
}
}
@@ -339,7 +343,7 @@ public byte[] renderData() {
ApptentiveLog.v(PAYLOADS, "Appending image attachment.");
ImageUtil.appendScaledDownImageToStream(storedFile.getSourceUriOrPath(), attachmentBytes);
} else {
- ApptentiveLog.v("Appending non-image attachment.");
+ ApptentiveLog.v(PAYLOADS, "Appending non-image attachment.");
Util.appendFileToStream(new File(storedFile.getSourceUriOrPath()), attachmentBytes);
}
} catch (Exception e) {
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/Configuration.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/Configuration.java
index b5edb2f07..3c45cf79a 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/model/Configuration.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/Configuration.java
@@ -18,11 +18,15 @@
import org.json.JSONException;
import org.json.JSONObject;
+import static com.apptentive.android.sdk.ApptentiveLogTag.CONVERSATION;
+
/**
* @author Sky Kelsey
*/
+// TODO: get rid of JSONObject
public class Configuration extends JSONObject {
private static final String KEY_METRICS_ENABLED = "metrics_enabled";
+ private static final String KEY_COLLECT_AD_ID = "collect_ad_id";
private static final String KEY_APP_DISPLAY_NAME = "app_display_name";
private static final String KEY_MESSAGE_CENTER = "message_center";
private static final String KEY_MESSAGE_CENTER_FG_POLL = "fg_poll";
@@ -36,6 +40,8 @@ public class Configuration extends JSONObject {
// This one is not sent in JSON, but as a header form the server.
private static final String KEY_CONFIGURATION_CACHE_EXPIRATION_MILLIS = "configuration_cache_expiration_millis";
+ // Store the last configuration object to avoid json parsing and disk IO
+ private static Configuration cachedConfiguration;
public Configuration() {
super();
@@ -48,11 +54,16 @@ public Configuration(String json) throws JSONException {
public void save() {
SharedPreferences prefs = ApptentiveInternal.getInstance().getGlobalSharedPrefs();
prefs.edit().putString(Constants.PREF_KEY_APP_CONFIG_JSON, toString()).apply();
+ cachedConfiguration = this;
}
public static Configuration load() {
- SharedPreferences prefs = ApptentiveInternal.getInstance().getGlobalSharedPrefs();
- return Configuration.load(prefs);
+ if (cachedConfiguration == null) {
+ SharedPreferences prefs = ApptentiveInternal.getInstance().getGlobalSharedPrefs();
+ cachedConfiguration = Configuration.load(prefs);
+ }
+
+ return cachedConfiguration;
}
public static Configuration load(SharedPreferences prefs) {
@@ -68,14 +79,11 @@ public static Configuration load(SharedPreferences prefs) {
}
public boolean isMetricsEnabled() {
- try {
- if (!isNull(KEY_METRICS_ENABLED)) {
- return getBoolean(KEY_METRICS_ENABLED);
- }
- } catch (JSONException e) {
- // Ignore
- }
- return true;
+ return getBoolean(KEY_METRICS_ENABLED, true);
+ }
+
+ public boolean isCollectingAdID() {
+ return getBoolean(KEY_COLLECT_AD_ID, false);
}
public String getAppDisplayName() {
@@ -129,15 +137,7 @@ public int getMessageCenterBgPoll() {
}
public boolean isMessageCenterEnabled() {
- try {
- if (!isNull(KEY_MESSAGE_CENTER_ENABLED)) {
- return getBoolean(KEY_MESSAGE_CENTER_ENABLED);
- }
- } catch (JSONException e) {
- // Move on.
- }
-
- return Constants.CONFIG_DEFAULT_MESSAGE_CENTER_ENABLED;
+ return getBoolean(KEY_MESSAGE_CENTER_ENABLED, Constants.CONFIG_DEFAULT_MESSAGE_CENTER_ENABLED);
}
public boolean isMessageCenterNotificationPopupEnabled() {
@@ -167,7 +167,7 @@ public boolean isHideBranding(Context context) {
Bundle metaData = ai.metaData;
return metaData.getBoolean(Constants.MANIFEST_KEY_INITIALLY_HIDE_BRANDING, Constants.CONFIG_DEFAULT_HIDE_BRANDING);
} catch (Exception e) {
- ApptentiveLog.w(e, "Unexpected error while reading %s manifest setting.", Constants.MANIFEST_KEY_INITIALLY_HIDE_BRANDING);
+ ApptentiveLog.w(CONVERSATION, e, "Unexpected error while reading %s manifest setting.", Constants.MANIFEST_KEY_INITIALLY_HIDE_BRANDING);
}
return Constants.CONFIG_DEFAULT_HIDE_BRANDING;
@@ -188,11 +188,24 @@ public void setConfigurationCacheExpirationMillis(long configurationCacheExpirat
try {
put(KEY_CONFIGURATION_CACHE_EXPIRATION_MILLIS, configurationCacheExpirationMillis);
} catch (JSONException e) {
- ApptentiveLog.w("Error adding %s to Configuration.", KEY_CONFIGURATION_CACHE_EXPIRATION_MILLIS);
+ ApptentiveLog.w(CONVERSATION, "Error adding %s to Configuration.", KEY_CONFIGURATION_CACHE_EXPIRATION_MILLIS);
}
}
public boolean hasConfigurationCacheExpired() {
return getConfigurationCacheExpirationMillis() < System.currentTimeMillis();
}
+
+ //region Helpers
+
+ private boolean getBoolean(String key, boolean defaultValue) {
+ try {
+ return optBoolean(key, defaultValue);
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while getting boolean key '%s'", key);
+ return defaultValue;
+ }
+ }
+
+ //endregion
}
\ No newline at end of file
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/ConversationTokenRequest.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/ConversationTokenRequest.java
index 630813e77..146254c53 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/model/ConversationTokenRequest.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/ConversationTokenRequest.java
@@ -7,6 +7,7 @@
package com.apptentive.android.sdk.model;
import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.ApptentiveLogTag;
import com.apptentive.android.sdk.util.StringUtils;
import org.json.JSONException;
@@ -14,6 +15,8 @@
import java.util.Iterator;
+import static com.apptentive.android.sdk.ApptentiveLogTag.CONVERSATION;
+
public class ConversationTokenRequest extends JSONObject {
public ConversationTokenRequest() {
}
@@ -22,7 +25,7 @@ public void setDevice(DevicePayload device) {
try {
put(DevicePayload.KEY, device == null ? null : device.getJsonObject());
} catch (JSONException e) {
- ApptentiveLog.e("Error adding %s to ConversationTokenRequest", DevicePayload.KEY);
+ ApptentiveLog.e(CONVERSATION, "Error adding %s to ConversationTokenRequest", DevicePayload.KEY);
}
}
@@ -30,7 +33,7 @@ public void setSdk(SdkPayload sdk) {
try {
put(SdkPayload.KEY, sdk == null ? null : sdk.getJsonObject());
} catch (JSONException e) {
- ApptentiveLog.e("Error adding %s to ConversationTokenRequest", SdkPayload.KEY);
+ ApptentiveLog.e(CONVERSATION, "Error adding %s to ConversationTokenRequest", SdkPayload.KEY);
}
}
@@ -38,7 +41,7 @@ public void setPerson(PersonPayload person) {
try {
put(PersonPayload.KEY, person == null ? null : person.getJsonObject());
} catch (JSONException e) {
- ApptentiveLog.e("Error adding %s to ConversationTokenRequest", PersonPayload.KEY);
+ ApptentiveLog.e(CONVERSATION, "Error adding %s to ConversationTokenRequest", PersonPayload.KEY);
}
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/CustomData.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/CustomData.java
index 675b87923..f9cde5133 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/model/CustomData.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/CustomData.java
@@ -6,6 +6,8 @@
import java.util.*;
+import static com.apptentive.android.sdk.ApptentiveLogTag.CONVERSATION;
+
/**
* @author Sky Kelsey
*/
@@ -54,7 +56,7 @@ public boolean equals(Object o) {
rightMap.put(key, right.getString(key));
}
} catch (JSONException e) {
- ApptentiveLog.e("Error comparing two device data entries: \"%s\" AND \"%s\"", left.toString(), right.toString());
+ ApptentiveLog.e(CONVERSATION, "Error comparing two device data entries: \"%s\" AND \"%s\"", left.toString(), right.toString());
}
return leftMap.equals(rightMap);
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/DevicePayload.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/DevicePayload.java
index 7150ccfe2..361357333 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/model/DevicePayload.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/DevicePayload.java
@@ -9,7 +9,6 @@
import com.apptentive.android.sdk.util.StringUtils;
import org.json.JSONException;
-import org.json.JSONObject;
public class DevicePayload extends JsonPayload {
@@ -34,13 +33,18 @@ public class DevicePayload extends JsonPayload {
private static final String KEY_BUILD_ID = "build_id";
private static final String KEY_BOOTLOADER_VERSION = "bootloader_version";
private static final String KEY_RADIO_VERSION = "radio_version";
- public static final String KEY_CUSTOM_DATA = "custom_data";
+ @SensitiveDataKey private static final String KEY_CUSTOM_DATA = "custom_data";
private static final String KEY_LOCALE_COUNTRY_CODE = "locale_country_code";
private static final String KEY_LOCALE_LANGUAGE_CODE = "locale_language_code";
private static final String KEY_LOCALE_RAW = "locale_raw";
private static final String KEY_UTC_OFFSET = "utc_offset";
+ @SensitiveDataKey private static final String KEY_ADVERTISER_ID = "advertiser_id";
private static final String KEY_INTEGRATION_CONFIG = "integration_config";
+ static {
+ registerSensitiveKeys(DevicePayload.class);
+ }
+
public DevicePayload() {
super(PayloadType.device);
}
@@ -178,6 +182,10 @@ public void setUtcOffset(String utcOffset) {
put(KEY_UTC_OFFSET, utcOffset);
}
+ public void setAdvertiserId(String advertiserId) {
+ put(KEY_ADVERTISER_ID, advertiserId);
+ }
+
@Override
protected String getJsonContainer() {
return "device";
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/EventPayload.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/EventPayload.java
index b79280f75..a97fa373c 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/model/EventPayload.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/EventPayload.java
@@ -16,6 +16,8 @@
import java.util.HashMap;
import java.util.Map;
+import static com.apptentive.android.sdk.ApptentiveLogTag.PAYLOADS;
+
/**
* @author Sky Kelsey
*/
@@ -25,7 +27,11 @@ public class EventPayload extends ConversationItem {
private static final String KEY_INTERACTION_ID = "interaction_id";
private static final String KEY_DATA = "data";
private static final String KEY_TRIGGER = "trigger";
- private static final String KEY_CUSTOM_DATA = "custom_data";
+ @SensitiveDataKey private static final String KEY_CUSTOM_DATA = "custom_data";
+
+ static {
+ registerSensitiveKeys(EventPayload.class);
+ }
public EventPayload(String json) throws JSONException {
super(PayloadType.event, json);
@@ -95,7 +101,7 @@ private JSONObject generateCustomDataJson(Map customData) {
try {
ret.put(key, value);
} catch (Exception e) {
- ApptentiveLog.w("Error adding custom data to Event: \"%s\" = \"%s\"", key, value.toString(), e);
+ ApptentiveLog.w(PAYLOADS, "Error adding custom data to Event: \"%s\" = \"%s\"", key, value.toString(), e);
}
}
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/ExtendedData.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/ExtendedData.java
index ea2bcfba8..046a63a69 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/model/ExtendedData.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/ExtendedData.java
@@ -13,6 +13,8 @@
import java.io.Serializable;
+import static com.apptentive.android.sdk.ApptentiveLogTag.PAYLOADS;
+
public abstract class ExtendedData implements Serializable {
private static final String KEY_VERSION = "version";
@@ -61,7 +63,7 @@ public static Type parse(String type) {
try {
return Type.valueOf(type);
} catch (IllegalArgumentException e) {
- ApptentiveLog.v("Error parsing unknown ExtendedData.PayloadType: " + type);
+ ApptentiveLog.v(PAYLOADS, "Error parsing unknown ExtendedData.PayloadType: " + type);
}
return unknown;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/JsonPayload.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/JsonPayload.java
index 1986e2178..fe2b0948f 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/model/JsonPayload.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/JsonPayload.java
@@ -9,17 +9,27 @@
import com.apptentive.android.sdk.ApptentiveLog;
import com.apptentive.android.sdk.encryption.Encryptor;
import com.apptentive.android.sdk.network.HttpRequestMethod;
+import com.apptentive.android.sdk.util.RuntimeUtils;
import com.apptentive.android.sdk.util.StringUtils;
import org.json.JSONException;
import org.json.JSONObject;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
import java.util.UUID;
import static com.apptentive.android.sdk.ApptentiveLogTag.PAYLOADS;
public abstract class JsonPayload extends Payload {
+ private static final Map, List> SENSITIVE_KEYS_LOOKUP = new HashMap<>();
+
private static final String KEY_NONCE = "nonce";
private final JSONObject jsonObject;
@@ -40,7 +50,7 @@ public JsonPayload(PayloadType type, String json) throws JSONException {
@Override
public byte[] renderData() throws JSONException {
String jsonString = marshallForSending().toString();
- ApptentiveLog.vv(PAYLOADS, jsonString);
+ ApptentiveLog.v(PAYLOADS, jsonString);
if (encryptionKey != null) {
byte[] bytes = jsonString.getBytes();
@@ -62,7 +72,7 @@ public byte[] renderData() throws JSONException {
protected void put(String key, String value) {
try {
- jsonObject.put(key, value);
+ jsonObject.put(key, toNullableValue(value));
} catch (Exception e) {
ApptentiveLog.e(e, "Exception while putting json pair '%s'='%s'", key, value);
}
@@ -94,7 +104,7 @@ protected void put(String key, double value) {
protected void put(String key, JSONObject object) {
try {
- jsonObject.put(key, object);
+ jsonObject.put(key, toNullableValue(object));
} catch (Exception e) {
ApptentiveLog.e(e, "Exception while putting json pair '%s'='%s'", key, object);
}
@@ -144,15 +154,43 @@ protected boolean isNull(String key) { // TODO: rename to containsKey
return jsonObject.isNull(key);
}
+ private Object toNullableValue(Object value) {
+ return value != null ? value : JSONObject.NULL;
+ }
+
//endregion
//region String Representation
@Override
public String toString() {
+ if (ApptentiveLog.shouldSanitizeLogMessages()) {
+ JSONObject safeJsonObject = createSafeJsonObject(jsonObject);
+ return StringUtils.format("%s %s", getClass().getSimpleName(), safeJsonObject);
+ }
return StringUtils.format("%s %s", getClass().getSimpleName(), jsonObject);
}
+ private JSONObject createSafeJsonObject(JSONObject jsonObject) {
+ try {
+ List sensitiveKeys = SENSITIVE_KEYS_LOOKUP.get(getClass());
+ if (sensitiveKeys != null && sensitiveKeys.size() > 0) {
+ JSONObject safeObject = new JSONObject();
+ Iterator iterator = jsonObject.keys();
+ while (iterator.hasNext()) {
+ String key = iterator.next();
+ Object value = sensitiveKeys.contains(key) ? "" : jsonObject.get(key);
+ safeObject.put(key, value);
+ }
+ return safeObject;
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while creating safe json object");
+ }
+
+ return null;
+ }
+
//endregion
//region Getters/Setters
@@ -207,4 +245,33 @@ protected final JSONObject marshallForSending() throws JSONException {
protected String getJsonContainer() {
return null;
}
+
+ //region Sensitive Keys
+
+ protected static void registerSensitiveKeys(Class extends JsonPayload> cls) {
+ List fields = RuntimeUtils.listFields(cls, new RuntimeUtils.FieldFilter() {
+ @Override
+ public boolean accept(Field field) {
+ return Modifier.isStatic(field.getModifiers()) && // static fields
+ field.getAnnotation(SensitiveDataKey.class) != null && // marked as 'sensitive'
+ field.getType().equals(String.class); // with type of String
+ }
+ });
+
+ if (fields.size() > 0) {
+ List keys = new ArrayList<>(fields.size());
+ try {
+ for (Field field : fields) {
+ field.setAccessible(true);
+ String value = (String) field.get(null);
+ keys.add(value);
+ }
+ SENSITIVE_KEYS_LOOKUP.put(cls, keys);
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while registering sensitive keys");
+ }
+ }
+ }
+
+ //endregion
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/PayloadData.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/PayloadData.java
index 176dfb7cf..cd97db635 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/model/PayloadData.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/PayloadData.java
@@ -9,6 +9,8 @@
import com.apptentive.android.sdk.network.HttpRequestMethod;
import com.apptentive.android.sdk.util.StringUtils;
+import static com.apptentive.android.sdk.ApptentiveLog.hideIfSanitized;
+
public class PayloadData {
private final PayloadType type;
private final String nonce;
@@ -65,7 +67,7 @@ public PayloadData(PayloadType type, String nonce, String conversationId, byte[]
@Override
public String toString() {
- return StringUtils.format("type=%s nonce=%s conversationId=%s authToken=%s httpRequestPath=%s", type, nonce, conversationId, authToken, httpRequestPath);
+ return StringUtils.format("type=%s nonce=%s conversationId=%s authToken=%s httpRequestPath=%s", type, nonce, conversationId, hideIfSanitized(authToken), httpRequestPath);
}
//endregion
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/PayloadType.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/PayloadType.java
index e9146daed..b098373fb 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/model/PayloadType.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/PayloadType.java
@@ -8,6 +8,8 @@
import com.apptentive.android.sdk.ApptentiveLog;
+import static com.apptentive.android.sdk.ApptentiveLogTag.PAYLOADS;
+
public enum PayloadType {
message,
event,
@@ -25,7 +27,7 @@ public static PayloadType parse(String type) {
try {
return PayloadType.valueOf(type);
} catch (IllegalArgumentException e) {
- ApptentiveLog.v("Error parsing unknown Payload.PayloadType: " + type);
+ ApptentiveLog.v(PAYLOADS, "Error parsing unknown Payload.PayloadType: " + type);
}
return unknown;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/PersonPayload.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/PersonPayload.java
index f1b0e7537..dfade04d6 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/model/PersonPayload.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/PersonPayload.java
@@ -6,12 +6,9 @@
package com.apptentive.android.sdk.model;
-import com.apptentive.android.sdk.ApptentiveLog;
-import com.apptentive.android.sdk.network.HttpRequestMethod;
import com.apptentive.android.sdk.util.StringUtils;
import org.json.JSONException;
-import org.json.JSONObject;
public class PersonPayload extends JsonPayload {
@@ -27,7 +24,11 @@ public class PersonPayload extends JsonPayload {
private static final String KEY_ZIP = "zip";
private static final String KEY_COUNTRY = "country";
private static final String KEY_BIRTHDAY = "birthday";
- public static final String KEY_CUSTOM_DATA = "custom_data";
+ @SensitiveDataKey private static final String KEY_CUSTOM_DATA = "custom_data";
+
+ static {
+ registerSensitiveKeys(PersonPayload.class);
+ }
public PersonPayload() {
super(PayloadType.person);
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/SdkAndAppReleasePayload.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/SdkAndAppReleasePayload.java
index fc1b7e110..085f28828 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/model/SdkAndAppReleasePayload.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/SdkAndAppReleasePayload.java
@@ -6,17 +6,9 @@
package com.apptentive.android.sdk.model;
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-
-import com.apptentive.android.sdk.ApptentiveInternal;
-import com.apptentive.android.sdk.ApptentiveLog;
-import com.apptentive.android.sdk.network.HttpRequestMethod;
import com.apptentive.android.sdk.util.StringUtils;
-import com.apptentive.android.sdk.util.Util;
+
+import static com.apptentive.android.sdk.ApptentiveLogTag.PAYLOADS;
/**
* A combined payload of {@link SdkPayload} and {@link AppReleasePayload} payloads.
@@ -191,45 +183,5 @@ public boolean getDebug() {
public void setDebug(boolean debug) {
put(KEY_DEBUG, debug);
}
-
- public static AppReleasePayload generateCurrentAppRelease(Context context) {
-
- AppReleasePayload appRelease = new AppReleasePayload();
-
- String appPackageName = context.getPackageName();
- PackageManager packageManager = context.getPackageManager();
-
- int currentVersionCode = 0;
- String currentVersionName = "0";
- int targetSdkVersion = 0;
- boolean isAppDebuggable = false;
- try {
- PackageInfo packageInfo = packageManager.getPackageInfo(appPackageName, PackageManager.GET_META_DATA | PackageManager.GET_RECEIVERS);
- ApplicationInfo ai = packageInfo.applicationInfo;
- currentVersionCode = packageInfo.versionCode;
- currentVersionName = packageInfo.versionName;
- targetSdkVersion = packageInfo.applicationInfo.targetSdkVersion;
- Bundle metaData = ai.metaData;
- if (metaData != null) {
- isAppDebuggable = (ai.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
- }
- } catch (PackageManager.NameNotFoundException e) {
- ApptentiveLog.e("Failed to read app's PackageInfo.");
- }
-
- int themeOverrideResId = context.getResources().getIdentifier("ApptentiveThemeOverride", "style", appPackageName);
-
- appRelease.setType("android");
- appRelease.setVersionName(currentVersionName);
- appRelease.setIdentifier(appPackageName);
- appRelease.setVersionCode(currentVersionCode);
- appRelease.setTargetSdkVersion(String.valueOf(targetSdkVersion));
- appRelease.setAppStore(Util.getInstallerPackageName(context));
- appRelease.setInheritStyle(ApptentiveInternal.getInstance().isAppUsingAppCompatTheme());
- appRelease.setOverrideStyle(themeOverrideResId != 0);
- appRelease.setDebug(isAppDebuggable);
-
- return appRelease;
- }
//endregion
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/SdkPayload.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/SdkPayload.java
index 303787a7b..f5dd0e06d 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/model/SdkPayload.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/SdkPayload.java
@@ -16,12 +16,16 @@ public class SdkPayload extends JsonPayload {
private static final String KEY_VERSION = "version";
private static final String KEY_PROGRAMMING_LANGUAGE = "programming_language";
- private static final String KEY_AUTHOR_NAME = "author_name";
- private static final String KEY_AUTHOR_EMAIL = "author_email";
+ @SensitiveDataKey private static final String KEY_AUTHOR_NAME = "author_name";
+ @SensitiveDataKey private static final String KEY_AUTHOR_EMAIL = "author_email";
private static final String KEY_PLATFORM = "platform";
private static final String KEY_DISTRIBUTION = "distribution";
private static final String KEY_DISTRIBUTION_VERSION = "distribution_version";
+ static {
+ registerSensitiveKeys(SdkPayload.class);
+ }
+
public SdkPayload() {
super(PayloadType.sdk);
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/SensitiveDataKey.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/SensitiveDataKey.java
new file mode 100644
index 000000000..c416015d0
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/SensitiveDataKey.java
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.model;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface SensitiveDataKey {
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/ActivityInteractionLauncher.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/ActivityInteractionLauncher.java
new file mode 100644
index 000000000..da33af45a
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/ActivityInteractionLauncher.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.module.engagement;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+
+import com.apptentive.android.sdk.ApptentiveInternal;
+import com.apptentive.android.sdk.ApptentiveViewActivity;
+import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction;
+import com.apptentive.android.sdk.util.Constants;
+
+class ActivityInteractionLauncher implements InteractionLauncher {
+ @Override
+ public boolean launch(Context context, Interaction interaction) {
+ Intent intent = new Intent();
+ intent.setClass(context.getApplicationContext(), ApptentiveViewActivity.class);
+ intent.putExtra(Constants.FragmentConfigKeys.TYPE, Constants.FragmentTypes.INTERACTION);
+ intent.putExtra(Interaction.KEY_NAME, interaction.toString());
+ /* non-activity context start an Activity, but it requires that a new task be created.
+ * This may fit specific use cases, but can create non-standard back stack behaviors in
+ * hosting application. non-activity context include application context, context from Service
+ * ContentProvider, and BroadcastReceiver
+ */
+ if (!(context instanceof Activity)) {
+ // check if any activity from the hosting app is running at foreground
+ Activity activity = ApptentiveInternal.getInstance().getCurrentTaskStackTopActivity();
+ if (activity != null) {
+ context = activity;
+ } else {
+ // If no foreground activity from the host app, launch Apptentive interaction as a new task
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+ }
+ }
+ context.startActivity(intent);
+
+ return true;
+ }
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/DefaultInteractionLauncherFactory.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/DefaultInteractionLauncherFactory.java
new file mode 100644
index 000000000..daafe3f74
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/DefaultInteractionLauncherFactory.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.module.engagement;
+
+import android.support.annotation.NonNull;
+
+import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction;
+import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction.DisplayType;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class DefaultInteractionLauncherFactory implements InteractionLauncherFactory {
+ private final Map launcherLookup;
+
+ DefaultInteractionLauncherFactory() {
+ launcherLookup = createLauncherLookup();
+ }
+
+ private Map createLauncherLookup() {
+ Map lookup = new HashMap<>();
+ lookup.put(DisplayType.notification, createNotificationInteractionLauncher());
+ // This is for maintaining existing behavior
+ lookup.put(DisplayType.unknown, createActivityInteractionLauncher());
+ return lookup;
+ }
+
+ @Override
+ public InteractionLauncher launcherForInteraction(Interaction interaction) {
+ return launcherLookup.get(interaction.getDisplayType());
+ }
+
+ // for Unit-tests
+
+ @NonNull
+ InteractionLauncher createActivityInteractionLauncher() {
+ return new ActivityInteractionLauncher();
+ }
+
+ @NonNull
+ InteractionLauncher createNotificationInteractionLauncher() {
+ return new NotificationInteractionLauncher();
+ }
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/EngagementModule.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/EngagementModule.java
index 7d2617d65..77846ff06 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/EngagementModule.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/EngagementModule.java
@@ -12,7 +12,6 @@
import com.apptentive.android.sdk.ApptentiveInternal;
import com.apptentive.android.sdk.ApptentiveLog;
-import com.apptentive.android.sdk.ApptentiveViewActivity;
import com.apptentive.android.sdk.conversation.Conversation;
import com.apptentive.android.sdk.debug.Assert;
import com.apptentive.android.sdk.model.EventPayload;
@@ -20,13 +19,13 @@
import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction;
import com.apptentive.android.sdk.module.engagement.interaction.model.MessageCenterInteraction;
import com.apptentive.android.sdk.module.metric.MetricModule;
-import com.apptentive.android.sdk.util.Constants;
import com.apptentive.android.sdk.util.Util;
import com.apptentive.android.sdk.util.threading.DispatchTask;
import java.util.Map;
import static com.apptentive.android.sdk.ApptentiveHelper.checkConversationQueue;
+import static com.apptentive.android.sdk.ApptentiveLogTag.*;
import static com.apptentive.android.sdk.util.threading.DispatchQueue.isMainQueue;
import static com.apptentive.android.sdk.util.threading.DispatchQueue.mainQueue;
@@ -35,6 +34,9 @@
*/
public class EngagementModule {
+ // this field gets overridden in unit tests (if renamed - update the test)
+ private static final InteractionLauncherFactory LAUNCHER_FACTORY = new DefaultInteractionLauncherFactory();
+
public static synchronized boolean engageInternal(Context context, Conversation conversation, String eventName) {
return engage(context, conversation, "com.apptentive", "app", null, eventName, null, null, (ExtendedData[]) null);
}
@@ -69,7 +71,7 @@ public static synchronized boolean engage(Context context, Conversation conversa
try {
String eventLabel = generateEventLabel(vendor, interaction, eventName);
- ApptentiveLog.d("engage(%s)", eventLabel);
+ ApptentiveLog.i(INTERACTIONS, "Engage event: '%s'", eventLabel);
String versionName = ApptentiveInternal.getInstance().getApplicationVersionName();
int versionCode = ApptentiveInternal.getInstance().getApplicationVersionCode();
@@ -77,7 +79,7 @@ public static synchronized boolean engage(Context context, Conversation conversa
conversation.addPayload(new EventPayload(eventLabel, interactionId, data, customData, extendedData));
return doEngage(conversation, context, eventLabel);
} catch (Exception e) {
- ApptentiveLog.w(e, "Error in engage()");
+ ApptentiveLog.e(INTERACTIONS, e, "Exception while engaging event '%s'", eventName);
MetricModule.sendError(e, null, null);
}
return false;
@@ -86,7 +88,7 @@ public static synchronized boolean engage(Context context, Conversation conversa
private static boolean doEngage(Conversation conversation, Context context, String eventLabel) {
checkConversationQueue();
- Interaction interaction = conversation.getApplicableInteraction(eventLabel);
+ Interaction interaction = conversation.getApplicableInteraction(eventLabel, true);
if (interaction != null) {
String versionName = ApptentiveInternal.getInstance().getApplicationVersionName();
int versionCode = ApptentiveInternal.getInstance().getApplicationVersionCode();
@@ -94,11 +96,21 @@ private static boolean doEngage(Conversation conversation, Context context, Stri
launchInteraction(context, interaction);
return true;
}
- ApptentiveLog.d("No interaction to show.");
+ ApptentiveLog.d(INTERACTIONS, "No interaction to show for event: '%s'", eventLabel);
return false;
}
public static void launchInteraction(final Context context, final Interaction interaction) {
+ if (context == null) {
+ ApptentiveLog.e("Unable to launch interaction: context is null"); // TODO: throw an exception instead?
+ return;
+ }
+
+ if (interaction == null) {
+ ApptentiveLog.e("Unable to launch interaction: interaction instance is null"); // TODO: throw an exception instead?
+ return;
+ }
+
if (!isMainQueue()) {
mainQueue().dispatchAsync(new DispatchTask() {
@Override
@@ -109,29 +121,17 @@ protected void execute() {
return;
}
- Context launchContext = context;
- if (interaction != null && launchContext != null) {
- ApptentiveLog.i("Launching interaction: %s", interaction.getType().toString());
- Intent intent = new Intent();
- intent.setClass(launchContext.getApplicationContext(), ApptentiveViewActivity.class);
- intent.putExtra(Constants.FragmentConfigKeys.TYPE, Constants.FragmentTypes.INTERACTION);
- intent.putExtra(Interaction.KEY_NAME, interaction.toString());
- /* non-activity context start an Activity, but it requires that a new task be created.
- * This may fit specific use cases, but can create non-standard back stack behaviors in
- * hosting application. non-activity context include application context, context from Service
- * ContentProvider, and BroadcastReceiver
- */
- if (!(launchContext instanceof Activity)) {
- // check if any activity from the hosting app is running at foreground
- Activity activity = ApptentiveInternal.getInstance().getCurrentTaskStackTopActivity();
- if (activity != null) {
- launchContext = activity;
- } else {
- // If no foreground activity from the host app, launch Apptentive interaction as a new task
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
- }
+ try {
+ ApptentiveLog.i(INTERACTIONS, "Launching interaction: '%s'", interaction.getType());
+ InteractionLauncher launcher = LAUNCHER_FACTORY.launcherForInteraction(interaction);
+ if (launcher != null) {
+ boolean launched = launcher.launch(context, interaction);
+ ApptentiveLog.d("Interaction %slaunched", launched ? "" : "NOT ");
+ } else {
+ ApptentiveLog.e("Interaction not launched: can't create launcher for interaction: %s", interaction);
}
- launchContext.startActivity(intent);
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while launching interaction: %s", interaction);
}
}
@@ -167,7 +167,7 @@ private static boolean canShowInteraction(Conversation conversation, String even
throw new IllegalArgumentException("Conversation is null");
}
- Interaction interaction = conversation.getApplicableInteraction(eventLabel);
+ Interaction interaction = conversation.getApplicableInteraction(eventLabel, false);
return interaction != null;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/InteractionLauncher.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/InteractionLauncher.java
new file mode 100644
index 000000000..9633f08b8
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/InteractionLauncher.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.module.engagement;
+
+import android.content.Context;
+
+import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction;
+
+public interface InteractionLauncher {
+ /**
+ * Returns true
if interaction was successfully launched
+ */
+ boolean launch(Context context, Interaction interaction);
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/InteractionLauncherFactory.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/InteractionLauncherFactory.java
new file mode 100644
index 000000000..ee845b3ef
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/InteractionLauncherFactory.java
@@ -0,0 +1,13 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.module.engagement;
+
+import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction;
+
+public interface InteractionLauncherFactory {
+ InteractionLauncher launcherForInteraction(Interaction interaction);
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/NotificationInteractionLauncher.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/NotificationInteractionLauncher.java
new file mode 100644
index 000000000..45377bf02
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/NotificationInteractionLauncher.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.module.engagement;
+
+import android.content.Context;
+import android.content.Intent;
+
+import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction;
+import com.apptentive.android.sdk.module.engagement.notification.ApptentiveNotificationInteractionBroadcastReceiver;
+import com.apptentive.android.sdk.util.NotificationUtils;
+
+import static com.apptentive.android.sdk.ApptentiveLogTag.NOTIFICATIONS;
+import static com.apptentive.android.sdk.util.Constants.NOTIFICATION_ACTION_DISPLAY;
+import static com.apptentive.android.sdk.util.Constants.NOTIFICATION_CHANNEL_DEFAULT;
+import static com.apptentive.android.sdk.util.Constants.NOTIFICATION_EXTRA_INTERACTION_DEFINITION;
+import static com.apptentive.android.sdk.util.Constants.NOTIFICATION_EXTRA_INTERACTION_TYPE;
+
+class NotificationInteractionLauncher implements InteractionLauncher {
+ @Override
+ public boolean launch(Context context, Interaction interaction) {
+ if (!NotificationUtils.isNotificationChannelEnabled(context, NOTIFICATION_CHANNEL_DEFAULT)) {
+ ApptentiveLog.e(NOTIFICATIONS, "Unable to engage notification interaction: notification channel is disabled");
+ return false;
+ }
+
+ final Intent launchIntent = new Intent(context, ApptentiveNotificationInteractionBroadcastReceiver.class);
+ launchIntent.setAction(NOTIFICATION_ACTION_DISPLAY);
+ launchIntent.putExtra(NOTIFICATION_EXTRA_INTERACTION_TYPE, interaction.getType().name());
+ launchIntent.putExtra(NOTIFICATION_EXTRA_INTERACTION_DEFINITION, interaction.toString());
+ context.sendBroadcast(launchIntent);
+ return true;
+ }
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/ApptentiveBaseFragment.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/ApptentiveBaseFragment.java
index 44991330b..977e307c0 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/ApptentiveBaseFragment.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/ApptentiveBaseFragment.java
@@ -59,7 +59,7 @@
public abstract class ApptentiveBaseFragment extends DialogFragment implements InteractionManager.InteractionUpdateListener {
- protected static final String EVENT_NAME_LAUNCH = "launch";
+ public static final String EVENT_NAME_LAUNCH = "launch";
private static final String HAS_LAUNCHED = "has_launched";
private final String fragmentName = getClass().getSimpleName();
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/MessageCenterFragment.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/MessageCenterFragment.java
index 7681f67a7..15daa380c 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/MessageCenterFragment.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/MessageCenterFragment.java
@@ -84,6 +84,7 @@
import static com.apptentive.android.sdk.ApptentiveHelper.dispatchConversationTask;
import static com.apptentive.android.sdk.debug.Assert.assertNotNull;
+import static com.apptentive.android.sdk.ApptentiveLogTag.MESSAGES;
import static com.apptentive.android.sdk.module.messagecenter.model.MessageCenterListItem.MESSAGE_COMPOSER;
import static com.apptentive.android.sdk.module.messagecenter.model.MessageCenterListItem.MESSAGE_CONTEXT;
import static com.apptentive.android.sdk.module.messagecenter.model.MessageCenterListItem.MESSAGE_OUTGOING;
@@ -303,7 +304,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
}
case Constants.REQUEST_CODE_PHOTO_FROM_SYSTEM_PICKER: {
if (data == null) {
- ApptentiveLog.d("no image is picked");
+ ApptentiveLog.d(MESSAGES, "no image is picked");
return;
}
imagePickerStillOpen = false;
@@ -756,22 +757,22 @@ public void removeImageFromComposer(final int position) {
public void openNonImageAttachment(final ImageItem image) {
if (image == null) {
- ApptentiveLog.d("No attachment argument.");
+ ApptentiveLog.d(MESSAGES, "No attachment argument.");
return;
}
try {
if (!Util.openFileAttachment(hostingActivityRef.get(), image.originalPath, image.localCachePath, image.mimeType)) {
- ApptentiveLog.d("Cannot open file attachment");
+ ApptentiveLog.d(MESSAGES, "Cannot open file attachment");
}
} catch (Exception e) {
- ApptentiveLog.e(e, "Error loading attachment");
+ ApptentiveLog.e(MESSAGES, e, "Error loading attachment");
}
}
public void showAttachmentDialog(final ImageItem image) {
if (image == null) {
- ApptentiveLog.d("No attachment argument.");
+ ApptentiveLog.d(MESSAGES, "No attachment argument.");
return;
}
@@ -790,7 +791,7 @@ public void showAttachmentDialog(final ImageItem image) {
dialog.show(ft, DIALOG_IMAGE_PREVIEW);
} catch (Exception e) {
- ApptentiveLog.e(e, "Error loading attachment preview.");
+ ApptentiveLog.e(MESSAGES, e, "Error loading attachment preview.");
}
}
@@ -1072,7 +1073,7 @@ public void onAttachImage() {
} catch (Exception e) {
e.printStackTrace();
imagePickerStillOpen = false;
- ApptentiveLog.d("can't launch image picker");
+ ApptentiveLog.w(MESSAGES, "can't launch image picker");
}
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/NoteFragment.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/NoteFragment.java
index 424a7b5bd..a59846fae 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/NoteFragment.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/NoteFragment.java
@@ -103,71 +103,71 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa
switch (buttonAction.getType()) {
case dismiss:
button.setOnClickListener(guarded(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- JSONObject data = new JSONObject();
- try {
- data.put(TextModalInteraction.EVENT_KEY_ACTION_ID, buttonAction.getId());
- data.put(Action.KEY_LABEL, buttonAction.getLabel());
- data.put(TextModalInteraction.EVENT_KEY_ACTION_POSITION, position);
- } catch (JSONException e) {
- ApptentiveLog.e(e, "Error creating Event data object.");
- }
- engageInternal(TextModalInteraction.EVENT_NAME_DISMISS, data.toString());
- transit();
- }
- }));
+ @Override
+ public void onClick(View view) {
+ JSONObject data = new JSONObject();
+ try {
+ data.put(TextModalInteraction.EVENT_KEY_ACTION_ID, buttonAction.getId());
+ data.put(Action.KEY_LABEL, buttonAction.getLabel());
+ data.put(TextModalInteraction.EVENT_KEY_ACTION_POSITION, position);
+ } catch (JSONException e) {
+ ApptentiveLog.e(e, "Error creating Event data object.");
+ }
+ engageInternal(TextModalInteraction.EVENT_NAME_DISMISS, data.toString());
+ transit();
+ }
+ }));
break;
case interaction:
button.setActivated(true);
button.setOnClickListener(guarded(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- LaunchInteractionAction launchInteractionButton = (LaunchInteractionAction) buttonAction;
- List invocations = launchInteractionButton.getInvocations();
- String interactionIdToLaunch = null;
- for (Invocation invocation : invocations) {
- FieldManager fieldManager = new FieldManager(getContext(), getConversation().getVersionHistory(), getConversation().getEventData(), getConversation().getPerson(), getConversation().getDevice(), getConversation().getAppRelease());
- if (invocation.isCriteriaMet(fieldManager)) {
- interactionIdToLaunch = invocation.getInteractionId();
- break;
- }
- }
-
- Interaction invokedInteraction = null;
- if (interactionIdToLaunch != null) {
- ConversationProxy conversation = getConversation();
- if (conversation != null) {
- String interactionsString = conversation.getInteractions();
- if (interactionsString != null) {
- try {
- Interactions interactions = new Interactions(interactionsString);
- invokedInteraction = interactions.getInteraction(interactionIdToLaunch);
- }catch (JSONException e) {
- // Should never happen.
- }
- }
- }
- }
-
- JSONObject data = new JSONObject();
- try {
- data.put(TextModalInteraction.EVENT_KEY_ACTION_ID, buttonAction.getId());
- data.put(Action.KEY_LABEL, buttonAction.getLabel());
- data.put(TextModalInteraction.EVENT_KEY_ACTION_POSITION, position);
- data.put(TextModalInteraction.EVENT_KEY_INVOKED_INTERACTION_ID, invokedInteraction == null ? JSONObject.NULL : invokedInteraction.getId());
- } catch (JSONException e) {
- ApptentiveLog.e(e, "Error creating Event data object.");
- }
-
- engageInternal(TextModalInteraction.EVENT_NAME_INTERACTION, data.toString());
- if (invokedInteraction != null) {
- EngagementModule.launchInteraction(getActivity(), invokedInteraction);
- }
- transit();
-
+ @Override
+ public void onClick(View view) {
+ LaunchInteractionAction launchInteractionButton = (LaunchInteractionAction) buttonAction;
+ List invocations = launchInteractionButton.getInvocations();
+ String interactionIdToLaunch = null;
+ for (Invocation invocation : invocations) {
+ FieldManager fieldManager = new FieldManager(getContext(), getConversation().getVersionHistory(), getConversation().getEventData(), getConversation().getPerson(), getConversation().getDevice(), getConversation().getAppRelease());
+ if (invocation.isCriteriaMet(fieldManager, false)) { // TODO: should we print details here as well?
+ interactionIdToLaunch = invocation.getInteractionId();
+ break;
+ }
+ }
+
+ Interaction invokedInteraction = null;
+ if (interactionIdToLaunch != null) {
+ ConversationProxy conversation = getConversation();
+ if (conversation != null) {
+ String interactionsString = conversation.getInteractions();
+ if (interactionsString != null) {
+ try {
+ Interactions interactions = new Interactions(interactionsString);
+ invokedInteraction = interactions.getInteraction(interactionIdToLaunch);
+ }catch (JSONException e) {
+ // Should never happen.
}
- }));
+ }
+ }
+ }
+
+ JSONObject data = new JSONObject();
+ try {
+ data.put(TextModalInteraction.EVENT_KEY_ACTION_ID, buttonAction.getId());
+ data.put(Action.KEY_LABEL, buttonAction.getLabel());
+ data.put(TextModalInteraction.EVENT_KEY_ACTION_POSITION, position);
+ data.put(TextModalInteraction.EVENT_KEY_INVOKED_INTERACTION_ID, invokedInteraction == null ? JSONObject.NULL : invokedInteraction.getId());
+ } catch (JSONException e) {
+ ApptentiveLog.e(e, "Error creating Event data object.");
+ }
+
+ engageInternal(TextModalInteraction.EVENT_NAME_INTERACTION, data.toString());
+ if (invokedInteraction != null) {
+ EngagementModule.launchInteraction(getActivity(), invokedInteraction);
+ }
+ transit();
+
+ }
+ }));
break;
}
bottomArea.addView(button);
@@ -186,4 +186,4 @@ public boolean onFragmentExit(ApptentiveViewExitType exitType) {
engageInternal(TextModalInteraction.EVENT_NAME_CANCEL, exitTypeToDataJson(exitType));
return false;
}
-}
+}
\ No newline at end of file
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java
index 54cb8f004..ae58898f9 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java
@@ -360,4 +360,4 @@ public int getToolbarNavigationIconResourceId(Resources.Theme activityTheme) {
public String getToolbarNavigationContentDescription() {
return getContext().getString(R.string.apptentive_survey_content_description_close_button);
}
-}
+}
\ No newline at end of file
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Interaction.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Interaction.java
index d5658f929..c4d4b32c5 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Interaction.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Interaction.java
@@ -13,12 +13,15 @@
import org.json.JSONException;
import org.json.JSONObject;
+import static com.apptentive.android.sdk.ApptentiveLogTag.INTERACTIONS;
+
public abstract class Interaction extends JSONObject {
public static final String KEY_NAME = "interaction";
public static final String KEY_ID = "id";
private static final String KEY_TYPE = "type";
+ public static final String KEY_DISPLAY_TYPE = "display_type";
private static final String KEY_VERSION = "version";
protected static final String KEY_CONFIGURATION = "configuration";
@@ -68,6 +71,22 @@ public Type getType() {
return Type.unknown;
}
+ public DisplayType getDisplayType() {
+ try {
+ if (isNull(KEY_DISPLAY_TYPE)) {
+ return getDefaultDisplayType();
+ }
+ return DisplayType.parse(getString(KEY_DISPLAY_TYPE));
+ } catch (JSONException e) {
+ // Ignore
+ }
+ return DisplayType.unknown;
+ }
+
+ protected DisplayType getDefaultDisplayType() {
+ return DisplayType.unknown;
+ }
+
public Integer getVersion() {
try {
if (!isNull(KEY_VERSION)) {
@@ -105,7 +124,21 @@ public static Type parse(String type) {
try {
return Type.valueOf(type);
} catch (IllegalArgumentException e) {
- ApptentiveLog.v("Error parsing unknown Interaction.Type: " + type);
+ ApptentiveLog.v(INTERACTIONS, "Error parsing unknown Interaction.Type: " + type);
+ }
+ return unknown;
+ }
+ }
+
+ public enum DisplayType {
+ notification,
+ unknown;
+
+ public static DisplayType parse(String type) {
+ try {
+ return DisplayType.valueOf(type);
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Error parsing interaction display_type: " + type);
}
return unknown;
}
@@ -143,7 +176,7 @@ public static Interaction parseInteraction(String interactionString) {
break;
}
} catch (JSONException e) {
- ApptentiveLog.w(e, "Error parsing Interaction");
+ ApptentiveLog.w(INTERACTIONS, e, "Error parsing Interaction");
// Ignore
}
return null;
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/InteractionCriteria.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/InteractionCriteria.java
index b9e5026bd..113fb6a4c 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/InteractionCriteria.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/InteractionCriteria.java
@@ -10,14 +10,17 @@
import com.apptentive.android.sdk.module.engagement.logic.Clause;
import com.apptentive.android.sdk.module.engagement.logic.ClauseParser;
import com.apptentive.android.sdk.module.engagement.logic.FieldManager;
+import com.apptentive.android.sdk.util.IndentBufferedPrinter;
+import com.apptentive.android.sdk.util.IndentPrinter;
import org.json.JSONException;
+import static com.apptentive.android.sdk.ApptentiveLogTag.*;
+
/**
* @author Sky Kelsey
*/
public class InteractionCriteria {
-
private String json;
public InteractionCriteria(String json) throws JSONException {
@@ -25,19 +28,28 @@ public InteractionCriteria(String json) throws JSONException {
}
public boolean isMet(FieldManager fieldManager) {
+ return isMet(fieldManager, true);
+ }
+
+ public boolean isMet(FieldManager fieldManager, boolean verbose) {
try {
Clause rootClause = ClauseParser.parse(json);
- ApptentiveLog.i("Evaluating Criteria");
boolean ret = false;
if (rootClause != null) {
- ret = rootClause.evaluate(fieldManager);
+ IndentPrinter printer = verbose ? new IndentBufferedPrinter() : IndentPrinter.NULL;
+ ret = rootClause.evaluate(fieldManager, printer);
+ if (verbose) {
+ ApptentiveLog.i(INTERACTIONS, "Criteria evaluated => %b", ret);
+ ApptentiveLog.d(INTERACTIONS, "Criteria evaluation details:\n%s", printer);
+ }
+ } else {
+ if (verbose) {
+ ApptentiveLog.i(INTERACTIONS, "Criteria could not be evaluated: no clause found");
+ }
}
- ApptentiveLog.i("- => %b", ret);
return ret;
- } catch (JSONException e) {
- ApptentiveLog.w(e, "Error parsing and running InteractionCriteria predicate logic.");
} catch (Exception e) {
- ApptentiveLog.w(e, "Error parsing and running InteractionCriteria predicate logic.");
+ ApptentiveLog.e(INTERACTIONS, e, "Exception while evaluating interaction criteria");
}
return false;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/InteractionManifest.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/InteractionManifest.java
index ba7fe5f68..090d5b3d9 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/InteractionManifest.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/InteractionManifest.java
@@ -12,6 +12,8 @@
import org.json.JSONException;
import org.json.JSONObject;
+import static com.apptentive.android.sdk.ApptentiveLogTag.INTERACTIONS;
+
public class InteractionManifest extends JSONObject {
public InteractionManifest(String json) throws JSONException {
@@ -42,7 +44,7 @@ public Interactions getInteractions() {
}
}
} catch (JSONException e) {
- ApptentiveLog.w(e, "Unable to load Interactions from InteractionManifest.");
+ ApptentiveLog.w(INTERACTIONS, e, "Unable to load Interactions from InteractionManifest.");
}
return null;
}
@@ -54,7 +56,7 @@ public Targets getTargets() {
return new Targets(targets.toString());
}
} catch (JSONException e) {
- ApptentiveLog.w(e, "Unable to load Targets from InteractionManifest.");
+ ApptentiveLog.w(INTERACTIONS, e, "Unable to load Targets from InteractionManifest.");
}
return null;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Interactions.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Interactions.java
index 1ca5ae471..664fb5409 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Interactions.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Interactions.java
@@ -15,6 +15,8 @@
import java.util.Iterator;
import java.util.List;
+import static com.apptentive.android.sdk.ApptentiveLogTag.INTERACTIONS;
+
/**
* A map of "interaction_id" => {Interaction}
*
@@ -36,7 +38,7 @@ public Interaction getInteraction(String id) {
return Interaction.Factory.parseInteraction(getJSONObject(id).toString());
}
} catch (JSONException e) {
- ApptentiveLog.w(e, "Exception parsing interactions array.");
+ ApptentiveLog.w(INTERACTIONS, e, "Exception parsing interactions array.");
}
return null;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Invocation.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Invocation.java
index 07ca043bf..8f9b33ee6 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Invocation.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Invocation.java
@@ -34,12 +34,12 @@ public String getInteractionId() {
return null;
}
- public boolean isCriteriaMet(FieldManager fieldManager) {
+ public boolean isCriteriaMet(FieldManager fieldManager, boolean verbose) {
try {
if (!isNull(KEY_CRITERIA)) {
JSONObject criteriaObject = getJSONObject(KEY_CRITERIA);
InteractionCriteria criteria = new InteractionCriteria(criteriaObject.toString());
- return criteria.isMet(fieldManager);
+ return criteria.isMet(fieldManager, verbose);
}
} catch (JSONException e) {
// Ignore
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Targets.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Targets.java
index 8e90bad79..3c340d641 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Targets.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/Targets.java
@@ -15,6 +15,8 @@
import org.json.JSONException;
import org.json.JSONObject;
+import static com.apptentive.android.sdk.ApptentiveLogTag.INTERACTIONS;
+
/**
* @author Sky Kelsey
*/
@@ -26,7 +28,7 @@ public Targets(String json) throws JSONException {
super(json);
}
- public String getApplicableInteraction(String eventLabel) {
+ public String getApplicableInteraction(String eventLabel, boolean verbose) {
JSONArray invocations = optJSONArray(eventLabel);
if (invocations != null) {
for (int i = 0; i < invocations.length(); i++) {
@@ -37,7 +39,7 @@ public String getApplicableInteraction(String eventLabel) {
Conversation conversation = ApptentiveInternal.getInstance().getConversation();
FieldManager fieldManager = new FieldManager(ApptentiveInternal.getInstance().getApplicationContext(), conversation.getVersionHistory(), conversation.getEventData(), conversation.getPerson(), conversation.getDevice(), conversation.getAppRelease());
- if (invocation.isCriteriaMet(fieldManager)) {
+ if (invocation.isCriteriaMet(fieldManager, verbose)) {
return invocation.getInteractionId();
}
} catch (JSONException e) {
@@ -46,7 +48,7 @@ public String getApplicableInteraction(String eventLabel) {
}
}
}
- ApptentiveLog.v("No runnable Interactions for EventLabel: %s", eventLabel);
+ ApptentiveLog.v(INTERACTIONS, "No runnable Interactions for EventLabel: %s", eventLabel);
return null;
}
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/common/Action.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/common/Action.java
index 4c213aba7..13f678eec 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/common/Action.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/common/Action.java
@@ -10,6 +10,8 @@
import org.json.JSONException;
import org.json.JSONObject;
+import static com.apptentive.android.sdk.ApptentiveLogTag.INTERACTIONS;
+
/**
* @author Sky Kelsey
*/
@@ -43,7 +45,7 @@ public static Type parse(String name) {
try {
return Type.valueOf(name);
} catch (IllegalArgumentException e) {
- ApptentiveLog.v("Error parsing unknown Action.Type: " + name);
+ ApptentiveLog.v(INTERACTIONS, "Error parsing unknown Action.Type: " + name);
}
return unknown;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/common/Actions.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/common/Actions.java
index 84bbcfe6d..02eff7034 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/common/Actions.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/common/Actions.java
@@ -14,6 +14,8 @@
import java.util.ArrayList;
import java.util.List;
+import static com.apptentive.android.sdk.ApptentiveLogTag.INTERACTIONS;
+
/**
* @author Sky Kelsey
*/
@@ -32,7 +34,7 @@ public List getAsList() {
}
}
} catch (JSONException e) {
- ApptentiveLog.w(e, "Exception parsing interactions array.");
+ ApptentiveLog.w(INTERACTIONS, e, "Exception parsing interactions array.");
}
return ret;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/survey/Question.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/survey/Question.java
index 77e254634..35a4511cb 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/survey/Question.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/survey/Question.java
@@ -8,6 +8,8 @@
import com.apptentive.android.sdk.ApptentiveLog;
+import static com.apptentive.android.sdk.ApptentiveLogTag.INTERACTIONS;
+
public interface Question {
int QUESTION_TYPE_SINGLELINE = 1;
int QUESTION_TYPE_MULTICHOICE = 2;
@@ -38,7 +40,7 @@ public static Type parse(String type) {
try {
return Type.valueOf(type);
} catch (IllegalArgumentException e) {
- ApptentiveLog.v("Error parsing unknown Question.Type: " + type);
+ ApptentiveLog.v(INTERACTIONS, "Error parsing unknown Question.Type: " + type);
}
return unknown;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/Clause.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/Clause.java
index b7fe5ea10..bd00dc5b5 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/Clause.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/Clause.java
@@ -6,7 +6,8 @@
package com.apptentive.android.sdk.module.engagement.logic;
-public interface Clause {
+import com.apptentive.android.sdk.util.IndentPrinter;
- boolean evaluate(FieldManager fieldManager);
+public interface Clause {
+ boolean evaluate(FieldManager fieldManager, IndentPrinter printer);
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/ClauseParser.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/ClauseParser.java
index 2ff5eef4c..3422a1802 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/ClauseParser.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/ClauseParser.java
@@ -15,6 +15,8 @@
import java.math.BigDecimal;
+import static com.apptentive.android.sdk.ApptentiveLogTag.INTERACTIONS;
+
/**
* @author Sky Kelsey
*/
@@ -22,14 +24,14 @@ public class ClauseParser {
private static final String KEY_COMPLEX_TYPE = "_type";
public static Clause parse(String json) throws JSONException {
- ApptentiveLog.v("+ Parsing Interaction Criteria.");
+ ApptentiveLog.v(INTERACTIONS, "+ Parsing Interaction Criteria.");
if (json == null) {
- ApptentiveLog.e("+ Interaction Criteria is null.");
+ ApptentiveLog.e(INTERACTIONS, "+ Interaction Criteria is null.");
return null;
}
JSONObject root = new JSONObject(json);
Clause ret = ClauseParser.parse(null, root);
- ApptentiveLog.v("+ Finished parsing Interaction Criteria.");
+ ApptentiveLog.v(INTERACTIONS, "+ Finished parsing Interaction Criteria.");
return ret;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/ConditionalClause.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/ConditionalClause.java
index e64098153..cc475a9c4 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/ConditionalClause.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/ConditionalClause.java
@@ -8,7 +8,7 @@
import com.apptentive.android.sdk.ApptentiveLog;
-import com.apptentive.android.sdk.util.Util;
+import com.apptentive.android.sdk.util.IndentPrinter;
import org.json.JSONObject;
@@ -16,6 +16,8 @@
import java.util.Iterator;
import java.util.List;
+import static com.apptentive.android.sdk.ApptentiveLogTag.INTERACTIONS;
+
/**
* @author Sky Kelsey
*/
@@ -23,17 +25,17 @@ public class ConditionalClause implements Clause {
private static final String KEY_COMPLEX_TYPE = "_type";
- String fieldName;
- List conditionalTests;
+ private final String fieldName;
+ private final List conditionalTests;
public ConditionalClause(String field, Object inputValue) {
this.fieldName = field.trim();
- conditionalTests = new ArrayList();
- ApptentiveLog.v(" + ConditionalClause for query: \"%s\"", fieldName);
+ ApptentiveLog.v(INTERACTIONS, " + ConditionalClause for query: \"%s\"", fieldName);
if (inputValue instanceof JSONObject && !isComplexType((JSONObject) inputValue)) {
conditionalTests = getConditions((JSONObject) inputValue);
} else {
+ conditionalTests = new ArrayList<>();
conditionalTests.add(new ConditionalTest(ConditionalOperator.$eq, ClauseParser.parseValue(inputValue)));
}
}
@@ -62,12 +64,12 @@ private boolean isComplexType(JSONObject jsonObject) {
* @return
*/
@Override
- public boolean evaluate(FieldManager fieldManager) {
- ApptentiveLog.v(" - %s", fieldName);
+ public boolean evaluate(FieldManager fieldManager, IndentPrinter printer) {
Comparable fieldValue = fieldManager.getValue(fieldName);
for (ConditionalTest test : conditionalTests) {
- ApptentiveLog.v(" - %s %s %s?", Util.classToString(fieldValue), test.operator, Util.classToString(test.parameter));
- if (!test.operator.apply(fieldValue, test.parameter)) {
+ boolean result = test.operator.apply(fieldValue, test.parameter);
+ printer.print("- %s => %b", test.operator.description(fieldManager.getDescription(fieldName), fieldValue, test.parameter), result);
+ if (!result) {
return false;
}
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/ConditionalOperator.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/ConditionalOperator.java
index 0c60233d4..cf846701d 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/ConditionalOperator.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/ConditionalOperator.java
@@ -7,7 +7,7 @@
package com.apptentive.android.sdk.module.engagement.logic;
import com.apptentive.android.sdk.Apptentive;
-import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.util.StringUtils;
import com.apptentive.android.sdk.util.Util;
import java.math.BigDecimal;
@@ -25,6 +25,11 @@ public boolean apply(Comparable first, Comparable second) {
boolean exists = first != null;
return exists == (Boolean) second;
}
+
+ @Override
+ public String description(String fieldName, Comparable first, Comparable second) {
+ return StringUtils.format("%s ('%s') exists", fieldName, first);
+ }
},
$ne {
@@ -41,6 +46,11 @@ public boolean apply(Comparable first, Comparable second) {
}
return first.compareTo(second) != 0;
}
+
+ @Override
+ public String description(String fieldName, Comparable first, Comparable second) {
+ return StringUtils.format("%s ('%s') not equal to '%s'", fieldName, first, second);
+ }
},
$eq {
@Override
@@ -59,6 +69,11 @@ public boolean apply(Comparable first, Comparable second) {
}
return first.compareTo(second) == 0;
}
+
+ @Override
+ public String description(String fieldName, Comparable first, Comparable second) {
+ return StringUtils.format("%s ('%s') equal to '%s'", fieldName, first, second);
+ }
},
$lt {
@@ -72,6 +87,11 @@ public boolean apply(Comparable first, Comparable second) {
}
return first.compareTo(second) < 0;
}
+
+ @Override
+ public String description(String fieldName, Comparable first, Comparable second) {
+ return StringUtils.format("%s (%s) less than %s", fieldName, first, second);
+ }
},
$lte {
@Override
@@ -84,6 +104,11 @@ public boolean apply(Comparable first, Comparable second) {
}
return first.compareTo(second) <= 0;
}
+
+ @Override
+ public String description(String fieldName, Comparable first, Comparable second) {
+ return StringUtils.format("%s ('%s') is less than or equal to '%s'", fieldName, first, second);
+ }
},
$gte {
@Override
@@ -96,6 +121,11 @@ public boolean apply(Comparable first, Comparable second) {
}
return first.compareTo(second) >= 0;
}
+
+ @Override
+ public String description(String fieldName, Comparable first, Comparable second) {
+ return StringUtils.format("%s ('%s') is greater than or equal to '%s'", fieldName, first, second);
+ }
},
$gt {
@Override
@@ -108,6 +138,11 @@ public boolean apply(Comparable first, Comparable second) {
}
return first.compareTo(second) > 0;
}
+
+ @Override
+ public String description(String fieldName, Comparable first, Comparable second) {
+ return StringUtils.format("%s ('%s') greater than '%s'", fieldName, first, second);
+ }
},
$contains {
@@ -121,6 +156,11 @@ public boolean apply(Comparable first, Comparable second) {
}
return ((String) first).toLowerCase().contains(((String) second).toLowerCase());
}
+
+ @Override
+ public String description(String fieldName, Comparable first, Comparable second) {
+ return StringUtils.format("%s ('%s') contains '%s'", fieldName, first, second);
+ }
},
$starts_with {
@Override
@@ -130,6 +170,11 @@ public boolean apply(Comparable first, Comparable second) {
}
return ((String) first).toLowerCase().startsWith(((String) second).toLowerCase());
}
+
+ @Override
+ public String description(String fieldName, Comparable first, Comparable second) {
+ return StringUtils.format("%s ('%s') starts with '%s'", fieldName, first, second);
+ }
},
$ends_with {
@Override
@@ -139,6 +184,11 @@ public boolean apply(Comparable first, Comparable second) {
}
return ((String) first).toLowerCase().endsWith(((String) second).toLowerCase());
}
+
+ @Override
+ public String description(String fieldName, Comparable first, Comparable second) {
+ return StringUtils.format("%s ('%s') ends with '%s'", fieldName, first, second);
+ }
},
$before {
@@ -154,9 +204,19 @@ public boolean apply(Comparable first, Comparable second) {
Double offset = ((BigDecimal) second).doubleValue();
Double currentTime = Util.currentTimeSeconds();
Apptentive.DateTime offsetDateTime = new Apptentive.DateTime(currentTime + offset);
- ApptentiveLog.v(" - %s?", Util.classToString(offsetDateTime));
return ((Apptentive.DateTime) first).compareTo(offsetDateTime) < 0;
}
+
+ @Override
+ public String description(String fieldName, Comparable first, Comparable second) {
+ if (!(second instanceof BigDecimal)) {
+ return StringUtils.format("%s ('%s') before date '%s'", fieldName, toPrettyDate(first), toPrettyDate(second));
+ }
+
+ Double offset = ((BigDecimal) second).doubleValue();
+ Double currentTime = Util.currentTimeSeconds();
+ return StringUtils.format("%s ('%s') before date '%s'", fieldName, toPrettyDate(first), toPrettyDate(currentTime + offset));
+ }
},
$after {
@Override
@@ -171,9 +231,19 @@ public boolean apply(Comparable first, Comparable second) {
Double offset = ((BigDecimal) second).doubleValue();
Double currentTime = Util.currentTimeSeconds();
Apptentive.DateTime offsetDateTime = new Apptentive.DateTime(currentTime + offset);
- ApptentiveLog.v(" - %s?", Util.classToString(offsetDateTime));
return ((Apptentive.DateTime) first).compareTo(offsetDateTime) > 0;
}
+
+ @Override
+ public String description(String fieldName, Comparable first, Comparable second) {
+ if (!(second instanceof BigDecimal)) {
+ return StringUtils.format("%s ('%s') after date '%s'", fieldName, toPrettyDate(first), toPrettyDate(second));
+ }
+
+ Double offset = ((BigDecimal) second).doubleValue();
+ Double currentTime = Util.currentTimeSeconds();
+ return StringUtils.format("%s ('%s') after date '%s'", fieldName, toPrettyDate(first), toPrettyDate(currentTime + offset));
+ }
},
unknown {
@@ -181,6 +251,12 @@ public boolean apply(Comparable first, Comparable second) {
public boolean apply(Comparable first, Comparable second) {
return false;
}
+
+
+ @Override
+ public String description(String fieldName, Comparable first, Comparable second) {
+ return StringUtils.format("Unknown field '%s'", fieldName);
+ }
};
public static ConditionalOperator parse(String name) {
@@ -195,4 +271,14 @@ public static ConditionalOperator parse(String name) {
}
public abstract boolean apply(Comparable first, Comparable second);
+ public abstract String description(String fieldName, Comparable first, Comparable second);
+
+ private static String toPrettyDate(Object value) {
+ if (value instanceof Apptentive.DateTime) {
+ Apptentive.DateTime date = (Apptentive.DateTime) value;
+ return StringUtils.toPrettyDate(date.getDateTime());
+ }
+
+ return StringUtils.toString(value);
+ }
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/ConditionalTest.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/ConditionalTest.java
index 47efcd872..2c12ae112 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/ConditionalTest.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/ConditionalTest.java
@@ -9,6 +9,8 @@
import com.apptentive.android.sdk.ApptentiveLog;
import com.apptentive.android.sdk.util.Util;
+import static com.apptentive.android.sdk.ApptentiveLogTag.INTERACTIONS;
+
/**
* @author Sky Kelsey
*/
@@ -18,7 +20,7 @@ public class ConditionalTest {
public Comparable parameter;
public ConditionalTest(ConditionalOperator operator, Object parameter) {
- ApptentiveLog.v(" + ConditionalTest: %s: %s", operator.name(), Util.classToString(parameter));
+ ApptentiveLog.v(INTERACTIONS, " + ConditionalTest: %s: %s", operator.name(), Util.classToString(parameter));
this.operator = operator;
if (parameter != null && !(parameter instanceof Comparable)) {
throw new IllegalArgumentException(String.format("Encountered non-Comparable parameter: %s", Util.classToString(parameter)));
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/FieldManager.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/FieldManager.java
index af3b74622..bd9f0e51a 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/FieldManager.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/FieldManager.java
@@ -10,6 +10,7 @@
import com.apptentive.android.sdk.Apptentive;
import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.ApptentiveLogTag;
import com.apptentive.android.sdk.debug.Assert;
import com.apptentive.android.sdk.storage.AppRelease;
import com.apptentive.android.sdk.storage.CustomData;
@@ -18,10 +19,14 @@
import com.apptentive.android.sdk.storage.Person;
import com.apptentive.android.sdk.storage.VersionHistory;
import com.apptentive.android.sdk.util.Constants;
+import com.apptentive.android.sdk.util.RuntimeUtils;
+import com.apptentive.android.sdk.util.StringUtils;
import com.apptentive.android.sdk.util.Util;
import java.math.BigDecimal;
+import static com.apptentive.android.sdk.ApptentiveLogTag.INTERACTIONS;
+
public class FieldManager {
Context context;
@@ -83,7 +88,7 @@ private Object doGetValue(String query) {
switch (sdkQuery) {
case version:
Apptentive.Version ret = new Apptentive.Version();
- ret.setVersion(Constants.APPTENTIVE_SDK_VERSION);
+ ret.setVersion(Constants.getApptentiveSdkVersion());
return ret;
}
}
@@ -107,9 +112,9 @@ private Object doGetValue(String query) {
case total:
return versionHistory.getTimeAtInstallTotal();
case version_code:
- return versionHistory.getTimeAtInstallForVersionCode(Util.getAppVersionCode(context));
+ return versionHistory.getTimeAtInstallForVersionCode(RuntimeUtils.getAppVersionCode(context));
case version_name:
- return versionHistory.getTimeAtInstallForVersionName(Util.getAppVersionName(context));
+ return versionHistory.getTimeAtInstallForVersionName(RuntimeUtils.getAppVersionName(context));
}
return new Apptentive.DateTime(Util.currentTimeSeconds());
}
@@ -123,10 +128,10 @@ private Object doGetValue(String query) {
case total: // Get total for all versions of the app.
return new BigDecimal(eventData.getInteractionCountTotal(interactionId));
case version_code:
- Integer appVersionCode = Util.getAppVersionCode(context);
+ Integer appVersionCode = RuntimeUtils.getAppVersionCode(context);
return new BigDecimal(eventData.getInteractionCountForVersionCode(interactionId, appVersionCode));
case version_name:
- String appVersionName = Util.getAppVersionName(context);
+ String appVersionName = RuntimeUtils.getAppVersionName(context);
return new BigDecimal(eventData.getInteractionCountForVersionName(interactionId, appVersionName));
default:
break;
@@ -158,10 +163,10 @@ private Object doGetValue(String query) {
case total: // Get total for all versions of the app.
return new BigDecimal(eventData.getEventCountTotal(eventLabel));
case version_code:
- Integer appVersionCode = Util.getAppVersionCode(context);
+ Integer appVersionCode = RuntimeUtils.getAppVersionCode(context);
return new BigDecimal(eventData.getEventCountForVersionCode(eventLabel, appVersionCode));
case version_name:
- String appVersionName = Util.getAppVersionName(context);
+ String appVersionName = RuntimeUtils.getAppVersionName(context);
return new BigDecimal(eventData.getEventCountForVersionName(eventLabel, appVersionName));
default:
break;
@@ -289,6 +294,207 @@ private Object doGetValue(String query) {
return null;
}
+ public String getDescription(String query) {
+
+ query = query.trim();
+ String[] tokens = query.split("/");
+ QueryPart topLevelQuery = QueryPart.parse(tokens[0]);
+
+ switch (topLevelQuery) {
+ case application: {
+ QueryPart applicationQuery = QueryPart.parse(tokens[1]);
+ switch (applicationQuery) {
+ case version_code: {
+ return "app version code";
+ }
+ case version_name: {
+ return "app version name";
+ }
+ case debug: {
+ return "app debuggable";
+ }
+ }
+ return null; // Default value
+ }
+ case sdk: {
+ QueryPart sdkQuery = QueryPart.parse(tokens[1]);
+ switch (sdkQuery) {
+ case version:
+ return "SDK version";
+ }
+ }
+ case current_time:
+ return "current time";
+ case is_update: {
+ QueryPart subQuery = QueryPart.parse(tokens[1]);
+ switch (subQuery) {
+ case version_code:
+ return "app version code changed";
+ case version_name:
+ return "app version name changed";
+ default:
+ break;
+ }
+ return null;
+ }
+ case time_at_install: {
+ QueryPart subQuery = QueryPart.parse(tokens[1]);
+ switch (subQuery) {
+ case total:
+ return "time at install";
+ case version_code:
+ return StringUtils.format("time at install for version code '%d'", RuntimeUtils.getAppVersionCode(context));
+ case version_name:
+ return StringUtils.format("time at install for version name '%s'", RuntimeUtils.getAppVersionName(context));
+ }
+ return null;
+ }
+ case interactions: {
+ String interactionId = tokens[1];
+ QueryPart queryPart1 = QueryPart.parse(tokens[2]);
+ switch (queryPart1) {
+ case invokes:
+ QueryPart queryPart2 = QueryPart.parse(tokens[3]);
+ switch (queryPart2) {
+ case total: // Get total for all versions of the app.
+ return StringUtils.format("number of invokes for interaction '%s'", interactionId);
+ case version_code:
+ int appVersionCode = RuntimeUtils.getAppVersionCode(context);
+ return StringUtils.format("number of invokes for interaction '%s' for version code '%d'", interactionId, appVersionCode);
+ case version_name:
+ String appVersionName = RuntimeUtils.getAppVersionName(context);
+ return StringUtils.format("number of invokes for interaction '%s' for version name '%s'", interactionId, appVersionName);
+ default:
+ break;
+ }
+ break;
+ case last_invoked_at:
+ QueryPart queryPart3 = QueryPart.parse(tokens[3]);
+ switch (queryPart3) {
+ case total:
+ return StringUtils.format("last time interaction '%s' was invoked", interactionId);
+ default:
+ break;
+ }
+ default:
+ break;
+ }
+ break;
+ }
+ case code_point: {
+ String eventLabel = tokens[1];
+ QueryPart queryPart1 = QueryPart.parse(tokens[2]);
+ switch (queryPart1) {
+ case invokes:
+ QueryPart queryPart2 = QueryPart.parse(tokens[3]);
+ switch (queryPart2) {
+ case total: // Get total for all versions of the app.
+ return StringUtils.format("number of invokes for event '%s'", eventLabel);
+ case version_code:
+ int appVersionCode = RuntimeUtils.getAppVersionCode(context);
+ return StringUtils.format("number of invokes for event '%s' for version code '%d'", eventLabel, appVersionCode);
+ case version_name:
+ String appVersionName = RuntimeUtils.getAppVersionName(context);
+ return StringUtils.format("number of invokes for event '%s' for version name '%s'", eventLabel, appVersionName);
+ default:
+ break;
+ }
+ break;
+ case last_invoked_at:
+ QueryPart queryPart3 = QueryPart.parse(tokens[3]);
+ switch (queryPart3) {
+ case total:
+ return StringUtils.format("last time event '%s' was invoked", eventLabel);
+ default:
+ break;
+ }
+ default:
+ break;
+ }
+ return null; // Default Value
+ }
+ case person: {
+ QueryPart subQuery = QueryPart.parse(tokens[1]);
+ if (person == null) {
+ return null;
+ }
+ switch (subQuery) {
+ case custom_data:
+ String customDataKey = tokens[2].trim();
+ return StringUtils.format("person_data['%s']", customDataKey);
+ case name:
+ return "person name";
+ case email:
+ return "person email";
+ case other:
+ return null;
+ }
+ }
+ case device: {
+ QueryPart subQuery = QueryPart.parse(tokens[1]);
+ if (device == null) {
+ return null;
+ }
+ switch (subQuery) {
+ case custom_data:
+ String customDataKey = tokens[2].trim();
+ return StringUtils.format("device_data['%s']", customDataKey);
+ case os_version:
+ return "device OS version";
+ case os_api_level:
+ return "device API level";
+ case board:
+ return "device board";
+ case bootloader_version:
+ return "device bootloader version";
+ case brand:
+ return "device brand";
+ case build_id:
+ return "device build id";
+ case build_type:
+ return "device build type";
+ case carrier:
+ return "device carrier";
+ case cpu:
+ return "device CPU";
+ case current_carrier:
+ return "device current carrier";
+ case device:
+ return "device";
+ case hardware:
+ return "device hardware";
+ case locale_country_code:
+ return "device country";
+ case locale_language_code:
+ return "device language";
+ case locale_raw:
+ return "device locale";
+ case manufacturer:
+ return "device manufacturer";
+ case model:
+ return "device model";
+ case network_type:
+ return "device network type";
+ case os_name:
+ return "device OS name";
+ case os_build:
+ return "device OS build";
+ case product:
+ return "device product";
+ case radio_version:
+ return "device radio version";
+ case uuid:
+ return "UUID";
+ case other:
+ return null;
+ }
+ }
+ default:
+ break;
+ }
+ return null;
+ }
+
private enum QueryPart {
application,
current_time,
@@ -345,7 +551,7 @@ public static QueryPart parse(String name) {
try {
return QueryPart.valueOf(name);
} catch (IllegalArgumentException e) {
- ApptentiveLog.d(e, "Unrecognized QueryPart: \"%s\". Defaulting to \"unknown\"", name);
+ ApptentiveLog.e(INTERACTIONS, "Unrecognized QueryPart: \"%s\". Defaulting to \"unknown\"", name);
}
}
return other;
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/LogicalClause.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/LogicalClause.java
index 3ad02b409..28af30bb8 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/LogicalClause.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/logic/LogicalClause.java
@@ -8,6 +8,7 @@
import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.util.IndentPrinter;
import org.json.JSONArray;
import org.json.JSONException;
@@ -17,20 +18,22 @@
import java.util.Iterator;
import java.util.List;
+import static com.apptentive.android.sdk.ApptentiveLogTag.INTERACTIONS;
+
/**
* @author Sky Kelsey
*/
public class LogicalClause implements Clause {
- protected String operatorName;
- protected LogicalOperator operator;
- protected List children;
+ private final String operatorName;
+ private final LogicalOperator operator;
+ private final List children;
protected LogicalClause(String key, Object value) throws JSONException {
operatorName = key.trim();
operator = LogicalOperator.parse(operatorName);
- children = new ArrayList();
- ApptentiveLog.v(" + LogicalClause of type \"%s\"", operatorName);
+ children = new ArrayList<>();
+ ApptentiveLog.v(INTERACTIONS, " + LogicalClause of type \"%s\"", operatorName);
if (value instanceof JSONArray) {
JSONArray jsonArray = (JSONArray) value;
for (int i = 0; i < jsonArray.length(); i++) {
@@ -49,51 +52,60 @@ protected LogicalClause(String key, Object value) throws JSONException {
}
}
} else {
- ApptentiveLog.w("Unrecognized LogicalClause: %s", value.toString());
+ ApptentiveLog.w(INTERACTIONS, "Unrecognized LogicalClause: %s", value.toString());
}
}
@Override
- public boolean evaluate(FieldManager fieldManager) {
- ApptentiveLog.v(" - <%s>", operator.name());
+ public boolean evaluate(FieldManager fieldManager, IndentPrinter printer) {
+ // to compress logs we 'inline' single $and operators
+ boolean printOperator = !LogicalOperator.$and.equals(operator) || children.size() > 1;
+ if (printOperator) {
+ printer.print("- %s:", operator.name());
+ printer.startBlock();
+ }
+ try {
+ return evaluateOperator(fieldManager, printer);
+ } finally {
+ if (printOperator) {
+ printer.endBlock();
+ }
+ }
+ }
+
+ private boolean evaluateOperator(FieldManager fieldManager, IndentPrinter printer) {
if (operator == LogicalOperator.$and) {
for (Clause clause : children) {
- boolean ret = clause.evaluate(fieldManager);
- ApptentiveLog.v(" - => %b", ret);
+ boolean ret = clause.evaluate(fieldManager, printer);
if (!ret) {
- ApptentiveLog.v(" - %s>", operator.name());
return false;
}
}
- ApptentiveLog.v(" - %s>", operator.name());
return true;
- } else if (operator == LogicalOperator.$or) {
+ }
+
+ if (operator == LogicalOperator.$or) {
for (Clause clause : children) {
- boolean ret = clause.evaluate(fieldManager);
- ApptentiveLog.v(" - => %b", ret);
+ boolean ret = clause.evaluate(fieldManager, printer);
if (ret) {
- ApptentiveLog.v(" - %s>", operator.name());
return true;
}
}
- ApptentiveLog.v(" - %s> => false", operator.name());
return false;
- } else if (operator == LogicalOperator.$not) {
+ }
+
+ if (operator == LogicalOperator.$not) {
if (children.size() != 1) {
throw new IllegalArgumentException("$not condition must have exactly one child, has ." + children.size());
}
Clause clause = children.get(0);
- boolean ret = clause.evaluate(fieldManager);
- ApptentiveLog.v(" - => %b", ret);
- ApptentiveLog.v(" - %s>", operator.name());
+ boolean ret = clause.evaluate(fieldManager, printer);
return !ret;
- } else {
- // Unsupported
- ApptentiveLog.v("Unsupported operation: \"%s\" => false", operatorName);
- ApptentiveLog.v(" - %s>", operator.name());
- return false;
}
+ // Unsupported
+ ApptentiveLog.v(INTERACTIONS, "Unsupported operation: \"%s\" => false", operatorName);
+ ApptentiveLog.v(INTERACTIONS, " - %s>", operator.name());
+ return false;
}
-
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/ApptentiveNotificationInteractionBroadcastReceiver.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/ApptentiveNotificationInteractionBroadcastReceiver.java
new file mode 100644
index 000000000..211a27405
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/ApptentiveNotificationInteractionBroadcastReceiver.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.module.engagement.notification;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.apptentive.android.sdk.ApptentiveLog;
+
+import static com.apptentive.android.sdk.ApptentiveLogTag.NOTIFICATION_INTERACTIONS;
+
+public class ApptentiveNotificationInteractionBroadcastReceiver extends BroadcastReceiver {
+
+ private static final InteractionNotificationBroadcastReceiverHandler DEFAULT_HANDLER = new DefaultInteractionNotificationBroadcastReceiverHandler();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ try {
+ DEFAULT_HANDLER.handleBroadcast(context, intent);
+ } catch (Exception e) {
+ ApptentiveLog.w(NOTIFICATION_INTERACTIONS, e, "Error handling Apptentive Interaction Notification broadcast.");
+ }
+ }
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/DefaultInteractionNotificationBroadcastReceiverHandler.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/DefaultInteractionNotificationBroadcastReceiverHandler.java
new file mode 100644
index 000000000..07d52d4dd
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/DefaultInteractionNotificationBroadcastReceiverHandler.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.module.engagement.notification;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+
+import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction;
+import com.apptentive.android.sdk.util.Constants;
+import com.apptentive.android.sdk.util.ContextUtils;
+
+import org.json.JSONException;
+
+import java.util.Iterator;
+
+import static com.apptentive.android.sdk.ApptentiveLogTag.NOTIFICATION_INTERACTIONS;
+import static com.apptentive.android.sdk.util.Constants.NOTIFICATION_CHANNEL_DEFAULT;
+import static com.apptentive.android.sdk.util.Constants.NOTIFICATION_EXTRA_INTERACTION_DEFINITION;
+
+public class DefaultInteractionNotificationBroadcastReceiverHandler implements InteractionNotificationBroadcastReceiverHandler {
+
+ private static final NoteInteractionNotificationAdapter DEFAULT_ADAPTER_NOTE = new NoteInteractionNotificationAdapter();
+
+ @Override
+ public void handleBroadcast(Context context, Intent intent) throws JSONException {
+ ApptentiveLog.d(NOTIFICATION_INTERACTIONS, "Received broadcast");
+ logIntent(intent);
+
+ // Set Notification Channel if supported by version
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ ContextUtils.getNotificationManager(context).createNotificationChannel(NotificationChannelHolder.getInstance());
+ }
+
+ // Get the interaction to display
+ Interaction.Type interactionType = Interaction.Type.parse(intent.getStringExtra(Constants.NOTIFICATION_EXTRA_INTERACTION_TYPE));
+ String interactionDefinition = intent.getStringExtra(NOTIFICATION_EXTRA_INTERACTION_DEFINITION);
+ if (interactionDefinition == null) {
+ ApptentiveLog.w("Interaction Notification Intent is missing extra %s", NOTIFICATION_EXTRA_INTERACTION_DEFINITION);
+ return;
+ }
+
+ InteractionNotificationAdapter interactionNotificationAdapter;
+ switch (interactionType) {
+ case TextModal:
+ interactionNotificationAdapter = DEFAULT_ADAPTER_NOTE;
+ break;
+ default:
+ ApptentiveLog.w("Attempted to launch Interaction as Notification, but that is not supported for the interaction type: %s", interactionDefinition);
+ return;
+ }
+ interactionNotificationAdapter.handleInteractionNotificationAction(context, NOTIFICATION_CHANNEL_DEFAULT, intent);
+ }
+
+ private void logIntent(Intent intent) {
+ if (ApptentiveLog.canLog(ApptentiveLog.Level.VERBOSE)) {
+ String action = intent.getAction();
+ ApptentiveLog.v(NOTIFICATION_INTERACTIONS, "Action: %s", action);
+ Bundle extras = intent.getExtras();
+ if (extras != null) {
+ Iterator extraKeys = extras.keySet().iterator();
+ ApptentiveLog.v(NOTIFICATION_INTERACTIONS, "Extras:");
+ while (extraKeys.hasNext()) {
+ String key = extraKeys.next();
+ ApptentiveLog.v(NOTIFICATION_INTERACTIONS, " \"%s\" = \"%s\"", key, String.valueOf(extras.get(key)));
+ }
+ }
+ }
+ }
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/InteractionNotificationAdapter.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/InteractionNotificationAdapter.java
new file mode 100644
index 000000000..4a302e6dc
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/InteractionNotificationAdapter.java
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.module.engagement.notification;
+
+import android.content.Context;
+import android.content.Intent;
+
+public interface InteractionNotificationAdapter {
+ void handleInteractionNotificationAction(Context context, String channelId, Intent intent);
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/InteractionNotificationBroadcastReceiverHandler.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/InteractionNotificationBroadcastReceiverHandler.java
new file mode 100644
index 000000000..cfda08286
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/InteractionNotificationBroadcastReceiverHandler.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.module.engagement.notification;
+
+import android.content.Context;
+import android.content.Intent;
+
+import org.json.JSONException;
+
+public interface InteractionNotificationBroadcastReceiverHandler {
+ void handleBroadcast(Context context, Intent intent) throws JSONException;
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/NoteInteractionNotificationAdapter.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/NoteInteractionNotificationAdapter.java
new file mode 100644
index 000000000..d49afa8de
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/NoteInteractionNotificationAdapter.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.module.engagement.notification;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.support.v4.app.NotificationCompat;
+import android.util.TypedValue;
+
+import com.apptentive.android.sdk.ApptentiveHelper;
+import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.R;
+import com.apptentive.android.sdk.conversation.Conversation;
+import com.apptentive.android.sdk.conversation.ConversationDispatchTask;
+import com.apptentive.android.sdk.module.engagement.EngagementModule;
+import com.apptentive.android.sdk.module.engagement.interaction.fragment.ApptentiveBaseFragment;
+import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction;
+import com.apptentive.android.sdk.module.engagement.interaction.model.Interactions;
+import com.apptentive.android.sdk.module.engagement.interaction.model.Invocation;
+import com.apptentive.android.sdk.module.engagement.interaction.model.TextModalInteraction;
+import com.apptentive.android.sdk.module.engagement.interaction.model.common.Action;
+import com.apptentive.android.sdk.module.engagement.interaction.model.common.Actions;
+import com.apptentive.android.sdk.module.engagement.interaction.model.common.LaunchInteractionAction;
+import com.apptentive.android.sdk.module.engagement.logic.FieldManager;
+import com.apptentive.android.sdk.util.Constants;
+import com.apptentive.android.sdk.util.StringUtils;
+import com.apptentive.android.sdk.util.Util;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.List;
+import java.util.Random;
+
+import static android.content.Context.NOTIFICATION_SERVICE;
+import static com.apptentive.android.sdk.ApptentiveHelper.dispatchConversationTask;
+import static com.apptentive.android.sdk.ApptentiveLogTag.NOTIFICATION_INTERACTIONS;
+import static com.apptentive.android.sdk.util.Constants.NOTIFICATION_ACTION_DELETE;
+import static com.apptentive.android.sdk.util.Constants.NOTIFICATION_ACTION_DISPLAY;
+import static com.apptentive.android.sdk.util.Constants.NOTIFICATION_ACTION_NOTE_BUTTON_PRESSED;
+import static com.apptentive.android.sdk.util.Constants.NOTIFICATION_EXTRA_ID;
+import static com.apptentive.android.sdk.util.Constants.NOTIFICATION_EXTRA_INTERACTION_DEFINITION;
+import static com.apptentive.android.sdk.util.Constants.NOTIFICATION_EXTRA_INTERACTION_TYPE;
+import static com.apptentive.android.sdk.util.Constants.NOTIFICATION_EXTRA_NOTE_ACTION_INDEX;
+import static com.apptentive.android.sdk.util.Constants.NOTIFICATION_ID_DEFAULT;
+
+public class NoteInteractionNotificationAdapter implements InteractionNotificationAdapter {
+
+ @Override
+ public void handleInteractionNotificationAction(Context context, String channelId, Intent intent) {
+
+ String action = intent.getAction();
+ String interactionString = intent.getStringExtra(NOTIFICATION_EXTRA_INTERACTION_DEFINITION);
+ TextModalInteraction interaction;
+ try {
+ interaction = new TextModalInteraction(intent.getStringExtra(NOTIFICATION_EXTRA_INTERACTION_DEFINITION));
+ } catch (JSONException e) {
+ ApptentiveLog.w(NOTIFICATION_INTERACTIONS, "Unable to parse interaction: %s", interactionString);
+ return;
+ }
+ if (StringUtils.equal(action, NOTIFICATION_ACTION_DISPLAY)) {
+ actionDisplayNotification(context, channelId, interaction);
+ } else if (StringUtils.equal(action, NOTIFICATION_ACTION_DELETE)) {
+ actionDelete(context, interaction);
+ } else if (StringUtils.equal(action, NOTIFICATION_ACTION_NOTE_BUTTON_PRESSED)) {
+ actionButtonPressed(context, intent, interaction);
+ } else {
+ ApptentiveLog.w(NOTIFICATION_INTERACTIONS, "Unsupported action %s for Interaction type %s", action, interaction.getType().name());
+ }
+ }
+
+ protected void actionDisplayNotification(final Context context, final String channelId, final TextModalInteraction interaction) {
+
+ // Build notification
+ final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId);
+
+ builder.setVibrate(new long[]{0, 100, 100, 100, 100, 100})
+ .setOnlyAlertOnce(true)
+ .setSmallIcon(R.drawable.apptentive_ic_stat_chat_bubble) // Set as default. Overridden below.
+ .setWhen(System.currentTimeMillis());
+
+ // Set up the Intent that is triggered if the user swipes or clears the Notification.
+ Intent deleteIntent = new Intent(context, ApptentiveNotificationInteractionBroadcastReceiver.class);
+ deleteIntent.putExtra(NOTIFICATION_EXTRA_ID, NOTIFICATION_ID_DEFAULT);
+ deleteIntent.putExtra(NOTIFICATION_EXTRA_INTERACTION_TYPE, interaction.getType().name());
+ deleteIntent.putExtra(NOTIFICATION_EXTRA_INTERACTION_DEFINITION, interaction.toString());
+ deleteIntent.setAction(NOTIFICATION_ACTION_DELETE);
+ PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, new Random().nextInt(), deleteIntent, PendingIntent.FLAG_ONE_SHOT);
+ builder.setDeleteIntent(deletePendingIntent);
+
+ // Set up the text content of the Notification.
+ NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle();
+ if (!StringUtils.isNullOrEmpty(interaction.getTitle())) {
+ builder.setContentTitle(interaction.getTitle());
+ bigTextStyle.setBigContentTitle(interaction.getTitle());
+ }
+ if (!StringUtils.isNullOrEmpty(interaction.getBody())) {
+ builder.setContentText(interaction.getBody());
+ bigTextStyle.bigText(interaction.getBody());
+ }
+ builder.setStyle(bigTextStyle);
+
+ // Set up the action buttons, if any are specified. Up to 3 are allowed.
+ Actions actions = interaction.getActions();
+ if (actions != null) {
+ List actionsList = actions.getAsList();
+ for (int i = 0; i < actionsList.size(); i++) {
+ Action action = actionsList.get(i);
+
+ if (i > 3) {
+ ApptentiveLog.d(NOTIFICATION_INTERACTIONS, "Can't have more than 3 buttons on a Note.");
+ return;
+ }
+ Action.Type actionType = action.getType();
+
+ Intent intent = new Intent(context, ApptentiveNotificationInteractionBroadcastReceiver.class);
+ intent.putExtra(NOTIFICATION_EXTRA_ID, NOTIFICATION_ID_DEFAULT);
+ intent.putExtra(NOTIFICATION_EXTRA_INTERACTION_TYPE, interaction.getType().name());
+ intent.putExtra(NOTIFICATION_EXTRA_INTERACTION_DEFINITION, interaction.toString());
+ intent.putExtra(NOTIFICATION_EXTRA_NOTE_ACTION_INDEX, i);
+ switch (actionType) {
+ // Don't worry about what each button does now, let the adapter make that choice when it does get pressed.
+ case interaction:
+ case dismiss:
+ intent.setAction(NOTIFICATION_ACTION_NOTE_BUTTON_PRESSED);
+ break;
+ case unknown:
+ return;
+ }
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(context, new Random().nextInt(), intent, PendingIntent.FLAG_ONE_SHOT);
+ builder.addAction(new NotificationCompat.Action.Builder(0, action.getLabel(), pendingIntent).build());
+ }
+ }
+
+ // Set styles on the Notification pulled from the app theme.
+ Resources.Theme theme = Util.buildApptentiveInteractionTheme(context);
+ if (theme != null) {
+ TypedValue icon = new TypedValue();
+ if (theme.resolveAttribute(R.attr.apptentiveInteractionNotificationSmallIcon, icon, true)) {
+ builder.setSmallIcon(icon.resourceId);
+ } else {
+ ApptentiveLog.d(NOTIFICATION_INTERACTIONS, "Unable to find icon in theme for setting Notification icon.");
+ }
+ TypedValue color = new TypedValue();
+ if (theme.resolveAttribute(R.attr.apptentiveInteractionNotificationColor, color, true)) {
+ builder.setColor(color.data);
+ } else {
+ ApptentiveLog.d(NOTIFICATION_INTERACTIONS, "Unable to find color in theme for setting Notification icon.");
+ }
+ } else {
+ ApptentiveLog.d(NOTIFICATION_INTERACTIONS, "Unable to build theme for getting Notification icon.");
+ }
+
+ Notification notification = builder.build();
+ ((NotificationManager) context.getSystemService(NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID_DEFAULT, notification);
+
+ // Sending an internal event here for tracking Interaction state transitions on the server. These events sent in this class are designed to duplicate the behavior of the existing Activity based display_type for Notes.
+ final JSONObject data = new JSONObject();
+ try {
+ data.put(TextModalInteraction.KEY_DISPLAY_TYPE, interaction.getDisplayType().name());
+ } catch (JSONException e) {
+ ApptentiveLog.e(NOTIFICATION_INTERACTIONS, e, "Error creating Event data object.");
+ }
+ dispatchConversationTask(new ConversationDispatchTask() {
+ @Override
+ protected boolean execute(Conversation conversation) {
+ return EngagementModule.engageInternal(context, conversation, interaction, ApptentiveBaseFragment.EVENT_NAME_LAUNCH, data.toString());
+ }
+ }, "engage Note Notification launch");
+ }
+
+ protected void actionButtonPressed(final Context context, final Intent incomingIntent, final TextModalInteraction interaction) {
+
+ int notificationId = incomingIntent.getIntExtra(Constants.NOTIFICATION_EXTRA_ID, NOTIFICATION_ID_DEFAULT);
+ final int index = incomingIntent.getIntExtra(Constants.NOTIFICATION_EXTRA_NOTE_ACTION_INDEX, Integer.MIN_VALUE);
+
+ // Perform the action specified for this button.
+ List actions = interaction.getActions().getAsList();
+ final Action action = actions.get(index);
+ Action.Type actionType = action.getType();
+ ApptentiveLog.v(NOTIFICATION_INTERACTIONS, "Note Notification button pressed with index %d and action type %s", index, actionType.name());
+ switch (actionType) {
+ case interaction: {
+ // First, make sure the Notification Drawer is dismissed.
+ context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+
+ // Dispatch a task to handle the rest, since it requires a Conversation.
+ ApptentiveHelper.dispatchConversationTask(new ConversationDispatchTask() {
+ @Override
+ protected boolean execute(Conversation conversation) {
+ // Retrieve the Interaction this button is supposed to launch
+ LaunchInteractionAction launchInteractionAction = (LaunchInteractionAction) action;
+ List invocations = launchInteractionAction.getInvocations();
+ String interactionIdToLaunch = null;
+
+ // Need to check each Invocation object's criteria to find the right one.
+ for (Invocation invocation : invocations) {
+ FieldManager fieldManager = new FieldManager(context, conversation.getVersionHistory(), conversation.getEventData(), conversation.getPerson(), conversation.getDevice(), conversation.getAppRelease());
+ if (invocation.isCriteriaMet(fieldManager, true)) {
+ interactionIdToLaunch = invocation.getInteractionId();
+ ApptentiveLog.v(NOTIFICATION_INTERACTIONS, "Found an Interaction to launch with id %s", interactionIdToLaunch);
+ break;
+ }
+ }
+
+ // If an Interaction can be launched, fetch its definition.
+ Interaction invokedInteraction = null;
+ if (interactionIdToLaunch != null) {
+ String interactionsString = conversation.getInteractions();
+ if (interactionsString != null) {
+ try {
+ Interactions interactions = new Interactions(interactionsString);
+ invokedInteraction = interactions.getInteraction(interactionIdToLaunch);
+ } catch (JSONException e) {
+ // Should never happen.
+ }
+ }
+ }
+
+ // Send the tracking event, now that we have all the necessary information.
+ final JSONObject data = new JSONObject();
+ try {
+ data.put(TextModalInteraction.EVENT_KEY_ACTION_ID, action.getId());
+ data.put(Action.KEY_LABEL, action.getLabel());
+ data.put(TextModalInteraction.EVENT_KEY_ACTION_POSITION, index);
+ data.put(TextModalInteraction.EVENT_KEY_INVOKED_INTERACTION_ID, invokedInteraction == null ? JSONObject.NULL : invokedInteraction.getId());
+ data.put(TextModalInteraction.KEY_DISPLAY_TYPE, interaction.getDisplayType().name());
+ } catch (JSONException e) {
+ ApptentiveLog.e(NOTIFICATION_INTERACTIONS, e, "Error creating Event data object.");
+ }
+ EngagementModule.engageInternal(context, conversation, interaction, TextModalInteraction.EVENT_NAME_INTERACTION, data.toString());
+
+ // Finally, launch the interaction, if there is one
+ if (invokedInteraction != null) {
+ ApptentiveLog.d(NOTIFICATION_INTERACTIONS, "Launching interaction from Note Notification action: %s", interactionIdToLaunch);
+ EngagementModule.launchInteraction(context, invokedInteraction);
+ } else {
+ ApptentiveLog.w(NOTIFICATION_INTERACTIONS, "No Interaction was found to display matching id %s", interactionIdToLaunch);
+ }
+ return false;
+ }
+ }, "choosing and launching Interaction from Note Notification Action");
+
+
+ break;
+ }
+ case dismiss:
+ // Just send the tracking event
+ final JSONObject data = new JSONObject();
+ try {
+ data.put(TextModalInteraction.EVENT_KEY_ACTION_ID, action.getId());
+ data.put(Action.KEY_LABEL, action.getLabel());
+ data.put(TextModalInteraction.EVENT_KEY_ACTION_POSITION, index);
+ data.put(TextModalInteraction.KEY_DISPLAY_TYPE, interaction.getDisplayType().name());
+ } catch (JSONException e) {
+ ApptentiveLog.e(NOTIFICATION_INTERACTIONS, e, "Error creating Event data object.");
+ }
+ dispatchConversationTask(new ConversationDispatchTask() {
+ @Override
+ protected boolean execute(Conversation conversation) {
+ return EngagementModule.engageInternal(context, conversation, interaction, TextModalInteraction.EVENT_NAME_DISMISS, data.toString());
+ }
+ }, "engage Note Notification dismiss");
+
+ break;
+ case unknown:
+ ApptentiveLog.w(NOTIFICATION_INTERACTIONS, "Unknown Note Interaction Notification button action. Can't do anything.");
+ break;
+ }
+
+ // Remove the Notification
+ ((NotificationManager) context.getSystemService(NOTIFICATION_SERVICE)).cancel(notificationId);
+ }
+
+ protected void actionDelete(final Context context, final TextModalInteraction interaction) {
+ ApptentiveLog.v(NOTIFICATION_INTERACTIONS, "Delete intent received.");
+ final JSONObject data = new JSONObject();
+ try {
+ data.put(TextModalInteraction.KEY_DISPLAY_TYPE, interaction.getDisplayType().name());
+ } catch (JSONException e) {
+ ApptentiveLog.e(NOTIFICATION_INTERACTIONS, e, "Error creating Event data object.");
+ }
+ dispatchConversationTask(new ConversationDispatchTask() {
+ @Override
+ protected boolean execute(Conversation conversation) {
+ return EngagementModule.engageInternal(context, conversation, interaction, TextModalInteraction.EVENT_NAME_CANCEL, data.toString());
+ }
+ }, "engage Note Notification cancel");
+ }
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/NotificationChannelHolder.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/NotificationChannelHolder.java
new file mode 100644
index 000000000..1b05a5e01
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/notification/NotificationChannelHolder.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.module.engagement.notification;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.graphics.Color;
+import android.support.annotation.RequiresApi;
+
+import static com.apptentive.android.sdk.util.Constants.NOTIFICATION_CHANNEL_DEFAULT;
+
+@RequiresApi(26)
+public class NotificationChannelHolder {
+
+ private static NotificationChannel instance;
+
+ static {
+ NotificationChannel newInstance = new NotificationChannel(NOTIFICATION_CHANNEL_DEFAULT, "Apptentive Notifications", NotificationManager.IMPORTANCE_DEFAULT);
+ newInstance.setDescription("Channel description");
+ newInstance.enableLights(true);
+ newInstance.setLightColor(Color.RED);
+ newInstance.enableVibration(true);
+ instance = newInstance;
+ }
+
+ public static NotificationChannel getInstance() {
+ return instance;
+ }
+}
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 0539df65a..1276bf2e8 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
@@ -22,6 +22,9 @@
import static com.apptentive.android.sdk.ApptentiveHelper.dispatchOnConversationQueueOnce;
import static com.apptentive.android.sdk.ApptentiveLogTag.MESSAGES;
+import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_KEY_INTERVAL;
+import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_MESSAGES_STOPPED_POLLING;
+import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_MESSAGES_STARTED_POLLING;
import static com.apptentive.android.sdk.debug.Assert.assertTrue;
class MessagePollingWorker implements Destroyable, MessageManager.MessageFetchListener {
@@ -44,7 +47,7 @@ class MessagePollingWorker implements Destroyable, MessageManager.MessageFetchLi
conf = Configuration.load();
backgroundPollingInterval = conf.getMessageCenterBgPoll() * 1000;
foregroundPollingInterval = conf.getMessageCenterFgPoll() * 1000;
- ApptentiveLog.vv("Message Polling Worker: bg=%d, fg=%d", backgroundPollingInterval, foregroundPollingInterval);
+ ApptentiveLog.v(MESSAGES, "Message Polling Worker: bg=%d, fg=%d", backgroundPollingInterval, foregroundPollingInterval);
}
@Override
@@ -57,7 +60,7 @@ public void destroy() {
@Override
public void onFetchFinish(MessageManager manager, List messages) {
if (isPolling()) {
- long pollingInterval = messageCenterInForeground ? foregroundPollingInterval : backgroundPollingInterval;
+ long pollingInterval = getPollingInterval();
ApptentiveLog.v(MESSAGES, "Scheduled polling messages in %d sec", pollingInterval / 1000);
dispatchOnConversationQueueOnce(messagePollingTask, pollingInterval);
}
@@ -83,16 +86,24 @@ void appWentToBackground() {
void setMessageCenterInForeground(boolean foreground) {
messageCenterInForeground = foreground;
if (foreground) {
- startPolling();
+ startPolling(true);
}
}
void startPolling() {
+ startPolling(false);
+ }
+
+ private void startPolling(boolean force) {
+ if (force) {
+ stopPolling();
+ }
+
if (!isPolling()) {
ApptentiveLog.v(MESSAGES, "Start polling messages (%s)", getLocalConversationIdentifier());
messagePollingTask = createPollingTask();
dispatchOnConversationQueueOnce(messagePollingTask, 0L);
- notifyStartPolling();
+ notifyStartPolling(getPollingInterval());
}
}
@@ -105,14 +116,20 @@ void stopPolling() {
}
}
+ private long getPollingInterval() {
+ return messageCenterInForeground ? foregroundPollingInterval : backgroundPollingInterval;
+ }
+
//region Notifications
- private void notifyStartPolling() {
- // TBD
+ private void notifyStartPolling(long interval) {
+ ApptentiveNotificationCenter.defaultCenter()
+ .postNotification(NOTIFICATION_MESSAGES_STARTED_POLLING,
+ NOTIFICATION_KEY_INTERVAL, interval);
}
private void notifyStopPolling() {
- // TBD
+ ApptentiveNotificationCenter.defaultCenter().postNotification(NOTIFICATION_MESSAGES_STOPPED_POLLING);
}
//endregion
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/model/MessageFactory.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/model/MessageFactory.java
index 8d998f47d..52a647546 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/model/MessageFactory.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/model/MessageFactory.java
@@ -14,6 +14,8 @@
import org.json.JSONException;
import org.json.JSONObject;
+import static com.apptentive.android.sdk.ApptentiveLogTag.MESSAGES;
+
public class MessageFactory {
public static ApptentiveMessage fromJson(String json) {
@@ -36,7 +38,7 @@ public static ApptentiveMessage fromJson(String json) {
break;
}
} catch (JSONException e) {
- ApptentiveLog.v(e, "Error parsing json as Message: %s", json);
+ ApptentiveLog.v(MESSAGES, e, "Error parsing json as Message: %s", json);
} catch (IllegalArgumentException e) {
// Exception treated as unknown type
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/view/ApptentiveAvatarView.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/view/ApptentiveAvatarView.java
index 869203800..5cb21fb0b 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/view/ApptentiveAvatarView.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/view/ApptentiveAvatarView.java
@@ -18,12 +18,16 @@
import android.widget.ImageView;
import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.ApptentiveLogTag;
import com.apptentive.android.sdk.R;
import com.apptentive.android.sdk.module.metric.MetricModule;
import java.io.IOException;
import java.net.URL;
+import static com.apptentive.android.sdk.ApptentiveLogTag.MESSAGES;
+import static com.apptentive.android.sdk.ApptentiveLogTag.UTIL;
+
/**
* @author Sky Kelsey
@@ -150,7 +154,7 @@ private Bitmap getBitmapFromDrawable(Drawable d) {
d.draw(canvas);
return b;
} catch (OutOfMemoryError e) {
- ApptentiveLog.w(e, "Error creating bitmap.");
+ ApptentiveLog.w(UTIL, e, "Error creating bitmap.");
return null;
}
}
@@ -222,7 +226,7 @@ public void run() {
URL url = new URL(urlString);
bitmap = BitmapFactory.decodeStream(url.openStream());
} catch (IOException e) {
- ApptentiveLog.d(e, "Error opening avatar from URL: \"%s\"", urlString);
+ ApptentiveLog.e(UTIL, e, "Error opening avatar from URL: \"%s\"", urlString);
}
if (bitmap != null) {
final Bitmap finalBitmap = bitmap;
@@ -237,7 +241,7 @@ public void run() {
Thread.UncaughtExceptionHandler handler = new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
- ApptentiveLog.w(throwable, "UncaughtException in AvatarView.");
+ ApptentiveLog.w(MESSAGES, throwable, "UncaughtException in AvatarView.");
MetricModule.sendError(throwable, null, null);
}
};
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/view/MessageCenterRecyclerViewAdapter.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/view/MessageCenterRecyclerViewAdapter.java
index 06beca6fd..87c79ddd2 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/view/MessageCenterRecyclerViewAdapter.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/view/MessageCenterRecyclerViewAdapter.java
@@ -15,6 +15,7 @@
import com.apptentive.android.sdk.ApptentiveHelper;
import com.apptentive.android.sdk.ApptentiveInternal;
import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.ApptentiveLogTag;
import com.apptentive.android.sdk.R;
import com.apptentive.android.sdk.conversation.Conversation;
import com.apptentive.android.sdk.conversation.ConversationDispatchTask;
@@ -48,6 +49,7 @@
import java.util.List;
import static com.apptentive.android.sdk.ApptentiveHelper.dispatchConversationTask;
+import static com.apptentive.android.sdk.ApptentiveLogTag.MESSAGES;
import static com.apptentive.android.sdk.module.messagecenter.model.MessageCenterListItem.GREETING;
import static com.apptentive.android.sdk.module.messagecenter.model.MessageCenterListItem.MESSAGE_AUTO;
import static com.apptentive.android.sdk.module.messagecenter.model.MessageCenterListItem.MESSAGE_COMPOSER;
@@ -124,7 +126,7 @@ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType
return new ContextMessageHolder(view);
}
}
- ApptentiveLog.w("onCreateViewHolder(%d) returning null.", viewType);
+ ApptentiveLog.w(MESSAGES, "onCreateViewHolder(%d) returning null.", viewType);
return null;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/metric/MetricModule.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/metric/MetricModule.java
index 3e7b2fb9d..ef3633318 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/module/metric/MetricModule.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/metric/MetricModule.java
@@ -21,6 +21,7 @@
import static com.apptentive.android.sdk.ApptentiveHelper.checkConversationQueue;
import static com.apptentive.android.sdk.ApptentiveHelper.dispatchOnConversationQueue;
import static com.apptentive.android.sdk.ApptentiveHelper.isConversationQueue;
+import static com.apptentive.android.sdk.ApptentiveLogTag.UTIL;
/**
* @author Sky Kelsey.
@@ -50,7 +51,7 @@ protected void execute() {
Configuration config = Configuration.load();
if (config.isMetricsEnabled()) {
- ApptentiveLog.v("Sending Metric: %s, trigger: %s, data: %s", type.getLabelName(), trigger, data != null ? data.toString() : "null");
+ ApptentiveLog.v(UTIL, "Sending Metric: %s, trigger: %s, data: %s", type.getLabelName(), trigger, data != null ? data.toString() : "null");
EventPayload event = new EventPayload(type.getLabelName(), trigger);
event.putData(data);
sendEvent(event);
@@ -93,14 +94,14 @@ protected void execute() {
}
Configuration config = Configuration.load();
if (config.isMetricsEnabled()) {
- ApptentiveLog.v("Sending Error Metric: %s, data: %s", type.getLabelName(), data.toString());
+ ApptentiveLog.v(UTIL, "Sending Error Metric: %s, data: %s", type.getLabelName(), data.toString());
EventPayload event = new EventPayload(type.getLabelName(), data);
sendEvent(event);
}
} catch (Exception e) {
// Since this is the last place in Apptentive code we can catch exceptions, we must catch all other Exceptions to
// prevent the app from crashing.
- ApptentiveLog.w(e, "Error creating Error Metric. Nothing we can do but log this.");
+ ApptentiveLog.w(UTIL, e, "Error creating Error Metric. Nothing we can do but log this.");
}
}
@@ -111,7 +112,7 @@ private static void sendEvent(EventPayload event) {
if (conversation != null) {
conversation.addPayload(event);
} else {
- ApptentiveLog.w("Unable to send event '%s': no active conversation", event);
+ ApptentiveLog.w(UTIL, "Unable to send event '%s': no active conversation", event);
}
}
}
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 604c2d04d..cccae194c 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
@@ -16,7 +16,6 @@
import com.apptentive.android.sdk.util.threading.DispatchQueue;
import com.apptentive.android.sdk.util.threading.DispatchTask;
-import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
@@ -32,7 +31,7 @@
import java.util.Set;
import java.util.zip.GZIPInputStream;
-import static com.apptentive.android.sdk.ApptentiveLog.Level.VERY_VERBOSE;
+import static com.apptentive.android.sdk.ApptentiveLog.Level.VERBOSE;
import static com.apptentive.android.sdk.ApptentiveLogTag.*;
import static com.apptentive.android.sdk.debug.Assert.*;
@@ -232,14 +231,14 @@ void dispatchSync(DispatchQueue networkQueue) {
} catch (NetworkUnavailableException e) {
responseCode = -1; // indicates failure
errorMessage = e.getMessage();
- ApptentiveLog.w(e.getMessage());
- ApptentiveLog.w("Cancelled? %b", isCancelled());
+ ApptentiveLog.w(NETWORK, e.getMessage());
+ ApptentiveLog.w(NETWORK, "Cancelled? %b", isCancelled());
} catch (Exception e) {
responseCode = -1; // indicates failure
errorMessage = e.getMessage();
- ApptentiveLog.e("Cancelled? %b", isCancelled());
+ ApptentiveLog.e(NETWORK, "Cancelled? %b", isCancelled());
if (!isCancelled()) {
- ApptentiveLog.e(e, "Unable to perform request");
+ ApptentiveLog.e(NETWORK, "Unable to perform request: %s", this);
}
}
@@ -271,8 +270,8 @@ private void sendRequestSync() throws Exception {
URL url = new URL(urlString);
ApptentiveLog.d(NETWORK, "Performing request: %s %s", method, url);
- if (ApptentiveLog.canLog(VERY_VERBOSE)) {
- ApptentiveLog.vv(NETWORK, "%s", toString());
+ if (ApptentiveLog.canLog(VERBOSE)) {
+ ApptentiveLog.v(NETWORK, "%s", toString());
}
retrying = false;
@@ -282,7 +281,7 @@ private void sendRequestSync() throws Exception {
connection.setReadTimeout(readTimeout);
if (!isNetworkConnectionPresent()) {
- ApptentiveLog.d("No network connection present. Request will fail.");
+ ApptentiveLog.d(NETWORK, "No network connection present. Request will fail.");
throw new NetworkUnavailableException("The network is not currently active.");
}
@@ -326,7 +325,11 @@ private void sendRequestSync() throws Exception {
boolean gzipped = isGzipContentEncoding(responseHeaders);
if (responseCode >= HttpURLConnection.HTTP_OK && responseCode < HttpURLConnection.HTTP_MULT_CHOICE) {
responseData = readResponse(connection.getInputStream(), gzipped);
- ApptentiveLog.v(NETWORK, "Response data: %s", responseData);
+ if (ApptentiveLog.shouldSanitizeLogMessages()) {
+ ApptentiveLog.v(NETWORK, "Response data: %d bytes", responseData.length());
+ } else {
+ ApptentiveLog.v(NETWORK, "Response data: %s", responseData);
+ }
} else {
errorMessage = StringUtils.format("Unexpected response code: %d (%s)", responseCode, connection.getResponseMessage());
responseData = readResponse(connection.getErrorStream(), gzipped);
@@ -369,12 +372,14 @@ private boolean retryRequest(DispatchQueue networkQueue, int responseCode) {
++retryAttempt;
if (!retryPolicy.shouldRetryRequest(responseCode, retryAttempt)) {
- ApptentiveLog.v(NETWORK, "Retry policy declined request retry");
+ ApptentiveLog.v(NETWORK, "Retry policy declined request retry: %s", this);
return false;
}
retrying = true;
- networkQueue.dispatchAsyncOnce(retryDispatchTask, retryPolicy.getRetryTimeoutMillis(retryAttempt));
+ long retryTimeout = retryPolicy.getRetryTimeoutMillis(retryAttempt);
+ ApptentiveLog.v(NETWORK, "Retry request in %d ms: %s", retryTimeout, this);
+ networkQueue.dispatchAsyncOnce(retryDispatchTask, retryTimeout);
return true;
}
@@ -482,12 +487,16 @@ public String toString() {
byte[] requestData = createRequestData();
String requestString;
String contentType = requestProperties.get("Content-Type").toString();
- if (contentType.contains("application/octet-stream") || contentType.contains("multipart/encrypted")) {
- requestString = "Base64 encoded binary request: " + Base64.encodeToString(requestData, Base64.NO_WRAP);
+ if (ApptentiveLog.shouldSanitizeLogMessages()) {
+ requestString = StringUtils.format(" %d bytes", requestData.length);
} else {
- requestString = new String(requestData);
+ if (contentType.contains("application/octet-stream") || contentType.contains("multipart/encrypted")) {
+ requestString = "Base64 encoded binary request: " + Base64.encodeToString(requestData, Base64.NO_WRAP);
+ } else {
+ requestString = new String(requestData);
+ }
}
- return String.format(
+ return StringUtils.format(
"\n" +
"Request:\n" +
"\t%s %s\n" +
@@ -499,7 +508,7 @@ public String toString() {
"\t%s",
/* Request */
method.name(), urlString,
- requestProperties,
+ sanitize(requestProperties),
requestString,
/* Response */
responseCode,
@@ -511,6 +520,18 @@ public String toString() {
return null;
}
+ private Map sanitize(Map requestProperties) {
+ if (ApptentiveLog.shouldSanitizeLogMessages()) {
+ HashMap copy = new HashMap<>(requestProperties);
+ if (copy.containsKey("Authorization")) {
+ copy.put("Authorization", "");
+ }
+ return copy;
+ }
+
+ return requestProperties;
+ }
+
//endregion
//region Getters/Setters
@@ -603,7 +624,7 @@ public Apptentive.AuthenticationFailedReason getAuthenticationFailedReason() {
String errorType = errorObject.optString("error_type", null);
return Apptentive.AuthenticationFailedReason.parse(errorType, error);
} catch (Exception e) {
- ApptentiveLog.w(e, "Error parsing authentication failure object.");
+ ApptentiveLog.w(NETWORK, e, "Error parsing authentication failure object.");
}
}
return Apptentive.AuthenticationFailedReason.UNKNOWN;
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/network/HttpRequestManager.java b/apptentive/src/main/java/com/apptentive/android/sdk/network/HttpRequestManager.java
index c6a523304..172be539a 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/network/HttpRequestManager.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/network/HttpRequestManager.java
@@ -25,13 +25,7 @@ public class HttpRequestManager {
private final DispatchQueue networkQueue;
private Listener listener;
-
- /**
- * Creates a request manager with a default concurrent "network" queue.
- */
- public HttpRequestManager() {
- this(DispatchQueue.createBackgroundQueue("Apptentive Network Queue", DispatchQueueType.Concurrent));
- }
+ private HttpRequest.Injector requestInjector;
/**
* Creates a request manager with custom network dispatch queue
@@ -57,6 +51,10 @@ synchronized HttpRequest startRequest(HttpRequest request) {
throw new IllegalArgumentException("Request is null");
}
+ if (requestInjector != null) {
+ request.setInjector(requestInjector);
+ }
+
registerRequest(request);
dispatchRequest(request);
notifyRequestStarted(request);
@@ -148,6 +146,10 @@ private void notifyCancelledAllRequests() {
//region Getters/Setters
+ public static HttpRequestManager sharedManager() {
+ return Holder.INSTANCE;
+ }
+
public Listener getListener() {
return listener;
}
@@ -156,6 +158,10 @@ public void setListener(Listener listener) {
this.listener = listener;
}
+ public void setRequestInjector(HttpRequest.Injector requestInjector) {
+ this.requestInjector = requestInjector;
+ }
+
//endregion
//region Listener
@@ -169,4 +175,12 @@ public interface Listener {
}
//endregion
+
+ //region Holder
+
+ private static class Holder {
+ private static final HttpRequestManager INSTANCE = new HttpRequestManager(DispatchQueue.createBackgroundQueue("Apptentive Network Queue", DispatchQueueType.Concurrent));
+ }
+
+ //endregion
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/network/HttpRequestRetryPolicyDefault.java b/apptentive/src/main/java/com/apptentive/android/sdk/network/HttpRequestRetryPolicyDefault.java
index 05fdad295..8dea205c4 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/network/HttpRequestRetryPolicyDefault.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/network/HttpRequestRetryPolicyDefault.java
@@ -6,12 +6,19 @@
package com.apptentive.android.sdk.network;
+import java.util.Random;
+
public class HttpRequestRetryPolicyDefault implements HttpRequestRetryPolicy {
public static final int RETRY_COUNT_INFINITE = -1;
public static final long DEFAULT_RETRY_TIMEOUT_MILLIS = 5 * 1000;
public static final int DEFAULT_RETRY_COUNT = 5;
+ /**
+ * Maximum retry timeout for the exponential back-off
+ */
+ private static final long MAX_RETRY_CAP = 10 * 60 * 1000L;
+
/**
* How many times should request retry before giving up
*/
@@ -22,6 +29,8 @@ public class HttpRequestRetryPolicyDefault implements HttpRequestRetryPolicy {
*/
private long retryTimeoutMillis = DEFAULT_RETRY_TIMEOUT_MILLIS;
+ private static final Random RANDOM = new Random();
+
/**
* Returns true
is request should be retried.
*
@@ -47,7 +56,8 @@ public boolean shouldRetryRequest(int responseCode, int retryAttempt) {
*/
@Override
public long getRetryTimeoutMillis(int retryAttempt) {
- return retryTimeoutMillis;
+ long temp = Math.min(MAX_RETRY_CAP, (long) (retryTimeoutMillis * Math.pow(2.0, retryAttempt - 1)));
+ return (long) ((temp / 2) * (1.0 + RANDOM.nextDouble()));
}
public void setMaxRetryCount(int maxRetryCount) {
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/notifications/ApptentiveNotificationCenter.java b/apptentive/src/main/java/com/apptentive/android/sdk/notifications/ApptentiveNotificationCenter.java
index 3f49b2cf7..831bb5376 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/notifications/ApptentiveNotificationCenter.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/notifications/ApptentiveNotificationCenter.java
@@ -8,6 +8,7 @@
import com.apptentive.android.sdk.ApptentiveLog;
import com.apptentive.android.sdk.util.ObjectUtils;
+import com.apptentive.android.sdk.util.StringUtils;
import java.util.Collections;
import java.util.HashMap;
@@ -97,12 +98,11 @@ public synchronized void postNotification(final String name, Object... args) {
* Creates a notification with a given name and user info and posts it to the receiver.
*/
public synchronized void postNotification(final String name, final Map userInfo) {
- final ApptentiveNotification notification = new ApptentiveNotification(name, userInfo);
- ApptentiveLog.v(NOTIFICATIONS, "Post notification: %s", notification);
+ ApptentiveLog.v(NOTIFICATIONS, "Post notification: name=%s userInfo={%s}", name, StringUtils.toString(userInfo));
- final ApptentiveNotificationObserverList list = findObserverList(notification.getName());
+ final ApptentiveNotificationObserverList list = findObserverList(name);
if (list != null) {
- list.notifyObservers(notification);
+ list.notifyObservers(new ApptentiveNotification(name, userInfo));
}
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/serialization/ObjectSerialization.java b/apptentive/src/main/java/com/apptentive/android/sdk/serialization/ObjectSerialization.java
index 4da135884..2a6d1e597 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/serialization/ObjectSerialization.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/serialization/ObjectSerialization.java
@@ -1,5 +1,7 @@
package com.apptentive.android.sdk.serialization;
+import android.support.v4.util.AtomicFile;
+
import com.apptentive.android.sdk.util.Util;
import java.io.DataInput;
@@ -19,13 +21,16 @@ public class ObjectSerialization {
* Writes an object ot a file
*/
public static void serialize(File file, SerializableObject object) throws IOException {
+ AtomicFile atomicFile = new AtomicFile(file);
FileOutputStream stream = null;
try {
- stream = new FileOutputStream(file);
+ stream = atomicFile.startWrite();
DataOutputStream out = new DataOutputStream(stream);
object.writeExternal(out);
- } finally {
- Util.ensureClosed(stream);
+ atomicFile.finishWrite(stream); // serialization was successful
+ } catch (Exception e) {
+ atomicFile.failWrite(stream); // serialization failed
+ throw new IOException(e); // throw exception up the chain
}
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/AppReleaseManager.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/AppReleaseManager.java
index 432c31c13..3a3935694 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/AppReleaseManager.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/AppReleaseManager.java
@@ -7,16 +7,15 @@
package com.apptentive.android.sdk.storage;
import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
import com.apptentive.android.sdk.ApptentiveInternal;
-import com.apptentive.android.sdk.ApptentiveLog;
import com.apptentive.android.sdk.model.*;
+import com.apptentive.android.sdk.util.ApplicationInfo;
+import com.apptentive.android.sdk.util.RuntimeUtils;
import com.apptentive.android.sdk.util.Util;
+import static com.apptentive.android.sdk.ApptentiveLogTag.CONVERSATION;
+
public class AppReleaseManager {
public static AppRelease generateCurrentAppRelease(Context context, ApptentiveInternal apptentiveInternal) {
@@ -24,39 +23,21 @@ public static AppRelease generateCurrentAppRelease(Context context, ApptentiveIn
AppRelease appRelease = new AppRelease();
String appPackageName = context.getPackageName();
- PackageManager packageManager = context.getPackageManager();
-
- int currentVersionCode = 0;
- String currentVersionName = "0";
- int targetSdkVersion = 0;
- boolean isAppDebuggable = false;
- try {
- PackageInfo packageInfo = packageManager.getPackageInfo(appPackageName, PackageManager.GET_META_DATA | PackageManager.GET_RECEIVERS);
- ApplicationInfo ai = packageInfo.applicationInfo;
- currentVersionCode = packageInfo.versionCode;
- currentVersionName = packageInfo.versionName;
- targetSdkVersion = packageInfo.applicationInfo.targetSdkVersion;
- Bundle metaData = ai.metaData;
- if (metaData != null) {
- isAppDebuggable = (ai.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
- }
- } catch (PackageManager.NameNotFoundException e) {
- ApptentiveLog.e("Failed to read app's PackageInfo.");
- }
-
int themeOverrideResId = context.getResources().getIdentifier("ApptentiveThemeOverride", "style", appPackageName);
+ ApplicationInfo applicationInfo = RuntimeUtils.getApplicationInfo(context);
+
appRelease.setAppStore(Util.getInstallerPackageName(context));
- appRelease.setDebug(isAppDebuggable);
+ appRelease.setDebug(applicationInfo.isDebuggable());
appRelease.setIdentifier(appPackageName);
if (apptentiveInternal != null) {
appRelease.setInheritStyle(apptentiveInternal.isAppUsingAppCompatTheme());
}
appRelease.setOverrideStyle(themeOverrideResId != 0);
- appRelease.setTargetSdkVersion(String.valueOf(targetSdkVersion));
+ appRelease.setTargetSdkVersion(String.valueOf(applicationInfo.getTargetSdkVersion()));
appRelease.setType("android");
- appRelease.setVersionCode(currentVersionCode);
- appRelease.setVersionName(currentVersionName);
+ appRelease.setVersionCode(applicationInfo.getVersionCode());
+ appRelease.setVersionName(applicationInfo.getVersionName());
return appRelease;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveDatabaseHelper.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveDatabaseHelper.java
index cc60130e9..e0f38299e 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveDatabaseHelper.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveDatabaseHelper.java
@@ -40,7 +40,9 @@
import java.util.UUID;
import static com.apptentive.android.sdk.ApptentiveHelper.dispatchConversationTask;
+import static com.apptentive.android.sdk.ApptentiveLog.hideIfSanitized;
import static com.apptentive.android.sdk.ApptentiveLogTag.DATABASE;
+import static com.apptentive.android.sdk.ApptentiveLogTag.MESSAGES;
import static com.apptentive.android.sdk.ApptentiveLogTag.PAYLOADS;
import static com.apptentive.android.sdk.debug.Assert.assertFalse;
import static com.apptentive.android.sdk.debug.Assert.assertNotNull;
@@ -408,18 +410,18 @@ private void upgradeVersion2to3(SQLiteDatabase db) {
db.beginTransaction();
// 1. Rename existing "payload" table to "legacy_payload"
- ApptentiveLog.vv(DATABASE, "\t1. Backing up \"payloads\" database to \"legacy_payloads\"");
+ ApptentiveLog.v(DATABASE, "\t1. Backing up \"payloads\" database to \"legacy_payloads\"");
db.execSQL(BACKUP_LEGACY_PAYLOAD_TABLE);
// 2. Create new Payload table as "payload"
- ApptentiveLog.vv(DATABASE, "\t2. Creating new \"payloads\" database.");
+ ApptentiveLog.v(DATABASE, "\t2. Creating new \"payloads\" database.");
db.execSQL(TABLE_CREATE_PAYLOAD);
// 3. Load legacy payloads
- ApptentiveLog.vv(DATABASE, "\t3. Loading legacy payloads.");
+ ApptentiveLog.v(DATABASE, "\t3. Loading legacy payloads.");
cursor = db.rawQuery(SQL_QUERY_PAYLOAD_LIST_LEGACY, null);
- ApptentiveLog.vv(DATABASE, "4. Save payloads into new table.");
+ ApptentiveLog.v(DATABASE, "4. Save payloads into new table.");
JsonPayload payload;
while (cursor.moveToNext()) {
PayloadType payloadType = PayloadType.parse(cursor.getString(1));
@@ -439,7 +441,7 @@ private void upgradeVersion2to3(SQLiteDatabase db) {
payload.setNonce(nonce);
// 4. Save each payload in the new table.
- ApptentiveLog.vv(DATABASE, "Payload of type %s:, %s", payload.getPayloadType().name(), payload);
+ ApptentiveLog.v(DATABASE, "Payload of type %s:, %s", payload.getPayloadType().name(), payload);
ContentValues values = new ContentValues();
values.put(PayloadEntry.COLUMN_IDENTIFIER.name, notNull(payload.getNonce()));
values.put(PayloadEntry.COLUMN_PAYLOAD_TYPE.name, notNull(payload.getPayloadType().name()));
@@ -455,7 +457,7 @@ private void upgradeVersion2to3(SQLiteDatabase db) {
);
File dest = getPayloadBodyFile(payload.getNonce());
- ApptentiveLog.v(DATABASE, "Saving payload body to: %s", dest);
+ ApptentiveLog.v(DATABASE, "Saving payload body to: %s", hideIfSanitized(dest));
Util.writeBytes(dest, payload.renderData());
values.put(PayloadEntry.COLUMN_ENCRYPTED.name, payload.hasEncryptionKey() ? TRUE : FALSE);
@@ -464,11 +466,11 @@ private void upgradeVersion2to3(SQLiteDatabase db) {
}
// 5. Migrate messages
- ApptentiveLog.vv(DATABASE, "\t6. Migrating messages.");
+ ApptentiveLog.v(DATABASE, "\t6. Migrating messages.");
migrateMessages(db);
// 6. Finally, delete the temporary legacy table
- ApptentiveLog.vv(DATABASE, "\t6. Delete temporary \"legacy_payloads\" database.");
+ ApptentiveLog.v(DATABASE, "\t6. Delete temporary \"legacy_payloads\" database.");
db.execSQL(DELETE_LEGACY_PAYLOAD_TABLE);
db.setTransactionSuccessful();
} catch (Exception e) {
@@ -505,7 +507,7 @@ private List getAllMessages(SQLiteDatabase db) {
String json = cursor.getString(6);
ApptentiveMessage message = MessageFactory.fromJson(json);
if (message == null) {
- ApptentiveLog.e("Error parsing Record json from database: %s", json);
+ ApptentiveLog.e(MESSAGES, "Error parsing Record json from database: %s", json);
continue;
}
message.setId(cursor.getString(1));
@@ -551,7 +553,7 @@ void addPayload(Payload payload) {
);
File dest = getPayloadBodyFile(payload.getNonce());
- ApptentiveLog.v(DATABASE, "Saving payload body to: %s", dest);
+ ApptentiveLog.v(DATABASE, "Saving payload body to: %s", hideIfSanitized(dest));
Util.writeBytes(dest, payload.renderData());
values.put(PayloadEntry.COLUMN_ENCRYPTED.name, payload.hasEncryptionKey() ? TRUE : FALSE);
@@ -567,7 +569,7 @@ void addPayload(Payload payload) {
}
}
- if (ApptentiveLog.canLog(ApptentiveLog.Level.VERY_VERBOSE)) {
+ if (ApptentiveLog.canLog(ApptentiveLog.Level.VERBOSE)) {
printPayloadTable("Added payload");
}
}
@@ -593,7 +595,7 @@ void deletePayload(String payloadIdentifier) {
File dest = getPayloadBodyFile(payloadIdentifier);
ApptentiveLog.v(DATABASE, "Deleted payload \"%s\" data file successfully? %b", payloadIdentifier, dest.delete());
- if (ApptentiveLog.canLog(ApptentiveLog.Level.VERY_VERBOSE)) {
+ if (ApptentiveLog.canLog(ApptentiveLog.Level.VERBOSE)) {
printPayloadTable("Deleted payload");
}
}
@@ -610,7 +612,7 @@ void deleteAllPayloads() {
}
PayloadData getOldestUnsentPayload() {
- if (ApptentiveLog.canLog(ApptentiveLog.Level.VERY_VERBOSE)) {
+ if (ApptentiveLog.canLog(ApptentiveLog.Level.VERBOSE)) {
printPayloadTable("getOldestUnsentPayload");
}
@@ -646,7 +648,7 @@ PayloadData getOldestUnsentPayload() {
File file = getPayloadBodyFile(nonce);
if (!file.exists()) {
- ApptentiveLog.w("Oldest unsent payload had no data file. Deleting.");
+ ApptentiveLog.w(PAYLOADS, "Oldest unsent payload had no data file. Deleting.");
deletePayload(nonce);
continue;
}
@@ -670,7 +672,7 @@ private String updatePayloadRequestPath(String path, String conversationId) {
}
void updateIncompletePayloads(String conversationId, String authToken, String localConversationId, boolean legacyPayloads) {
- if (ApptentiveLog.canLog(ApptentiveLog.Level.VERY_VERBOSE)) {
+ if (ApptentiveLog.canLog(ApptentiveLog.Level.VERBOSE)) {
printPayloadTable("updateIncompletePayloads BEFORE");
}
@@ -697,7 +699,7 @@ void updateIncompletePayloads(String conversationId, String authToken, String lo
// remove incomplete payloads which don't belong to an active conversation
removeCorruptedPayloads();
- if (ApptentiveLog.canLog(ApptentiveLog.Level.VERY_VERBOSE)) {
+ if (ApptentiveLog.canLog(ApptentiveLog.Level.VERBOSE)) {
printPayloadTable("updateIncompletePayloads AFTER");
}
}
@@ -851,7 +853,7 @@ private void printPayloadTable(String title) {
cursor = db.rawQuery(SQL_QUERY_PAYLOAD_GET_IN_SEND_ORDER, null);
int payloadCount = cursor.getCount();
if (payloadCount == 0) {
- ApptentiveLog.vv(PAYLOADS, "%s (%d payload(s))", title, payloadCount);
+ ApptentiveLog.v(PAYLOADS, "%s (%d payload(s))", title, payloadCount);
return;
}
@@ -879,13 +881,13 @@ private void printPayloadTable(String title) {
cursor.getString(PayloadEntry.COLUMN_CONTENT_TYPE.index),
cursor.getString(PayloadEntry.COLUMN_CONVERSATION_ID.index),
cursor.getString(PayloadEntry.COLUMN_REQUEST_METHOD.index),
- cursor.getString(PayloadEntry.COLUMN_PATH.index),
+ hideIfSanitized(cursor.getString(PayloadEntry.COLUMN_PATH.index)),
cursor.getInt(PayloadEntry.COLUMN_ENCRYPTED.index),
cursor.getString(PayloadEntry.COLUMN_LOCAL_CONVERSATION_ID.index),
- cursor.getString(PayloadEntry.COLUMN_AUTH_TOKEN.index)
+ hideIfSanitized(cursor.getString(PayloadEntry.COLUMN_AUTH_TOKEN.index))
};
}
- ApptentiveLog.vv(PAYLOADS, "%s (%d payload(s)):\n%s", title, payloadCount, StringUtils.table(rows));
+ ApptentiveLog.v(PAYLOADS, "%s (%d payload(s)):\n%s", title, payloadCount, StringUtils.table(rows));
} catch (Exception ignored) {
ignored.printStackTrace();
} finally {
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 d44bc4ea7..394850b72 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
@@ -19,7 +19,6 @@
import com.apptentive.android.sdk.notifications.ApptentiveNotification;
import com.apptentive.android.sdk.notifications.ApptentiveNotificationCenter;
import com.apptentive.android.sdk.notifications.ApptentiveNotificationObserver;
-import com.apptentive.android.sdk.util.ObjectUtils;
import com.apptentive.android.sdk.util.threading.DispatchQueue;
import com.apptentive.android.sdk.util.threading.DispatchTask;
@@ -34,6 +33,7 @@
import static com.apptentive.android.sdk.ApptentiveHelper.checkConversationQueue;
import static com.apptentive.android.sdk.ApptentiveHelper.dispatchOnConversationQueue;
+import static com.apptentive.android.sdk.ApptentiveLogTag.CONVERSATION;
import static com.apptentive.android.sdk.ApptentiveLogTag.PAYLOADS;
import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_APP_ENTERED_BACKGROUND;
import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_APP_ENTERED_FOREGROUND;
@@ -209,7 +209,7 @@ public void onFinishSending(PayloadSender sender, PayloadData payload, boolean c
ApptentiveLog.v(PAYLOADS, "Payload failed to send due to a connection error.");
retrySending(5000);
return;
- } else if (responseCode > 500) {
+ } else if (responseCode >= 500) {
ApptentiveLog.v(PAYLOADS, "Payload failed to send due to a server error.");
retrySending(5000);
return;
@@ -307,7 +307,7 @@ public void onReceiveNotification(ApptentiveNotification notification) {
final String conversationLocalIdentifier = notNull(conversation.getLocalIdentifier());
final boolean legacyPayloads = ConversationState.LEGACY_PENDING.equals(conversation.getPrevState());
- ApptentiveLog.d("Conversation %s state changed %s -> %s.", conversationId, conversation.getPrevState(), conversation.getState());
+ ApptentiveLog.d(CONVERSATION, "Conversation %s state changed %s -> %s.", conversationId, conversation.getPrevState(), conversation.getState());
// when the Conversation ID comes back from the server, we need to update
// the payloads that may have already been enqueued so
// that they each have the Conversation ID.
@@ -319,7 +319,7 @@ public void run() {
dbHelper.updateIncompletePayloads(conversationId, conversationToken, conversationLocalIdentifier, legacyPayloads);
sendNextPayloadSync(); // after we've updated payloads - we need to send them
} catch (Exception e) {
- ApptentiveLog.e(e, "Exception while trying to update incomplete payloads");
+ ApptentiveLog.e(CONVERSATION, e, "Exception while trying to update incomplete payloads");
}
}
});
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/Device.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/Device.java
index fb5f9a78c..28c8fcfbd 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/Device.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/Device.java
@@ -6,8 +6,6 @@
package com.apptentive.android.sdk.storage;
-import android.text.TextUtils;
-
import com.apptentive.android.sdk.util.StringUtils;
public class Device implements Saveable, DataChangedListener {
@@ -38,6 +36,7 @@ public class Device implements Saveable, DataChangedListener {
private String localeLanguageCode;
private String localeRaw;
private String utcOffset;
+ private String advertiserId;
private IntegrationConfig integrationConfig;
private transient DataChangedListener listener;
@@ -103,6 +102,7 @@ public Device clone() {
clone.localeLanguageCode = localeLanguageCode;
clone.localeRaw = localeRaw;
clone.utcOffset = utcOffset;
+ clone.advertiserId = advertiserId;
if (integrationConfig != null) {
clone.integrationConfig = integrationConfig.clone();
}
@@ -376,6 +376,17 @@ public void setUtcOffset(String utcOffset) {
}
}
+ public String getAdvertiserId() {
+ return advertiserId;
+ }
+
+ public void setAdvertiserId(String advertiserId) {
+ if (!StringUtils.equal(this.advertiserId, advertiserId)) {
+ this.advertiserId = advertiserId;
+ notifyDataChanged();
+ }
+ }
+
public IntegrationConfig getIntegrationConfig() {
return integrationConfig;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/DeviceManager.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/DeviceManager.java
index aab86f0f7..bf4d97655 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/DeviceManager.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/DeviceManager.java
@@ -10,8 +10,10 @@
import android.os.Build;
import android.telephony.TelephonyManager;
-import com.apptentive.android.sdk.ApptentiveInternal;
+import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.model.Configuration;
import com.apptentive.android.sdk.model.DevicePayload;
+import com.apptentive.android.sdk.util.AdvertiserManager;
import com.apptentive.android.sdk.util.Constants;
import com.apptentive.android.sdk.util.Util;
@@ -22,7 +24,6 @@
* A helper class with static methods for and diffing information about the current device.
*/
public class DeviceManager {
-
public static Device generateNewDevice(Context context) {
Device device = new Device();
@@ -42,6 +43,21 @@ public static Device generateNewDevice(Context context) {
device.setBuildType(Build.TYPE);
device.setBuildId(Build.ID);
+ // advertiser id
+ try {
+ Configuration configuration = Configuration.load();
+ if (configuration.isCollectingAdID()) {
+ AdvertiserManager.AdvertisingIdClientInfo info = AdvertiserManager.getAdvertisingIdClientInfo();
+ if (info != null && !info.isLimitAdTrackingEnabled()) {
+ device.setAdvertiserId(info.getId());
+ } else {
+ ApptentiveLog.w("Advertising ID tracking is not available or limited");
+ }
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while collecting advertising ID");
+ }
+
// Second, set the stuff that requires querying system services.
TelephonyManager tm = ((TelephonyManager) (context.getSystemService(Context.TELEPHONY_SERVICE)));
device.setCarrier(tm.getSimOperatorName());
@@ -193,6 +209,11 @@ public static DevicePayload getDiffPayload(com.apptentive.android.sdk.storage.De
changed = true;
}
+ if (oldDevice == null || !equal(oldDevice.getAdvertiserId(), newDevice.getAdvertiserId())) {
+ ret.setAdvertiserId(newDevice.getAdvertiserId());
+ changed = true;
+ }
+
if (oldDevice == null || !equal(oldDevice.getIntegrationConfig(), newDevice.getIntegrationConfig())) {
IntegrationConfig integrationConfig = newDevice.getIntegrationConfig();
ret.setIntegrationConfig(integrationConfig != null ? integrationConfig.toJson() : null);
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/EncryptedFileSerializer.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/EncryptedFileSerializer.java
index a2dc89274..8db840768 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/EncryptedFileSerializer.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/EncryptedFileSerializer.java
@@ -12,6 +12,8 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
@@ -29,7 +31,7 @@ public EncryptedFileSerializer(File file, String encryptionKey) {
}
@Override
- protected void serialize(File file, Object object) throws SerializerException {
+ protected void serialize(FileOutputStream stream, Object object) throws Exception {
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
try {
@@ -39,9 +41,7 @@ protected void serialize(File file, Object object) throws SerializerException {
final byte[] unencryptedBytes = bos.toByteArray();
Encryptor encryptor = new Encryptor(encryptionKey);
final byte[] encryptedBytes = encryptor.encrypt(unencryptedBytes);
- Util.writeBytes(file, encryptedBytes);
- } catch (Exception e) {
- throw new SerializerException(e);
+ stream.write(encryptedBytes); // TODO: should we write using a buffer?
} finally {
Util.ensureClosed(bos);
Util.ensureClosed(oos);
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/FileSerializer.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/FileSerializer.java
index 767aa5029..747a4ad54 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/FileSerializer.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/FileSerializer.java
@@ -6,12 +6,15 @@
package com.apptentive.android.sdk.storage;
+import android.support.v4.util.AtomicFile;
+
import com.apptentive.android.sdk.util.Util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
+import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
@@ -29,7 +32,18 @@ public FileSerializer(File file) {
@Override
public void serialize(Object object) throws SerializerException {
file.getParentFile().mkdirs();
- serialize(file, object);
+
+ AtomicFile atomicFile = new AtomicFile(file);
+ FileOutputStream stream = null;
+ try {
+ stream = atomicFile.startWrite();
+ serialize(stream, object);
+ atomicFile.finishWrite(stream);
+ } catch (Exception e) {
+ atomicFile.failWrite(stream);
+ throw new SerializerException(e);
+ }
+
}
@Override
@@ -37,20 +51,14 @@ public Object deserialize() throws SerializerException {
return deserialize(file);
}
- protected void serialize(File file, Object object) throws SerializerException {
- ByteArrayOutputStream bos;
+ protected void serialize(FileOutputStream stream, Object object) throws Exception {
ObjectOutputStream oos = null;
- FileOutputStream fos = null;
try {
- bos = new ByteArrayOutputStream();
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(object);
- fos = new FileOutputStream(file);
- fos.write(bos.toByteArray());
- } catch (Exception e) {
- throw new SerializerException(e);
+ stream.write(bos.toByteArray());
} finally {
- Util.ensureClosed(fos);
Util.ensureClosed(oos);
}
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/SdkManager.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/SdkManager.java
index aea9df391..f35139421 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/SdkManager.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/SdkManager.java
@@ -20,14 +20,14 @@ public static Sdk generateCurrentSdk(Context context) {
Sdk sdk = new Sdk();
// First, get all the information we can load from static resources.
- sdk.setVersion(Constants.APPTENTIVE_SDK_VERSION);
+ sdk.setVersion(Constants.getApptentiveSdkVersion());
sdk.setPlatform("Android");
// Distribution and distribution version are optionally set in the manifest by the wrapping platform (Cordova, mParticle, etc.)
Resources resources = context.getResources();
sdk.setDistribution(resources.getString(R.string.apptentive_distribution));
sdk.setDistributionVersion(resources.getString(R.string.apptentive_distribution_version));
- ApptentiveLog.vv("SDK: %s:%s", sdk.getDistribution(), sdk.getDistributionVersion());
+ ApptentiveLog.v("SDK: %s:%s", sdk.getDistribution(), sdk.getDistributionVersion());
return sdk;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/AdvertiserManager.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/AdvertiserManager.java
new file mode 100644
index 000000000..5580574aa
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/AdvertiserManager.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.util;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+
+import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.notifications.ApptentiveNotificationCenter;
+
+import static com.apptentive.android.sdk.ApptentiveLogTag.ADVERTISER_ID;
+import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_ADVERTISER_ID_DID_RESOLVE;
+import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_KEY_ADVERTISER_CLIENT_INFO;
+import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_KEY_SUCCESSFUL;
+
+public class AdvertiserManager {
+ private static final String CLASS_ADVERTISING_ID_CLIENT = "com.google.android.gms.ads.identifier.AdvertisingIdClient";
+ private static final String METHOD_GET_ADVERTISING_ID_INFO = "getAdvertisingIdInfo";
+ private static final String METHOD_GET_ID = "getId";
+ private static final String METHOD_IS_LIMIT_AD_TRACKING_ENABLED = "isLimitAdTrackingEnabled";
+
+ private static AdvertisingIdClientInfo cachedClientInfo;
+
+ public static synchronized @Nullable AdvertisingIdClientInfo getAdvertisingIdClientInfo() {
+ return cachedClientInfo;
+ }
+
+ /**
+ * Returns true if changed
+ */
+ public static synchronized boolean updateAdvertisingIdClientInfo(Context context) {
+ ApptentiveLog.v(ADVERTISER_ID, "Updating advertiser ID client info...");
+ AdvertisingIdClientInfo clientInfo = resolveAdvertisingIdClientInfo(context);
+ if (clientInfo != null && clientInfo.equals(cachedClientInfo)) {
+ return false; // no changes
+ }
+
+ ApptentiveLog.v(ADVERTISER_ID, "Advertiser ID client info changed: %s", clientInfo);
+ cachedClientInfo = clientInfo;
+ notifyClientInfoChanged(cachedClientInfo);
+ return true;
+ }
+
+ private static @Nullable AdvertisingIdClientInfo resolveAdvertisingIdClientInfo(Context context) {
+ try {
+ Invocation advertisingIdClient = Invocation.fromClass(CLASS_ADVERTISING_ID_CLIENT);
+ Object infoObject = advertisingIdClient.invokeMethod(METHOD_GET_ADVERTISING_ID_INFO, new Class>[] { Context.class }, new Object[] { context });
+ if (infoObject == null) {
+ ApptentiveLog.w("Unable to resolve advertising ID: '%s' did not return a valid value", METHOD_GET_ADVERTISING_ID_INFO);
+ return null;
+ }
+
+ Invocation info = Invocation.fromObject(infoObject);
+ String id = info.invokeStringMethod(METHOD_GET_ID);
+ boolean limitAdTrackingEnabled = info.invokeBooleanMethod(METHOD_IS_LIMIT_AD_TRACKING_ENABLED);
+ return new AdvertisingIdClientInfo(id, limitAdTrackingEnabled);
+ }
+ catch (Exception e) {
+ Throwable cause = e.getCause();
+ if (cause != null) {
+ if (StringUtils.equal(cause.getClass().getSimpleName(), "GooglePlayServicesNotAvailableException")) {
+ ApptentiveLog.e(e, "Unable to resolve advertising ID: Google Play is not installed on this device");
+ return null;
+ }
+
+ if (StringUtils.equal(cause.getClass().getSimpleName(), "GooglePlayServicesRepairableException")) {
+ ApptentiveLog.e(e, "Unable to resolve advertising ID: error connecting to Google Play Services");
+ return null;
+ }
+ }
+
+ ApptentiveLog.e(e, "Unable to resolve advertising ID");
+ return null;
+ }
+ }
+
+ private static void notifyClientInfoChanged(AdvertisingIdClientInfo clientInfo) {
+ ApptentiveNotificationCenter.defaultCenter()
+ .postNotification(NOTIFICATION_ADVERTISER_ID_DID_RESOLVE,
+ NOTIFICATION_KEY_SUCCESSFUL, clientInfo != null,
+ NOTIFICATION_KEY_ADVERTISER_CLIENT_INFO, clientInfo);
+ }
+
+ public static class AdvertisingIdClientInfo {
+ private final String id;
+ private final boolean limitAdTrackingEnabled;
+
+ public AdvertisingIdClientInfo(String id, boolean limitAdTrackingEnabled) {
+ this.id = id;
+ this.limitAdTrackingEnabled = limitAdTrackingEnabled;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public final boolean isLimitAdTrackingEnabled() {
+ return limitAdTrackingEnabled;
+ }
+
+ @Override
+ public String toString() {
+ return StringUtils.format("%s: id=%s limited=%b", getClass().getSimpleName(), ApptentiveLog.hideIfSanitized(id), limitAdTrackingEnabled);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ AdvertisingIdClientInfo that = (AdvertisingIdClientInfo) o;
+ return limitAdTrackingEnabled == that.limitAdTrackingEnabled && StringUtils.equal(id, that.id);
+ }
+ }
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/ApplicationInfo.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/ApplicationInfo.java
new file mode 100644
index 000000000..d65494130
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/ApplicationInfo.java
@@ -0,0 +1,38 @@
+package com.apptentive.android.sdk.util;
+
+public class ApplicationInfo {
+ static final ApplicationInfo NULL = new ApplicationInfo("0", -1, -1, false); // TODO: figure out constant values
+
+ private final String versionName;
+ private final int versionCode;
+ private final int targetSdkVersion;
+ private final boolean debuggable;
+
+ ApplicationInfo(String versionName, int versionCode, int targetSdkVersion, boolean debuggable) {
+ this.versionName = versionName;
+ this.versionCode = versionCode;
+ this.targetSdkVersion = targetSdkVersion;
+ this.debuggable = debuggable;
+ }
+
+ public String getVersionName() {
+ return versionName;
+ }
+
+ public int getVersionCode() {
+ return versionCode;
+ }
+
+ public int getTargetSdkVersion() {
+ return targetSdkVersion;
+ }
+
+ public boolean isDebuggable() {
+ return debuggable;
+ }
+
+ @Override
+ public String toString() {
+ return StringUtils.format("%s: versionName=%s versionCode=%d targetSdkVersion=%s debuggable=%b", getClass().getSimpleName(), versionName, versionCode, targetSdkVersion, debuggable);
+ }
+}
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 60600d95a..f2a7e6ffd 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 = "5.0.5";
+ private static final String APPTENTIVE_SDK_VERSION = "5.1.0";
public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 45000;
public static final int DEFAULT_READ_TIMEOUT_MILLIS = 45000;
@@ -64,6 +64,7 @@ public class Constants {
//region Database and File Storage
public static final String CONVERSATIONS_DIR = "apptentive/conversations";
+ public static final String CONVERSATION_METADATA_FILE = "conversation-v1.meta";
public static final String PAYLOAD_DATA_DIR = "payloads";
public static final String PAYLOAD_DATA_FILE_SUFFIX = ".data";
//endregion
@@ -87,10 +88,28 @@ public class Constants {
//endregion
//region File names
- public static final String FILE_APPTENTIVE_LOG_FILE = "apptentive-log.txt";
public static final String FILE_APPTENTIVE_ENGAGEMENT_MANIFEST = "apptentive-engagement-manifest.txt";
+ /**
+ * File extension for each log file
+ */
+ public static final String LOG_FILE_PREFIX = "apptentive-";
+ /**
+ * File extension for each log file
+ */
+ public static final String LOG_FILE_EXT = ".log";
//endregion
+ //region Notification Interactions
+ public static final String NOTIFICATION_CHANNEL_DEFAULT = "com.apptentive.notification.channel.DEFAULT";
+ public static final String NOTIFICATION_ACTION_DELETE = "com.apptentive.notification.action.DELETE";
+ public static final String NOTIFICATION_ACTION_DISPLAY = "com.apptentive.notification.action.DISPLAY";
+ public static final String NOTIFICATION_EXTRA_INTERACTION_TYPE = "com.apptentive.notification.extra.INTERACTION_TYPE";
+ public static final String NOTIFICATION_EXTRA_INTERACTION_DEFINITION= "com.apptentive.notification.extra.INTERACTION_DEFINITION";
+ public static final String NOTIFICATION_EXTRA_ID= "com.apptentive.notification.extra.ID";
+ public static final int NOTIFICATION_ID_DEFAULT = 0xFEEDBAAC;
+ public static final String NOTIFICATION_ACTION_NOTE_BUTTON_PRESSED = "com.apptentive.notification.action.note.BUTTON_PRESSED";
+ public static final String NOTIFICATION_EXTRA_NOTE_ACTION_INDEX = "com.apptentive.notification.extra.note.ACTION_INDEX";
+ //endregion
//region Old keys no longer used
public static final String MANIFEST_KEY_SDK_DISTRIBUTION = "apptentive_sdk_distribution";
@@ -182,4 +201,14 @@ public static String networkTypeAsString(int networkTypeAsInt) {
return networkTypeLookup[0];
}
}
+
+ // Only accessible from the tester client via reflection (should start with 'overridden')
+ private static String overriddenSdkVersion;
+
+ public static String getApptentiveSdkVersion() {
+ if (overriddenSdkVersion != null) {
+ return overriddenSdkVersion;
+ }
+ return APPTENTIVE_SDK_VERSION;
+ }
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/ContextUtils.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/ContextUtils.java
new file mode 100644
index 000000000..b1aa6813a
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/ContextUtils.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.util;
+
+import android.app.NotificationManager;
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.apptentive.android.sdk.debug.Assert;
+
+import static android.content.Context.NOTIFICATION_SERVICE;
+
+public class ContextUtils {
+ public static NotificationManager getNotificationManager(@NonNull Context context) {
+ return getSystemService(context, NOTIFICATION_SERVICE, NotificationManager.class);
+ }
+
+ private static @Nullable T getSystemService(@NonNull Context context, @NonNull String name, @NonNull Class extends T> cls) {
+ Object service = context.getSystemService(name);
+ Assert.assertTrue(cls.isInstance(service), "Unexpected service class: %s", cls);
+ return ObjectUtils.as(service, cls);
+ }
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/IndentBufferedPrinter.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/IndentBufferedPrinter.java
new file mode 100644
index 000000000..4f3a527bd
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/IndentBufferedPrinter.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.util;
+
+public class IndentBufferedPrinter extends IndentPrinter {
+ private final StringBuilder buffer;
+
+ public IndentBufferedPrinter() {
+ buffer = new StringBuilder();
+ }
+
+ @Override
+ protected void printInternal(String message) {
+ if (buffer.length() > 0) {
+ buffer.append('\n');
+ }
+ buffer.append(message);
+ }
+
+ @Override
+ public String toString() {
+ return buffer.toString();
+ }
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/IndentPrinter.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/IndentPrinter.java
new file mode 100644
index 000000000..986ce8eca
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/IndentPrinter.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.util;
+
+import static com.apptentive.android.sdk.debug.Assert.assertTrue;
+
+public abstract class IndentPrinter {
+ private static final String INDENT = " ";
+ private final StringBuilder indentBuffer;
+
+ public IndentPrinter() {
+ indentBuffer = new StringBuilder();
+ }
+
+ protected abstract void printInternal(String message);
+
+ public IndentPrinter print(String format, Object... args) {
+ String message = indentBuffer + StringUtils.format(format, args);
+ printInternal(message);
+ return this;
+ }
+
+ public IndentPrinter startBlock() {
+ indentBuffer.append(INDENT);
+ return this;
+ }
+
+ public IndentPrinter endBlock() {
+ assertTrue(indentBuffer.length() >= INDENT.length());
+ if (indentBuffer.length() >= INDENT.length()) {
+ indentBuffer.setLength(indentBuffer.length() - INDENT.length());
+ }
+ return this;
+ }
+
+ public static final IndentPrinter NULL = new IndentPrinter() {
+ @Override
+ public IndentPrinter print(String format, Object... args) {
+ // don't create any unnecessary objects here
+ return this;
+ }
+
+ @Override
+ protected void printInternal(String message) {
+ }
+
+ @Override
+ public IndentPrinter startBlock() {
+ return this;
+ }
+
+ @Override
+ public IndentPrinter endBlock() {
+ return this;
+ }
+ };
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/Invocation.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/Invocation.java
new file mode 100644
index 000000000..935d13f1d
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/Invocation.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.util;
+
+import java.lang.reflect.Method;
+
+public class Invocation {
+ private static final Class>[] EMPTY_PARAMETER_TYPES = new Class>[0];
+ private static final Object[] EMPTY_ARGS = new Object[0];
+
+ private final Class> cls;
+ private final Object target;
+
+ public static Invocation fromClass(String name) throws InvocationException {
+ if (name == null) {
+ throw new IllegalArgumentException("Class name is null");
+ }
+
+ Class> cls = RuntimeUtils.classForName(name);
+ if (cls == null) {
+ throw new InvocationException("Unable to find class '%s'", name);
+ }
+
+ return fromClass(cls);
+ }
+
+ public static Invocation fromClass(Class> cls) {
+ if (cls == null) {
+ throw new IllegalArgumentException("Class is null");
+ }
+
+ return new Invocation(cls, null);
+ }
+
+ public static Invocation fromObject(Object target) {
+ if (target == null) {
+ throw new IllegalArgumentException("Target is null");
+ }
+
+ return new Invocation(target.getClass(), target);
+ }
+
+ private Invocation(Class> cls, Object target) {
+ this.cls = cls;
+ this.target = target;
+ }
+
+ public boolean invokeBooleanMethod(String name) throws InvocationException {
+ return invokeBooleanMethod(name, EMPTY_PARAMETER_TYPES, EMPTY_ARGS);
+ }
+
+ public boolean invokeBooleanMethod(String name, Class>[] parameterTypes, Object[] args) throws InvocationException {
+ Object result = invokeMethod(name, parameterTypes, args);
+ if (result == null) {
+ throw new InvocationException("Unable to invoke method '%s' on class '%s': null returned", name, cls);
+ }
+
+ if (!(result instanceof Boolean)) {
+ throw new InvocationException("Unable to invoke method '%s' on class '%s': mismatch return type '%s'", name, cls, result.getClass());
+ }
+
+ return (Boolean) result;
+ }
+
+ public String invokeStringMethod(String name) throws InvocationException {
+ return invokeStringMethod(name, EMPTY_PARAMETER_TYPES, EMPTY_ARGS);
+ }
+
+ public String invokeStringMethod(String name, Class>[] parameterTypes, Object[] args) throws InvocationException {
+ Object result = invokeMethod(name, parameterTypes, args);
+ if (result != null && !(result instanceof String)) {
+ throw new InvocationException("Unable to invoke method '%s' on class '%s': mismatch return type '%s'", name, cls, result.getClass());
+
+ }
+
+ return (String) result;
+ }
+
+ public Object invokeMethod(String name, Class>[] parameterTypes, Object[] args) throws InvocationException {
+ try {
+ Method method = cls.getDeclaredMethod(name, parameterTypes);
+ if (method == null) {
+ throw new InvocationException("Unable to invoke method '%s' on class '%s': method not found", name, cls);
+ }
+
+ return method.invoke(target, args);
+ } catch (InvocationException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new InvocationException(e, "Unable to invoke method '%s' on class '%s'", name, cls);
+ }
+ }
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/InvocationException.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/InvocationException.java
new file mode 100644
index 000000000..7c205326c
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/InvocationException.java
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.util;
+
+public class InvocationException extends Exception {
+ public InvocationException(String format, Object... args) {
+ super(StringUtils.format(format, args));
+ }
+
+ public InvocationException(Throwable cause, String format, Object... args) {
+ super(StringUtils.format(format, args), cause);
+ }
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/JsonDiffer.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/JsonDiffer.java
index e18b77b50..1195a3fdb 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/util/JsonDiffer.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/JsonDiffer.java
@@ -13,6 +13,8 @@
import java.util.*;
+import static com.apptentive.android.sdk.ApptentiveLogTag.UTIL;
+
/**
* @author Sky Kelsey
*/
@@ -59,7 +61,7 @@ public static JSONObject getDiff(JSONObject original, JSONObject updated) {
// Do nothing.
}
} catch (JSONException e) {
- ApptentiveLog.w(e, "Error diffing object with key %s", key);
+ ApptentiveLog.w(UTIL, e, "Error diffing object with key %s", key);
} finally {
it.remove();
}
@@ -80,7 +82,7 @@ public static JSONObject getDiff(JSONObject original, JSONObject updated) {
if (ret.length() == 0) {
ret = null;
}
- ApptentiveLog.v("Generated diff: %s", ret);
+ ApptentiveLog.v(UTIL, "Generated diff: %s", ret);
return ret;
}
@@ -105,7 +107,7 @@ public static boolean areObjectsEqual(Object left, Object right) {
return false;
}
} catch (JSONException e) {
- ApptentiveLog.w(e, "Error comparing JSONObjects");
+ ApptentiveLog.w(UTIL, e, "Error comparing JSONObjects");
return false;
}
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/NotificationUtils.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/NotificationUtils.java
new file mode 100644
index 000000000..7ab647502
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/NotificationUtils.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2018, Apptentive, Inc. All Rights Reserved.
+ * Please refer to the LICENSE file for the terms and conditions
+ * under which redistribution and use of this file is permitted.
+ */
+
+package com.apptentive.android.sdk.util;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.v4.app.NotificationManagerCompat;
+
+public class NotificationUtils {
+ public static boolean isNotificationChannelEnabled(Context context, @NonNull String channelId){
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ if (!StringUtils.isNullOrEmpty(channelId)) {
+ NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ NotificationChannel channel = manager.getNotificationChannel(channelId);
+ return channel == null || channel.getImportance() != NotificationManager.IMPORTANCE_NONE;
+ }
+ return false;
+ }
+
+ return NotificationManagerCompat.from(context).areNotificationsEnabled();
+ }
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/ObjectUtils.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/ObjectUtils.java
index 8044289da..45a9c6642 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/util/ObjectUtils.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/ObjectUtils.java
@@ -21,9 +21,6 @@
package com.apptentive.android.sdk.util;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
import java.util.HashMap;
import java.util.Map;
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/RuntimeUtils.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/RuntimeUtils.java
index 5da14b3d4..9f0e5c384 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/util/RuntimeUtils.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/RuntimeUtils.java
@@ -7,53 +7,106 @@
package com.apptentive.android.sdk.util;
import android.content.Context;
-import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
-import android.os.Bundle;
+import android.support.annotation.NonNull;
+import com.apptentive.android.sdk.Apptentive;
import com.apptentive.android.sdk.ApptentiveLog;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
/**
* Collection of helper functions for Android runtime queries.
*/
public class RuntimeUtils {
+ private static ApplicationInfo cachedApplicationInfo;
+
+ public synchronized static @NonNull
+ ApplicationInfo getApplicationInfo(Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("Context is null");
+ }
+
+ if (cachedApplicationInfo == null) {
+ // cache the value once (since it won't change while the app is running)
+ try {
+ PackageManager packageManager = context.getPackageManager();
+ PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
+ android.content.pm.ApplicationInfo ai = packageInfo.applicationInfo;
+ boolean debuggable = ai != null && (ai.flags & android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0;
+ int targetSdkVersion = ai != null ? ai.targetSdkVersion : 0;
+ cachedApplicationInfo = new ApplicationInfo(packageInfo.versionName, packageInfo.versionCode, targetSdkVersion, debuggable);
+ ApptentiveLog.v("Resolved application info: %s", cachedApplicationInfo);
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while getting app info");
+ cachedApplicationInfo = ApplicationInfo.NULL;
+ }
+ }
+ return cachedApplicationInfo;
+ }
+
+ public static String getAppVersionName(Context context) {
+ return getApplicationInfo(context).getVersionName();
+ }
+
+ public static int getAppVersionCode(Context context) {
+ return getApplicationInfo(context).getVersionCode();
+ }
+
/**
* Returns true
is the app is running in a debug mode
*/
public static boolean isAppDebuggable(Context context) {
- if (context == null) {
- throw new IllegalArgumentException("Context is null");
- }
+ return getApplicationInfo(context).isDebuggable();
+ }
+ public static Class> classForName(String name) {
try {
- final String appPackageName = context.getPackageName();
- final PackageManager packageManager = context.getPackageManager();
-
- PackageInfo packageInfo = packageManager.getPackageInfo(appPackageName, PackageManager.GET_META_DATA | PackageManager.GET_RECEIVERS);
- ApplicationInfo ai = packageInfo.applicationInfo;
- Bundle metaData = ai.metaData;
- if (metaData != null) {
- return (ai.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
- }
+ return Class.forName(name);
} catch (Exception e) {
- ApptentiveLog.e("Failed to read app's PackageInfo.");
+ ApptentiveLog.e(e, "Unable to get class with name '%s'", name);
}
-
- return false;
+ return null;
}
public static void overrideStaticFinalField(Class> cls, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field instanceField = cls.getDeclaredField(fieldName);
instanceField.setAccessible(true);
- Field modifiersField = Field.class.getDeclaredField("modifiers");
+ Field modifiersField = getFieldModifiers();
modifiersField.setAccessible(true);
modifiersField.setInt(instanceField, instanceField.getModifiers() & ~Modifier.FINAL);
instanceField.set(null, value);
}
+
+ private static Field getFieldModifiers() throws NoSuchFieldException {
+ try {
+ return Field.class.getDeclaredField("modifiers"); // logic tests
+ } catch (Exception ignored) {
+ }
+
+ return Field.class.getDeclaredField("accessFlags"); // android instrumentation tests
+ }
+
+ public static List listFields(Class> cls, FieldFilter filter) {
+ List result = new ArrayList<>();
+ while (cls != null) {
+ for (Field field : cls.getDeclaredFields()) {
+ if (filter.accept(field)) {
+ result.add(field);
+ }
+ }
+ cls = cls.getSuperclass();
+ }
+ return result;
+ }
+
+ public interface FieldFilter {
+ boolean accept(Field field);
+ }
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/StringUtils.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/StringUtils.java
index 8c515cf37..cbe4ede19 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/util/StringUtils.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/StringUtils.java
@@ -26,13 +26,18 @@
import org.json.JSONObject;
import java.net.URLEncoder;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
/**
* A collection of useful string-related functions
*/
public final class StringUtils {
+ private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-dd-MM HH:mm:ss:SSS", Locale.US);
/**
* Safe String.format
@@ -75,6 +80,11 @@ public static String toString(Map, ?> map) {
return result.toString();
}
+ public static String toPrettyDate(double timeInSeconds) {
+ long timeInMillis = (long) (1000L * timeInSeconds);
+ return DATE_FORMAT.format(new Date(timeInMillis));
+ }
+
/**
* Constructs and returns a string object that is the result of interposing a separator between the elements of the array
*/
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/Util.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/Util.java
index 8052ae3ce..5f204888d 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/util/Util.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/Util.java
@@ -16,6 +16,7 @@
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
@@ -47,11 +48,14 @@
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.webkit.URLUtil;
+import android.widget.Toast;
import com.apptentive.android.sdk.ApptentiveInternal;
import com.apptentive.android.sdk.ApptentiveLog;
import com.apptentive.android.sdk.R;
import com.apptentive.android.sdk.model.StoredFile;
+import com.apptentive.android.sdk.util.threading.DispatchQueue;
+import com.apptentive.android.sdk.util.threading.DispatchTask;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
@@ -74,13 +78,14 @@
import java.lang.reflect.Field;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.TimeZone;
-import java.util.UUID;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.*;
-public class Util {
+import static com.apptentive.android.sdk.ApptentiveLogTag.UTIL;
+// TODO: this class does too much - split into smaller classes and clean up
+public class Util {
public static int getStatusBarHeight(Window window) {
Rect rectangle = new Rect();
window.getDecorView().getWindowVisibleDisplayFrame(rectangle);
@@ -119,6 +124,24 @@ public static void showSoftKeyboard(Activity activity, View target) {
}
}
+ public static void showToast(final Context context, final String message, final int duration) {
+ if (!DispatchQueue.isMainQueue()) {
+ DispatchQueue.mainQueue().dispatchAsync(new DispatchTask() {
+ @Override
+ protected void execute() {
+ showToast(context, message, duration);
+ }
+ });
+ return;
+ }
+
+ try {
+ Toast.makeText(context, message, duration).show();
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while trying to display toast message");
+ }
+ }
+
public static boolean isNetworkConnectionPresent() {
Context context = ApptentiveInternal.getInstance().getApplicationContext();
if (context == null) {
@@ -235,26 +258,12 @@ public static String stackTraceAsString(Throwable throwable) {
return sw.toString();
}
- public static String getAppVersionName(Context appContext) {
- try {
- PackageManager packageManager = appContext.getPackageManager();
- PackageInfo packageInfo = packageManager.getPackageInfo(appContext.getPackageName(), 0);
- return packageInfo.versionName;
- } catch (PackageManager.NameNotFoundException e) {
- ApptentiveLog.e(e, "Error getting app version name.");
- }
- return null;
- }
-
- public static int getAppVersionCode(Context appContext) {
+ public static String getStackTraceString(Throwable throwable) {
try {
- PackageManager packageManager = appContext.getPackageManager();
- PackageInfo packageInfo = packageManager.getPackageInfo(appContext.getPackageName(), 0);
- return packageInfo.versionCode;
- } catch (PackageManager.NameNotFoundException e) {
- ApptentiveLog.e(e, "Error getting app version code.");
+ return android.util.Log.getStackTraceString(throwable);
+ } catch (Exception e) {
+ return stackTraceAsString(throwable); // fallback for unit-tests
}
- return -1;
}
/**
@@ -312,7 +321,7 @@ public static Integer getMajorOsVersion() {
return Integer.parseInt(parts[0]);
}
} catch (Exception e) {
- ApptentiveLog.w(e, "Error getting major OS version");
+ ApptentiveLog.w(UTIL, e, "Error getting major OS version");
}
return -1;
}
@@ -745,28 +754,52 @@ public static byte[] readBytes(File file) throws IOException {
}
public static void writeText(File file, String text) throws IOException {
- if (file == null) {
- throw new IllegalArgumentException("'file' is null");
- }
-
if (text == null) {
throw new IllegalArgumentException("'text' is null");
}
- File parentFile = file.getParentFile();
- if (!parentFile.exists() && !parentFile.mkdirs()) {
- throw new IOException("Parent file could not be created: " + parentFile);
+ PrintStream output = null;
+ try {
+ output = openTextWrite(file, false); // TODO: make a parameter
+ output.print(text);
+ } finally {
+ ensureClosed(output);
+ }
+ }
+
+ public static void writeText(File file, List text) throws IOException {
+ writeText(file, text, false);
+ }
+
+ public static void writeText(File file, List text, boolean append) throws IOException {
+ if (text == null) {
+ throw new IllegalArgumentException("'text' is null");
}
PrintStream output = null;
try {
- output = new PrintStream(file, "UTF-8");
- output.print(text);
+ output = openTextWrite(file, append);
+ for (String line : text) {
+ output.println(line);
+ }
} finally {
ensureClosed(output);
}
}
+ private static PrintStream openTextWrite(File file, boolean append) throws IOException {
+ if (file == null) {
+ throw new IllegalArgumentException("'file' is null");
+ }
+
+ File parentFile = file.getParentFile();
+ if (!parentFile.exists() && !parentFile.mkdirs()) {
+ throw new IOException("Parent file could not be created: " + parentFile);
+ }
+
+ return new PrintStream(new FileOutputStream(file, append), false, "UTF-8");
+ }
+
public static void appendFileToStream(File file, OutputStream outputStream) throws IOException {
if (file == null) {
throw new IllegalArgumentException("'file' is null");
@@ -905,9 +938,9 @@ public static StoredFile createLocalStoredFile(InputStream is, String sourceUrl,
while ((count = is.read(buf, 0, 2048)) != -1) {
cos.write(buf, 0, count);
}
- ApptentiveLog.d("File saved, size = " + (cos.getBytesWritten() / 1024) + "k");
+ ApptentiveLog.v(UTIL, "File saved, size = " + (cos.getBytesWritten() / 1024) + "k");
} catch (IOException e) {
- ApptentiveLog.e("Error creating local copy of file attachment.");
+ ApptentiveLog.e(UTIL, "Error creating local copy of file attachment.");
return null;
} finally {
Util.ensureClosed(cos);
@@ -1004,6 +1037,47 @@ public static void replaceDefaultFont(Context context, String fontFilePath) {
}
}
+ /**
+ * Builds out the main theme that we would like to use for all Apptentive UI, basing it on the
+ * existing app theme, and adding Apptentive's theme where it doesn't override the existing app's
+ * attributes. Finally, it forces changes to the theme using ApptentiveThemeOverride.
+ * @param context The context for the app or Activity whose theme we want to inherit from.
+ * @return A {@link Resources.Theme}
+ */
+ public static Resources.Theme buildApptentiveInteractionTheme(Context context) {
+ Resources.Theme theme = context.getResources().newTheme();
+
+ // 1. Start by basing this on the Apptentive theme.
+ theme.applyStyle(R.style.ApptentiveTheme_Base_Versioned, true);
+
+ // 2. Get the theme from the host app. Overwrite what we have so far with the app's theme from
+ // the AndroidManifest.xml. This ensures that the app's styling shows up in our UI.
+ int appTheme;
+ try {
+ PackageManager packageManager = context.getPackageManager();
+ PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
+ ApplicationInfo ai = packageInfo.applicationInfo;
+ appTheme = ai.theme;
+ if (appTheme != 0) {
+ theme.applyStyle(appTheme, true);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // Can't happen
+ return null;
+ }
+
+ // Step 3: Restore Apptentive UI window properties that may have been overridden in Step 2. This
+ // ensures Apptentive interaction has a modal feel and look.
+ theme.applyStyle(R.style.ApptentiveBaseFrameTheme, true);
+
+ // Step 4: Apply optional theme override specified in host app's style
+ int themeOverrideResId = context.getResources().getIdentifier("ApptentiveThemeOverride", "style", context.getPackageName());
+ if (themeOverrideResId != 0) {
+ theme.applyStyle(themeOverrideResId, true);
+ }
+ return theme;
+ }
+
public static String humanReadableByteCount(long bytes, boolean si) {
int unit = si ? 1000 : 1024;
if (bytes < unit) return bytes + " B";
@@ -1027,7 +1101,7 @@ public static File getInternalDir(Context context, String path, boolean createIf
if (!internalDir.exists() && createIfNecessary) {
boolean succeed = internalDir.mkdirs();
if (!succeed) {
- ApptentiveLog.w("Unable to create internal directory: %s", internalDir);
+ ApptentiveLog.w(UTIL, "Unable to create internal directory: %s", internalDir);
}
}
return internalDir;
@@ -1113,4 +1187,9 @@ public void onClick(View v) {
return null;
}
+
+ public static String currentDateAsFilename(String prefix, String suffix) {
+ DateFormat df = new SimpleDateFormat("yyyy-MM-dd_hh-mm-ss", Locale.US);
+ return prefix + df.format(new Date()) + suffix;
+ }
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/image/ApptentiveAttachmentLoader.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/image/ApptentiveAttachmentLoader.java
index 197dc5bed..4316a2b38 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/util/image/ApptentiveAttachmentLoader.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/image/ApptentiveAttachmentLoader.java
@@ -14,7 +14,6 @@
import android.webkit.URLUtil;
import android.widget.ImageView;
-import com.apptentive.android.sdk.ApptentiveInternal;
import com.apptentive.android.sdk.ApptentiveLog;
import com.apptentive.android.sdk.R;
import com.apptentive.android.sdk.util.cache.ImageMemoryCache;
@@ -27,6 +26,8 @@
import java.util.HashSet;
import java.util.concurrent.RejectedExecutionException;
+import static com.apptentive.android.sdk.ApptentiveLogTag.UTIL;
+
public class ApptentiveAttachmentLoader {
public static final int DRAWABLE_DOWNLOAD_TAG = R.id.apptentive_drawable_downloader;
@@ -165,15 +166,15 @@ public LoaderCallback getLoaderCallback() {
public void load() {
ImageView imageView = mImageViewRef.get();
if (imageView != null) {
- ApptentiveLog.v("ApptentiveAttachmentLoader load requested:" + uri);
- ApptentiveLog.v("ApptentiveAttachmentLoader load requested on:" + imageView.toString() );
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader load requested:" + uri);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader load requested on:" + imageView.toString() );
// Handle the duplicate requests on the same grid item view
LoaderRequest oldLoaderRequest = (LoaderRequest) imageView.getTag(DRAWABLE_DOWNLOAD_TAG);
if (oldLoaderRequest != null) {
// If old request on the same view also loads from the same source, cancel the current one
if (oldLoaderRequest.getUrl().equals(uri)) {
- ApptentiveLog.v("ApptentiveAttachmentLoader load new request denied:" + uri );
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader load new request denied:" + uri );
return;
}
// If old request on the same view loads from different source, cancel the old one
@@ -181,7 +182,7 @@ public void load() {
}
if (TextUtils.isEmpty(uri)) {
- ApptentiveLog.v("ApptentiveAttachmentLoader loadDrawable(clear)");
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader loadDrawable(clear)");
loadDrawable(null);
imageView.setTag(DRAWABLE_DOWNLOAD_TAG, null);
return;
@@ -192,7 +193,7 @@ public void load() {
if (cachedBitmap != null) {
mWasDownloaded = false;
- ApptentiveLog.v("ApptentiveAttachmentLoader loadDrawable(found in cache)");
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader loadDrawable(found in cache)");
loadDrawable(cachedBitmap);
imageView.setTag(DRAWABLE_DOWNLOAD_TAG, null);
} else {
@@ -220,7 +221,7 @@ public void doDownload() {
if (imageView != null && imageView.getTag(DRAWABLE_DOWNLOAD_TAG) == this && URLUtil.isNetworkUrl(uri)) {
mDrawableDownloaderTask = new ApptentiveDownloaderTask(imageView, this);
try {
- ApptentiveLog.v("ApptentiveAttachmentLoader doDownload: " + uri);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader doDownload: " + uri);
// Conversation token is needed if the download url is a redirect link from an Apptentive endpoint
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mDrawableDownloaderTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, uri, diskCacheFilePath, conversationToken);
@@ -255,7 +256,7 @@ private boolean isBeingDownloaded() {
@SuppressLint("NewApi")
private void loadImageFromDisk(ImageView imageView) {
if (imageView != null && !mIsCancelled) {
- ApptentiveLog.v("ApptentiveAttachmentLoader loadImageFromDisk: " + uri);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader loadImageFromDisk: " + uri);
mDrawableLoaderTask = new ApptentiveDrawableLoaderTask(imageView, this);
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
@@ -276,7 +277,7 @@ private void loadAttachmentFromDisk(ImageView imageView) {
}
private void cancel() {
- ApptentiveLog.v("ApptentiveAttachmentLoader cancel requested for: " + uri);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader cancel requested for: " + uri);
mIsCancelled = true;
ArrayList duplicates = duplicateDownloads.get(uri);
@@ -377,7 +378,7 @@ private void loadDrawable(Bitmap d) {
}
private void loadDrawable(Bitmap d, boolean animate) {
- ApptentiveLog.v("ApptentiveAttachmentLoader loadDrawable");
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader loadDrawable");
ImageView imageView = getImageView();
if (imageView != null) {
if (loadingTaskCallback != null) {
@@ -389,7 +390,7 @@ private void loadDrawable(Bitmap d, boolean animate) {
// called when the download starts
@Override
public void onDownloadStart() {
- ApptentiveLog.v("ApptentiveAttachmentLoader onDownloadStarted");
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader onDownloadStarted");
ImageView imageView = getImageView();
if (imageView != null) {
if (loadingTaskCallback != null) {
@@ -401,7 +402,7 @@ public void onDownloadStart() {
// called when the download is in progress
@Override
public void onProgress(int progress) {
- ApptentiveLog.v("ApptentiveAttachmentLoader onProgress: " + progress);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader onProgress: " + progress);
ImageView imageView = getImageView();
if (imageView != null) {
if (loadingTaskCallback != null) {
@@ -412,7 +413,7 @@ public void onProgress(int progress) {
ArrayList duplicates = duplicateDownloads.get(uri);
if (duplicates != null) {
for (LoaderRequest dup : duplicates) {
- ApptentiveLog.v("ApptentiveAttachmentLoader onProgress (dup): " + progress);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader onProgress (dup): " + progress);
// update the progress on the duplicate downloads
if (dup != null && dup.getImageView() != null &&
dup.getImageView().getTag(DRAWABLE_DOWNLOAD_TAG) == dup) {
@@ -425,7 +426,7 @@ public void onProgress(int progress) {
// called when the download has completed
@Override
public void onDownloadComplete() {
- ApptentiveLog.v("ApptentiveAttachmentLoader onDownloadComplete: " + uri);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader onDownloadComplete: " + uri);
runningDownLoaderRequests.remove(this);
filesBeingDownloaded.remove(diskCacheFilePath);
@@ -446,7 +447,7 @@ public void onDownloadComplete() {
ArrayList duplicates = duplicateDownloads.get(uri);
if (duplicates != null) {
for (LoaderRequest dup : duplicates) {
- ApptentiveLog.v("ApptentiveAttachmentLoader onDownloadComplete (dup): " + dup.uri);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader onDownloadComplete (dup): " + dup.uri);
// load the image.
if (dup != null && dup.getImageView() != null &&
dup.getImageView().getTag(DRAWABLE_DOWNLOAD_TAG) == dup) {
@@ -472,7 +473,7 @@ public void onDownloadComplete() {
// called if there is an error with the download
@Override
public void onDownloadError() {
- ApptentiveLog.v("ApptentiveAttachmentLoader onDownloadError: " + uri);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader onDownloadError: " + uri);
runningDownLoaderRequests.remove(this);
filesBeingDownloaded.remove(diskCacheFilePath);
ImageView imageView = getImageView();
@@ -496,7 +497,7 @@ public void onDownloadError() {
duplicateDownloads.remove(uri);
}
for (LoaderRequest dup : duplicates) {
- ApptentiveLog.v("ApptentiveAttachmentLoader onDownloadError (dup): " + dup.uri);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader onDownloadError (dup): " + dup.uri);
// load the image.
if (dup != null && dup.getImageView() != null &&
dup.getImageView().getTag(DRAWABLE_DOWNLOAD_TAG) == dup) {
@@ -522,7 +523,7 @@ public void onDownloadError() {
@Override
public void onDownloadCancel() {
mIsCancelled = true;
- ApptentiveLog.v("ApptentiveAttachmentLoader onDownloadCancel: " + uri);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader onDownloadCancel: " + uri);
runningDownLoaderRequests.remove(this);
filesBeingDownloaded.remove(diskCacheFilePath);
@@ -557,7 +558,7 @@ public void onDownloadCancel() {
if (!queuedDownLoaderRequests.isEmpty()) {
LoaderRequest d = queuedDownLoaderRequests.remove(0);
- ApptentiveLog.v("ApptentiveAttachmentLoader starting DL of: " + d.getUrl());
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader starting DL of: " + d.getUrl());
d.doDownload();
}
}
@@ -565,7 +566,7 @@ public void onDownloadCancel() {
// called if the file is not found on the file system
@Override
public void notFound() {
- ApptentiveLog.v("ApptentiveAttachmentLoader notFound: " + uri);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader notFound: " + uri);
if (mIsCancelled) {
return;
}
@@ -593,7 +594,7 @@ public void notFound() {
int downloadIndex = indexOfDownloadWithDifferentURL();
while (queuedIndex != -1) {
queuedDownLoaderRequests.remove(queuedIndex);
- ApptentiveLog.v("ApptentiveAttachmentLoader notFound(Removing): " + uri);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader notFound(Removing): " + uri);
queuedIndex = indexOfQueuedDownloadWithDifferentURL();
}
if (downloadIndex != -1) {
@@ -601,16 +602,16 @@ public void notFound() {
ApptentiveDownloaderTask downloadTask = runningLoaderRequest.getDrawableDownloaderTask();
if (downloadTask != null) {
downloadTask.cancel(true);
- ApptentiveLog.v("ApptentiveAttachmentLoader notFound(Cancelling): " + uri);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader notFound(Cancelling): " + uri);
}
}
if (!(isBeingDownloaded() || isQueuedForDownload())) {
if (runningDownLoaderRequests.size() >= maxDownloads) {
- ApptentiveLog.v("ApptentiveAttachmentLoader notFound(Queuing): " + uri);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader notFound(Queuing): " + uri);
queuedDownLoaderRequests.add(this);
} else {
- ApptentiveLog.v("ApptentiveAttachmentLoader notFound(Downloading): " + uri);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader notFound(Downloading): " + uri);
doDownload();
}
}
@@ -622,7 +623,7 @@ public void loadBitmap(Bitmap b) {
bitmapMemoryCache.addObjectToCache(ImageMemoryCache.generateMemoryCacheEntryKey(uri, imageViewWidth, imageViewHeight), b);
ImageView imageView = getImageView();
if (imageView != null && this == imageView.getTag(DRAWABLE_DOWNLOAD_TAG)) {
- ApptentiveLog.v("ApptentiveAttachmentLoader loadDrawable(add to cache)");
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader loadDrawable(add to cache)");
loadDrawable(b);
imageView.setTag(DRAWABLE_DOWNLOAD_TAG, null);
}
@@ -631,7 +632,7 @@ public void loadBitmap(Bitmap b) {
@Override
public void onLoadError() {
- ApptentiveLog.v("ApptentiveAttachmentLoader onLoadError: " + uri);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader onLoadError: " + uri);
ImageView imageView = getImageView();
if (imageView != null && this == imageView.getTag(DRAWABLE_DOWNLOAD_TAG)) {
@@ -646,7 +647,7 @@ public void onLoadError() {
@Override
public void onLoadCancelled() {
- ApptentiveLog.v("ApptentiveAttachmentLoader onLoadCancelled: " + uri);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader onLoadCancelled: " + uri);
ImageView imageView = getImageView();
if (imageView != null && this == imageView.getTag(DRAWABLE_DOWNLOAD_TAG)) {
imageView.setTag(DRAWABLE_DOWNLOAD_TAG, null);
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/image/ImageGridViewAdapter.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/image/ImageGridViewAdapter.java
index 2ba064ead..1651e9cd4 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/util/image/ImageGridViewAdapter.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/image/ImageGridViewAdapter.java
@@ -32,6 +32,7 @@
import java.util.List;
import static com.apptentive.android.sdk.util.Util.guarded;
+import static com.apptentive.android.sdk.ApptentiveLogTag.UTIL;
public class ImageGridViewAdapter extends BaseAdapter {
@@ -485,7 +486,7 @@ public void onLoaded(ImageView view, int pos, Bitmap d) {
}
image.setImageResource(R.drawable.apptentive_generic_file_thumbnail);
if (downloadItems.contains(data.originalPath)) {
- ApptentiveLog.d("ApptentiveAttachmentLoader onLoaded callback");
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader onLoaded callback");
downloadItems.remove(data.originalPath);
Util.openFileAttachment(view.getContext(), data.originalPath, data.localCachePath, data.mimeType);
}
@@ -519,7 +520,7 @@ public void onDownloadProgress(int progress) {
} else if (progress >= 0) {
progressBarDownload.setVisibility(View.VISIBLE);
progressBarDownload.setProgress(progress);
- ApptentiveLog.d("ApptentiveAttachmentLoader progress callback: " + progress);
+ ApptentiveLog.v(UTIL, "ApptentiveAttachmentLoader progress callback: " + progress);
}
}
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/image/ImageUtil.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/image/ImageUtil.java
index f72fb7c14..bcf68bea7 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/util/image/ImageUtil.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/image/ImageUtil.java
@@ -33,6 +33,8 @@
import java.lang.ref.WeakReference;
import java.net.URL;
+import static com.apptentive.android.sdk.ApptentiveLogTag.UTIL;
+
public class ImageUtil {
private static final int MAX_SENT_IMAGE_EDGE = 1024;
@@ -122,14 +124,14 @@ private static Bitmap createLightweightScaledBitmap(String fileAbsolutePath, Uri
height = decodeBoundsOptions.outHeight;
}
- ApptentiveLog.v("Original bitmap dimensions: %d x %d", width, height);
+ ApptentiveLog.v(UTIL, "Original bitmap dimensions: %d x %d", width, height);
int sampleRatio = Math.min(width / minShrunkWidth, height / minShrunkHeight);
if (sampleRatio >= 2) {
options.inSampleSize = sampleRatio;
}
options.inScaled = false;
options.inJustDecodeBounds = false;
- ApptentiveLog.v("Bitmap sample size = %d", options.inSampleSize);
+ ApptentiveLog.v(UTIL, "Bitmap sample size = %d", options.inSampleSize);
Bitmap retImg = null;
if (bCreateFromUri && context != null) {
@@ -147,7 +149,7 @@ private static Bitmap createLightweightScaledBitmap(String fileAbsolutePath, Uri
}
- ApptentiveLog.d("Sampled bitmap size = %d X %d", options.outWidth, options.outHeight);
+ ApptentiveLog.v(UTIL, "Sampled bitmap size = %d X %d", options.outWidth, options.outHeight);
if ((orientation != 0 && orientation != -1) && retImg != null) {
Matrix matrix = new Matrix();
@@ -228,13 +230,13 @@ public synchronized static Bitmap createScaledBitmapFromLocalImageSource(String
if (ratio < 1.0f) { // Don't blow up small images, only shrink bigger ones.
int newWidth = (int) (ratio * width);
int newHeight = (int) (ratio * height);
- ApptentiveLog.v("Scaling image further down to %d x %d", newWidth, newHeight);
+ ApptentiveLog.v(UTIL, "Scaling image further down to %d x %d", newWidth, newHeight);
try {
outBitmap = Bitmap.createScaledBitmap(tempBitmap, newWidth, newHeight, true);
} catch (IllegalArgumentException e) {
throw new NullPointerException("Failed to create scaled bitmap");
}
- ApptentiveLog.d("Final bitmap dimensions: %d x %d", outBitmap.getWidth(), outBitmap.getHeight());
+ ApptentiveLog.v(UTIL, "Final bitmap dimensions: %d x %d", outBitmap.getWidth(), outBitmap.getHeight());
tempBitmap.recycle();
}
return outBitmap;
@@ -285,14 +287,14 @@ public static boolean createScaledDownImageCacheFile(String sourcePath, String c
// TODO: Is JPEG what we want here?
smaller.compress(Bitmap.CompressFormat.JPEG, 95, cos);
cos.flush();
- ApptentiveLog.d("Bitmap saved, size = " + (cos.getBytesWritten() / 1024) + "k");
+ ApptentiveLog.v(UTIL, "Bitmap saved, size = " + (cos.getBytesWritten() / 1024) + "k");
smaller.recycle();
System.gc();
} catch (FileNotFoundException e) {
- ApptentiveLog.e(e, "File not found while storing image.");
+ ApptentiveLog.e(UTIL, e, "File not found while storing image.");
return false;
} catch (Exception e) {
- ApptentiveLog.a(e, "Error storing image.");
+ ApptentiveLog.a(UTIL, e, "Error storing image.");
return false;
} finally {
Util.ensureClosed(cos);
@@ -318,11 +320,11 @@ public static boolean appendScaledDownImageToStream(String sourcePath, OutputStr
Bitmap smaller = ImageUtil.createScaledBitmapFromLocalImageSource(sourcePath, MAX_SENT_IMAGE_EDGE, MAX_SENT_IMAGE_EDGE, null, imageOrientation);
smaller.compress(Bitmap.CompressFormat.JPEG, 95, cos);
cos.flush();
- ApptentiveLog.v("Bitmap bytes appended, size = " + (cos.getBytesWritten() / 1024) + "k");
+ ApptentiveLog.v(UTIL, "Bitmap bytes appended, size = " + (cos.getBytesWritten() / 1024) + "k");
smaller.recycle();
return true;
} catch (Exception e) {
- ApptentiveLog.a(e, "Error storing image.");
+ ApptentiveLog.a(UTIL, e, "Error storing image.");
return false;
} finally {
Util.ensureClosed(cos);
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/util/task/ApptentiveDownloaderTask.java b/apptentive/src/main/java/com/apptentive/android/sdk/util/task/ApptentiveDownloaderTask.java
index d3466674f..510d17dba 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/util/task/ApptentiveDownloaderTask.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/util/task/ApptentiveDownloaderTask.java
@@ -29,6 +29,8 @@
import com.apptentive.android.sdk.util.Constants;
import com.apptentive.android.sdk.util.Util;
+import static com.apptentive.android.sdk.ApptentiveLogTag.UTIL;
+
public class ApptentiveDownloaderTask extends AsyncTask