diff --git a/.gitignore b/.gitignore
index cd6997439..65a64d0fe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,6 +24,7 @@ build/
.idea/vcs.xml
.idea/workspace.xml
.idea/libraries
+.idea/runConfigurations
projectFilesBackup/
# Eclipse
diff --git a/.idea/runConfigurations/Apptentive_Tests.xml b/.idea/runConfigurations/Apptentive_Tests.xml
deleted file mode 100644
index ea16eeb5a..000000000
--- a/.idea/runConfigurations/Apptentive_Tests.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..3e1cd0fb3
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,43 @@
+language: android
+env:
+ global:
+ - SNAPSHOT_REPOSITORY_USERNAME=travis
+ - SNAPSHOT_REPOSITORY_URL=http://54.183.158.246:8081/artifactory/apptentive-snapshots
+ - secure: pc2twMw60say0ASdXiJiRAD6tx9Qy82DIMw83qPijB2wyVHLpLbjptqBFyTYy4+JYthZ8xcB5Yretiv//AQS4wdDPsJNwOKUGXamm8IBx+1wnhG/R/ROz67Ibj4XWHIX24GaKN/MD8tCN9VPdeNEL1jysSEVxqqsvOGBsxitAyI=
+jdk:
+ - oraclejdk8
+before_cache:
+ - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
+ - rm -fr $HOME/.gradle/caches/*/plugin-resolution/
+cache:
+ directories:
+ - $HOME/.gradle/caches/
+ - $HOME/.gradle/wrapper/
+ - $HOME/.android/build-cache
+android:
+ components:
+ - tools
+ - tools
+ - platform-tools
+ - build-tools-25.0.3
+ - android-24
+ - android-25
+ - extra-google-google_play_services
+ - extra-google-m2repository
+ - addon-google_apis-google-25
+ - sys-img-armeabi-v7a-android-19
+install: true
+before_script:
+ - echo no | android create avd --force -n test -t android-19 --abi armeabi-v7a
+ - emulator -avd test -no-audio -no-window &
+ - android-wait-for-emulator
+ - adb shell input keyevent 82 &
+script:
+ - ./gradlew :apptentive:test -i
+after_script:
+ - if [ "$TRAVIS_BRANCH" = "develop" ]; then
+ ./gradlew :apptentive:uploadArchives;
+ fi
+notifications:
+ slack:
+ secure: HejMl0EUociwGu+5djx95snbS+m/Yw8DseQKCSqeyWvMQLrAy8bi9oa89mZvXnvjqSVY3kKRZgJncEkQdIe9c7xwgNA9QYLkc7UVbXqga291HMoNnWaIMewD2ervbzM4aBQAHnkDr+GsXgb7+1YdOktIn8dA7jdIuB90ar4So9U=
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5b28565f8..6ed158fa2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+# 2017-07-19 - 4.0.0
+
+#### Improvements
+
+* Added the ability to log customers in to different private conversations. Customers who are logged in will be able to see a private conversation that is distinct from the converations of other customers using the same app on the same device. When they log out, their data is protected with strong encryption. Logging back in unlocks their data again, and Apptentive resumes providing services to the customer.
+
# 2017-03-02 - 3.4.1
#### Improvements
diff --git a/README.md b/README.md
index 07323afc0..f14f06956 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|3.4.1|aar)
+##### Binary releases are hosted for Maven [here](http://search.maven.org/#artifactdetails|com.apptentive|apptentive-android|4.0.0|aar)
#### Reporting Bugs
diff --git a/apptentive/build.gradle b/apptentive/build.gradle
index 933d33e8c..80d4293c7 100644
--- a/apptentive/build.gradle
+++ b/apptentive/build.gradle
@@ -2,10 +2,6 @@ init()
apply plugin: 'com.android.library'
-repositories {
- jcenter()
-}
-
dependencies {
compile 'com.android.support:support-v4:24.2.1'
compile 'com.android.support:appcompat-v7:24.2.1'
@@ -24,14 +20,14 @@ dependencies {
}
android {
- compileSdkVersion 24
- buildToolsVersion '24.0.3'
+ compileSdkVersion 25
+ buildToolsVersion '25.0.3'
defaultConfig {
minSdkVersion 14
- targetSdkVersion 24
+ targetSdkVersion 25
// BUILD_NUMBER is provided by Jenkins. Default to 1 in dev builds.
- versionCode System.getenv("BUILD_NUMBER") as Integer ?: 1
+ versionCode System.getenv("BUILD_NUMBER") as Integer ?: System.getenv("TRAVIS_BUILD_NUMBER") as Integer ?: 1
versionName project.version
consumerProguardFiles 'consumer-proguard-rules.pro'
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@@ -72,14 +68,14 @@ def getReleaseRepositoryUrl() {
}
def getSnapshotRepositoryUrl() {
- return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL : ""
+ return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL : System.getenv("SNAPSHOT_REPOSITORY_URL")
}
def getRepositoryUsername() {
if (isReleaseBuild()) {
return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : ""
} else {
- return hasProperty('SNAPSHOT_REPOSITORY_USERNAME') ? SNAPSHOT_REPOSITORY_USERNAME : ""
+ return hasProperty('SNAPSHOT_REPOSITORY_USERNAME') ? SNAPSHOT_REPOSITORY_USERNAME : System.getenv("SNAPSHOT_REPOSITORY_USERNAME")
}
}
@@ -87,7 +83,7 @@ def getRepositoryPassword() {
if (isReleaseBuild()) {
return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : ""
} else {
- return hasProperty('SNAPSHOT_REPOSITORY_PASSWORD') ? SNAPSHOT_REPOSITORY_PASSWORD : ""
+ return hasProperty('SNAPSHOT_REPOSITORY_PASSWORD') ? SNAPSHOT_REPOSITORY_PASSWORD : System.getenv("SNAPSHOT_REPOSITORY_PASSWORD")
}
}
@@ -162,7 +158,7 @@ task androidJavadocs(type: Javadoc) {
classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
}
-androidJavadocs << {
+androidJavadocs.doLast {
println "Javadocs written to: " + destinationDir
}
diff --git a/apptentive/src/androidTest/assets/apptentive-v2 b/apptentive/src/androidTest/assets/apptentive-v2
new file mode 100644
index 000000000..51b0ff1be
Binary files /dev/null and b/apptentive/src/androidTest/assets/apptentive-v2 differ
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
new file mode 100644
index 000000000..f833e83ee
--- /dev/null
+++ b/apptentive/src/androidTest/java/com/apptentive/android/sdk/conversation/FileMessageStoreTest.java
@@ -0,0 +1,298 @@
+/*
+ * 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.conversation;
+
+import com.apptentive.android.sdk.ApptentiveInternal;
+import com.apptentive.android.sdk.ApptentiveInternalMock;
+import com.apptentive.android.sdk.TestCaseBase;
+import com.apptentive.android.sdk.model.ApptentiveMessage;
+import com.apptentive.android.sdk.model.CompoundMessage;
+import com.apptentive.android.sdk.util.StringUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
+import static com.apptentive.android.sdk.model.ApptentiveMessage.State;
+import static junit.framework.Assert.assertEquals;
+
+public class FileMessageStoreTest extends TestCaseBase {
+ private static final boolean READ = true;
+ private static final boolean UNREAD = false;
+
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ @Before
+ public void setUp() {
+ super.setUp();
+ ApptentiveInternal.setInstance(new ApptentiveInternalMock());
+ }
+
+ @After
+ public void tearDown() {
+ super.tearDown();
+ ApptentiveInternal.setInstance(null);
+ }
+
+ @Test
+ public void testAddingAndLoadingMessages() throws Exception {
+ File file = getTempFile();
+
+ // create a few messages and add them to the store
+ FileMessageStore store = new FileMessageStore(file);
+ store.addOrUpdateMessages(createMessage("1", State.sending, READ, 10.0));
+ store.addOrUpdateMessages(createMessage("2", State.sent, UNREAD, 20.0));
+ store.addOrUpdateMessages(createMessage("3", State.saved, READ, 30.0));
+
+ // reload store and check saved messages
+ store = new FileMessageStore(file);
+ addResult(store.getAllMessages());
+
+ assertResult(
+ "{'nonce':'1','client_created_at':'10','state':'sending','read':'true'}",
+ "{'nonce':'2','client_created_at':'20','state':'sent','read':'false'}",
+ "{'nonce':'3','client_created_at':'30','state':'saved','read':'true'}");
+
+ // reload the store again and add another message
+ store = new FileMessageStore(file);
+ store.addOrUpdateMessages(createMessage("4", State.sent, UNREAD, 40.0));
+ addResult(store.getAllMessages());
+
+ assertResult(
+ "{'nonce':'1','client_created_at':'10','state':'sending','read':'true'}",
+ "{'nonce':'2','client_created_at':'20','state':'sent','read':'false'}",
+ "{'nonce':'3','client_created_at':'30','state':'saved','read':'true'}",
+ "{'nonce':'4','client_created_at':'40','state':'sent','read':'false'}");
+ }
+
+ @Test
+ public void updateMessage() throws Exception {
+
+ File file = getTempFile();
+
+ // create a few messages and add them to the store
+ FileMessageStore store = new FileMessageStore(file);
+ store.addOrUpdateMessages(createMessage("1", State.sending, READ, 10.0));
+ store.addOrUpdateMessages(createMessage("2", State.sent, UNREAD, 20.0));
+ store.addOrUpdateMessages(createMessage("3", State.saved, READ, 30.0));
+
+ // reload the store and change a single message
+ store = new FileMessageStore(file);
+ store.updateMessage(createMessage("2", State.saved, READ, 40.0));
+ addResult(store.getAllMessages());
+
+ assertResult(
+ "{'nonce':'1','client_created_at':'10','state':'sending','read':'true'}",
+ "{'nonce':'2','client_created_at':'40','state':'saved','read':'true'}",
+ "{'nonce':'3','client_created_at':'30','state':'saved','read':'true'}");
+
+
+ // reload the store and check the stored messages
+ store = new FileMessageStore(file);
+ addResult(store.getAllMessages());
+
+ assertResult(
+ "{'nonce':'1','client_created_at':'10','state':'sending','read':'true'}",
+ "{'nonce':'2','client_created_at':'40','state':'saved','read':'true'}",
+ "{'nonce':'3','client_created_at':'30','state':'saved','read':'true'}");
+ }
+
+ @Test
+ public void getLastReceivedMessageId() throws Exception {
+ File file = getTempFile();
+
+ // create a few messages and add them to the store
+ FileMessageStore store = new FileMessageStore(file);
+ store.addOrUpdateMessages(createMessage("1", State.saved, READ, 10.0, "111"));
+ store.addOrUpdateMessages(createMessage("2", State.saved, UNREAD, 20.0, "222"));
+ store.addOrUpdateMessages(createMessage("3", State.sending, READ, 30.0, "333"));
+ store.addOrUpdateMessages(createMessage("4", State.sent, UNREAD, 40.0, "444"));
+
+ assertEquals("222", store.getLastReceivedMessageId());
+
+ // reload the store and check again
+ store = new FileMessageStore(file);
+ assertEquals("222", store.getLastReceivedMessageId());
+ }
+
+ @Test
+ public void getUnreadMessageCount() throws Exception {
+ File file = getTempFile();
+
+ // create a few messages and add them to the store
+ FileMessageStore store = new FileMessageStore(file);
+ store.addOrUpdateMessages(createMessage("1", State.sending, READ, 10.0));
+ store.addOrUpdateMessages(createMessage("2", State.sent, UNREAD, 20.0));
+ store.addOrUpdateMessages(createMessage("3", State.saved, READ, 30.0));
+ store.addOrUpdateMessages(createMessage("4", State.sending, UNREAD, 40.0));
+
+ assertEquals(2, store.getUnreadMessageCount());
+
+ // reload store and check saved messages
+ store = new FileMessageStore(file);
+ assertEquals(2, store.getUnreadMessageCount());
+ }
+
+ @Test
+ public void deleteAllMessages() throws Exception {
+ File file = getTempFile();
+
+ // create a few messages and add them to the store
+ FileMessageStore store = new FileMessageStore(file);
+ store.addOrUpdateMessages(createMessage("1", State.sending, READ, 10.0));
+ store.addOrUpdateMessages(createMessage("2", State.sent, UNREAD, 20.0));
+ store.addOrUpdateMessages(createMessage("3", State.saved, READ, 30.0));
+ store.addOrUpdateMessages(createMessage("4", State.sending, UNREAD, 40.0));
+
+ // delete all messages
+ store.deleteAllMessages();
+
+ // check stored messages
+ addResult(store.getAllMessages());
+ assertResult();
+
+ // reload the store and check for messages
+ store = new FileMessageStore(file);
+ addResult(store.getAllMessages());
+ assertResult();
+ }
+
+ @Test
+ public void deleteAllMessagesAfterReload() throws Exception {
+ File file = getTempFile();
+
+ // create a few messages and add them to the store
+ FileMessageStore store = new FileMessageStore(file);
+ store.addOrUpdateMessages(createMessage("1", State.sending, READ, 10.0));
+ store.addOrUpdateMessages(createMessage("2", State.sent, UNREAD, 20.0));
+ store.addOrUpdateMessages(createMessage("3", State.saved, READ, 30.0));
+ store.addOrUpdateMessages(createMessage("4", State.sending, UNREAD, 40.0));
+
+ // delete all messages
+ store.deleteAllMessages();
+
+ // reload the store and check for messages
+ store = new FileMessageStore(file);
+ addResult(store.getAllMessages());
+ assertResult();
+ }
+
+ @Test
+ public void deleteMessage() throws Exception {
+ File file = getTempFile();
+
+ // create a few messages and add them to the store
+ FileMessageStore store = new FileMessageStore(file);
+ store.addOrUpdateMessages(createMessage("1", State.sending, READ, 10.0));
+ store.addOrUpdateMessages(createMessage("2", State.sent, UNREAD, 20.0));
+ store.addOrUpdateMessages(createMessage("3", State.saved, READ, 30.0));
+ store.addOrUpdateMessages(createMessage("4", State.sending, UNREAD, 40.0));
+
+ store.deleteMessage("2");
+ store.deleteMessage("4");
+
+ addResult(store.getAllMessages());
+
+ assertResult(
+ "{'nonce':'1','client_created_at':'10','state':'sending','read':'true'}",
+ "{'nonce':'3','client_created_at':'30','state':'saved','read':'true'}");
+ }
+
+ @Test
+ public void deleteMessageAndReload() throws Exception {
+ File file = getTempFile();
+
+ // create a few messages and add them to the store
+ FileMessageStore store = new FileMessageStore(file);
+ store.addOrUpdateMessages(createMessage("1", State.sending, READ, 10.0));
+ store.addOrUpdateMessages(createMessage("2", State.sent, UNREAD, 20.0));
+ store.addOrUpdateMessages(createMessage("3", State.saved, READ, 30.0));
+ store.addOrUpdateMessages(createMessage("4", State.sending, UNREAD, 40.0));
+
+ store.deleteMessage("2");
+ store.deleteMessage("4");
+
+ // reload store
+ store = new FileMessageStore(file);
+ addResult(store.getAllMessages());
+
+ assertResult(
+ "{'nonce':'1','client_created_at':'10','state':'sending','read':'true'}",
+ "{'nonce':'3','client_created_at':'30','state':'saved','read':'true'}");
+
+ // delete more
+ store.deleteMessage("1");
+ addResult(store.getAllMessages());
+
+ assertResult("{'nonce':'3','client_created_at':'30','state':'saved','read':'true'}");
+
+ // reload store
+ store = new FileMessageStore(file);
+ addResult(store.getAllMessages());
+
+ assertResult("{'nonce':'3','client_created_at':'30','state':'saved','read':'true'}");
+ }
+
+ private ApptentiveMessage createMessage(String nonce, State state, boolean read, double clientCreatedAt) throws JSONException {
+ return createMessage(nonce, state, read, clientCreatedAt, UUID.randomUUID().toString());
+ }
+
+ private ApptentiveMessage createMessage(String nonce, State state, boolean read, double clientCreatedAt, String id) throws JSONException {
+ JSONObject object = new JSONObject();
+ object.put("nonce", nonce);
+ object.put("client_created_at", clientCreatedAt);
+ CompoundMessage message = new CompoundMessage(object.toString());
+ message.setId(id);
+ message.setState(state);
+ message.setRead(read);
+ message.setNonce(nonce);
+ return message;
+ }
+
+ private File getTempFile() throws IOException {
+ final File file = tempFolder.newFile();
+ file.delete(); // this file might exist
+ return file;
+ }
+
+ private void addResult(List messages) throws JSONException {
+ for (ApptentiveMessage message : messages) {
+ addResult(toString(message));
+ }
+ }
+
+ private String toString(ApptentiveMessage message) throws JSONException {
+ String result = "{";
+ final Iterator keys = message.getJsonObject().keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ if (key.equals("id")) { // 'id' is randomly generated each time (so don't test it)
+ continue;
+ }
+ if (key.equals("type")) { // it's always 'CompoundMessage'
+ continue;
+ }
+ result += StringUtils.format("'%s':'%s',", key, message.getJsonObject().get(key));
+ }
+
+ result += StringUtils.format("'state':'%s',", message.getState().name());
+ result += StringUtils.format("'read':'%s'", message.isRead());
+ result += "}";
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/apptentive/src/androidTest/java/com/apptentive/android/sdk/encryption/EncryptorTest.java b/apptentive/src/androidTest/java/com/apptentive/android/sdk/encryption/EncryptorTest.java
new file mode 100644
index 000000000..7cb9d1789
--- /dev/null
+++ b/apptentive/src/androidTest/java/com/apptentive/android/sdk/encryption/EncryptorTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.encryption;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Random;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class EncryptorTest {
+
+ private static final int TEST_DATA_SIZE = 8096;
+ private Encryptor encryptor;
+ private byte[] testData;
+
+ @Before
+ public void setupEncryptor() throws Exception {
+ // Generate a key and setup the crypto
+ KeyGenerator keyGen = KeyGenerator.getInstance("AES");
+ keyGen.init(256);
+ SecretKey secretKey = keyGen.generateKey();
+ encryptor = new Encryptor(secretKey.getEncoded());
+
+ // Set up the test data
+ testData = new byte[TEST_DATA_SIZE];
+ new Random().nextBytes(testData);
+ }
+
+ @Test
+ public void testRoundTripEncryption() throws Exception {
+ long start = System.currentTimeMillis();
+ byte[] cipherText = encryptor.encrypt(testData);
+ assertNotNull(cipherText);
+ byte[] plainText = encryptor.decrypt(cipherText);
+ long stop = System.currentTimeMillis();
+ System.out.println(String.format("Round trip encryption took: %dms", stop - start));
+ assertNotNull(plainText);
+ assertTrue(Arrays.equals(plainText, testData));
+ }
+}
\ 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
new file mode 100644
index 000000000..ff1b239a7
--- /dev/null
+++ b/apptentive/src/androidTest/java/com/apptentive/android/sdk/network/HttpRequestManagerTest.java
@@ -0,0 +1,327 @@
+package com.apptentive.android.sdk.network;
+
+import com.apptentive.android.sdk.TestCaseBase;
+import com.apptentive.android.sdk.network.MockHttpURLConnection.DefaultResponseHandler;
+import com.apptentive.android.sdk.util.threading.MockDispatchQueue;
+
+import junit.framework.Assert;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class HttpRequestManagerTest extends TestCaseBase {
+
+ private HttpRequestManager requestManager;
+ private MockDispatchQueue networkQueue;
+
+ @Before
+ public void setUp() {
+ super.setUp();
+
+ networkQueue = new MockDispatchQueue(false);
+ requestManager = new MockHttpRequestManager(networkQueue);
+ }
+
+ @After
+ public void tearDown() {
+ requestManager.cancelAll();
+ super.tearDown();
+ }
+
+ @Test
+ public void testStartRequest() {
+ startRequest(new MockHttpRequest("1"));
+ startRequest(new MockHttpRequest("2").setMockResponseCode(204));
+ startRequest(new MockHttpRequest("3").setMockResponseCode(500));
+ startRequest(new MockHttpRequest("4").setThrowsExceptionOnConnect(true));
+ startRequest(new MockHttpRequest("5").setThrowsExceptionOnDisconnect(true));
+ dispatchRequests();
+
+ assertResult(
+ "finished: 1",
+ "finished: 2",
+ "failed: 3 Unexpected response code: 500 (Internal Server Error)",
+ "failed: 4 Connection error",
+ "failed: 5 Disconnection error"
+ );
+ }
+
+ @Test
+ public void testRequestData() {
+ final String expected = "Some test data with Unicode chars 文字";
+
+ final AtomicBoolean finished = new AtomicBoolean(false);
+
+ HttpRequest request = new MockHttpRequest("request").setResponseData(expected);
+ request.addListener(new HttpRequest.Adapter() {
+ @Override
+ public void onFinish(HttpRequest request) {
+ Assert.assertEquals(expected, request.getResponseData());
+ finished.set(true);
+ }
+ });
+ requestManager.startRequest(request);
+ dispatchRequests();
+
+ Assert.assertTrue(finished.get());
+ }
+
+ @Test
+ public void testJsonRequestData() throws JSONException {
+ final JSONObject requestObject = new JSONObject();
+ requestObject.put("key1", "value1");
+ requestObject.put("key2", "value2");
+ requestObject.put("key3", "value3");
+
+ final JSONObject expected = new JSONObject();
+ expected.put("int", 10);
+ expected.put("string", "value");
+ expected.put("boolean", true);
+ expected.put("float", 3.14f);
+
+ JSONObject inner = new JSONObject();
+ inner.put("key", "value");
+ expected.put("inner", inner);
+
+ final AtomicBoolean finished = new AtomicBoolean(false);
+
+ HttpJsonRequest request = new MockHttpJsonRequest("request", requestObject).setMockResponseData(expected);
+ request.addListener(new HttpRequest.Adapter() {
+ @Override
+ public void onFinish(HttpJsonRequest request) {
+ Assert.assertEquals(expected.toString(), request.getResponseObject().toString());
+ finished.set(true);
+ }
+
+ @Override
+ public void onFail(HttpJsonRequest request, String reason) {
+ Assert.fail(reason);
+ }
+ });
+ requestManager.startRequest(request);
+ dispatchRequests();
+
+ Assert.assertTrue(finished.get());
+ }
+
+ @Test
+ public void testJsonRequestCorruptedData() throws JSONException {
+ final JSONObject requestObject = new JSONObject();
+ requestObject.put("key1", "value1");
+ requestObject.put("key2", "value2");
+ requestObject.put("key3", "value3");
+
+ String invalidJson = "{ key1 : value key2 : value2 }";
+
+ final AtomicBoolean finished = new AtomicBoolean(false);
+
+ HttpJsonRequest request = new MockHttpJsonRequest("request", requestObject).setMockResponseData(invalidJson);
+ request.addListener(new HttpRequest.Adapter() {
+ @Override
+ public void onFail(HttpJsonRequest request, String reason) {
+ finished.set(true);
+ }
+ });
+ requestManager.startRequest(request);
+ dispatchRequests();
+
+ Assert.assertTrue(finished.get());
+ }
+
+ @Test
+ public void testListener() {
+ requestManager.setListener(new HttpRequestManager.Listener() {
+ @Override
+ public void onRequestStart(HttpRequestManager manager, HttpRequest request) {
+ addResult("start: " + request);
+ }
+
+ @Override
+ public void onRequestFinish(HttpRequestManager manager, HttpRequest request) {
+ if (request.isSuccessful()) {
+ addResult("finish: " + request);
+ } else if (request.isCancelled()) {
+ addResult("cancel: " + request);
+ } else {
+ addResult("fail: " + request);
+ }
+ }
+
+ @Override
+ public void onRequestsCancel(HttpRequestManager manager) {
+ addResult("cancel all");
+ }
+ });
+
+
+ // start requests and let them finish
+ requestManager.startRequest(new MockHttpRequest("1"));
+ requestManager.startRequest(new MockHttpRequest("2").setMockResponseCode(500));
+ requestManager.startRequest(new MockHttpRequest("3").setThrowsExceptionOnConnect(true));
+ dispatchRequests();
+
+ assertResult(
+ "start: 1",
+ "start: 2",
+ "start: 3",
+ "finish: 1",
+ "fail: 2",
+ "fail: 3"
+ );
+
+ // start requests and cancel some
+ requestManager.startRequest(new MockHttpRequest("4"));
+ requestManager.startRequest(new MockHttpRequest("5")).cancel();
+ requestManager.startRequest(new MockHttpRequest("6"));
+ dispatchRequests();
+
+ assertResult(
+ "start: 4",
+ "start: 5",
+ "start: 6",
+ "finish: 4",
+ "cancel: 5",
+ "finish: 6"
+ );
+
+ // start requests and cancel them all
+ requestManager.startRequest(new MockHttpRequest("4"));
+ requestManager.startRequest(new MockHttpRequest("5"));
+ requestManager.startRequest(new MockHttpRequest("6"));
+ requestManager.cancelAll();
+ dispatchRequests();
+
+ assertResult(
+ "start: 4",
+ "start: 5",
+ "start: 6",
+ "cancel all",
+ "cancel: 4",
+ "cancel: 5",
+ "cancel: 6"
+ );
+ }
+
+ @Test
+ public void testFailedRetry() {
+ HttpRequestRetryPolicyDefault retryPolicy = new HttpRequestRetryPolicyDefault() {
+ @Override
+ public boolean shouldRetryRequest(int responseCode, int retryAttempt) {
+ return responseCode != -1 && super.shouldRetryRequest(responseCode, retryAttempt); // don't retry on an exception
+ }
+ };
+ retryPolicy.setMaxRetryCount(2);
+ retryPolicy.setRetryTimeoutMillis(0);
+
+ startRequest(new MockHttpRequest("1").setMockResponseCode(500).setRetryPolicy(retryPolicy));
+ startRequest(new MockHttpRequest("2").setMockResponseCode(400).setRetryPolicy(retryPolicy));
+ startRequest(new MockHttpRequest("3").setMockResponseCode(204).setRetryPolicy(retryPolicy));
+ startRequest(new MockHttpRequest("4").setThrowsExceptionOnConnect(true).setRetryPolicy(retryPolicy));
+ startRequest(new MockHttpRequest("5").setThrowsExceptionOnDisconnect(true).setRetryPolicy(retryPolicy));
+ dispatchRequests();
+
+ assertResult(
+ "failed: 2 Unexpected response code: 400 (Bad Request)",
+ "finished: 3",
+ "failed: 4 Connection error",
+ "failed: 5 Disconnection error",
+ "retried: 1",
+ "retried: 1",
+ "failed: 1 Unexpected response code: 500 (Internal Server Error)"
+ );
+ }
+
+ @Test
+ public void testSuccessfulRetry() {
+ HttpRequestRetryPolicyDefault retryPolicy = new HttpRequestRetryPolicyDefault() {
+ @Override
+ public boolean shouldRetryRequest(int responseCode, int retryAttempt) {
+ return responseCode != -1 && super.shouldRetryRequest(responseCode, retryAttempt); // don't retry on an exception
+ }
+ };
+ retryPolicy.setMaxRetryCount(3);
+ retryPolicy.setRetryTimeoutMillis(0);
+
+ // fail this request twice and then finish successfully
+ startRequest(new MockHttpRequest("1").setMockResponseHandler(new DefaultResponseHandler() {
+ int requestAttempts = 0;
+
+ @Override
+ public int getResponseCode() {
+ return requestAttempts++ < 2 ? 500 : 200;
+ }
+ }).setRetryPolicy(retryPolicy));
+
+ startRequest(new MockHttpRequest("2").setMockResponseCode(400).setRetryPolicy(retryPolicy));
+ startRequest(new MockHttpRequest("3").setMockResponseCode(204).setRetryPolicy(retryPolicy));
+ startRequest(new MockHttpRequest("4").setThrowsExceptionOnConnect(true).setRetryPolicy(retryPolicy));
+ startRequest(new MockHttpRequest("5").setThrowsExceptionOnDisconnect(true).setRetryPolicy(retryPolicy));
+ dispatchRequests();
+
+ assertResult(
+ "failed: 2 Unexpected response code: 400 (Bad Request)",
+ "finished: 3",
+ "failed: 4 Connection error",
+ "failed: 5 Disconnection error",
+ "retried: 1",
+ "retried: 1",
+ "finished: 1"
+ );
+ }
+
+ //region Helpers
+
+ private void startRequest(HttpRequest request) {
+ request.addListener(new HttpRequest.Listener() {
+ @Override
+ public void onFinish(MockHttpRequest request) {
+ addResult("finished: " + request);
+ }
+
+ @Override
+ public void onCancel(MockHttpRequest request) {
+ addResult("cancelled: " + request);
+ }
+
+ @Override
+ public void onFail(MockHttpRequest request, String reason) {
+ addResult("failed: " + request + " " + reason);
+ }
+ });
+
+ requestManager.startRequest(request);
+ }
+
+ private void dispatchRequests() {
+ networkQueue.dispatchTasks();
+ }
+
+ //endregion
+
+ //region Mock HttpRequestManager
+
+ private class MockHttpRequestManager extends HttpRequestManager {
+ MockHttpRequestManager(MockDispatchQueue networkQueue) {
+ super(networkQueue);
+ }
+
+ @Override
+ void dispatchRequest(HttpRequest request) {
+ if (request.retrying) {
+ addResult("retried: " + request);
+ }
+ super.dispatchRequest(request);
+ }
+
+ @Override
+ synchronized HttpRequest startRequest(HttpRequest request) {
+ request.setRequestManager(this);
+ return super.startRequest(request);
+ }
+ }
+}
\ No newline at end of file
diff --git a/apptentive/src/androidTest/java/com/apptentive/android/sdk/storage/ApptentiveDatabaseHelperTest.java b/apptentive/src/androidTest/java/com/apptentive/android/sdk/storage/ApptentiveDatabaseHelperTest.java
new file mode 100644
index 000000000..39ae07744
--- /dev/null
+++ b/apptentive/src/androidTest/java/com/apptentive/android/sdk/storage/ApptentiveDatabaseHelperTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.storage;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.apptentive.android.sdk.model.AppReleasePayload;
+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.model.SdkPayload;
+import com.apptentive.android.sdk.module.messagecenter.model.MessageFactory;
+
+import org.json.JSONException;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class ApptentiveDatabaseHelperTest {
+
+ @After
+ public void tearDown() throws Exception {
+ deleteDbFile(InstrumentationRegistry.getContext());
+ }
+
+ @Test
+ public void testMigration() throws Exception {
+ final Context context = InstrumentationRegistry.getContext();
+ replaceDbFile(context, "apptentive-v2");
+
+ Payload[] expectedPayloads = {
+ new AppReleasePayload("{\"type\":\"android\",\"version_name\":\"3.4.1\",\"identifier\":\"com.apptentive.dev\",\"version_code\":51,\"target_sdk_version\":\"24\",\"inheriting_styles\":true,\"overriding_styles\":true,\"debug\":true}"),
+ new SdkPayload("{\"version\":\"3.4.1\",\"platform\":\"Android\"}"),
+ new EventPayload("{\"nonce\":\"b9a91f27-87b4-4bd9-b9a0-5c605891824a\",\"client_created_at\":1.492737199856E9,\"client_created_at_utc_offset\":-25200,\"label\":\"com.apptentive#app#launch\"}"),
+ new DevicePayload("{\"device\":\"bullhead\",\"integration_config\":{},\"locale_country_code\":\"US\",\"carrier\":\"\",\"uuid\":\"6c0b74d07c064421\",\"build_type\":\"user\",\"cpu\":\"arm64-v8a\",\"os_build\":\"3687331\",\"manufacturer\":\"LGE\",\"radio_version\":\"M8994F-2.6.36.2.20\",\"os_name\":\"Android\",\"build_id\":\"N4F26T\",\"utc_offset\":\"-28800\",\"bootloader_version\":\"BHZ11h\",\"board\":\"bullhead\",\"os_api_level\":\"25\",\"current_carrier\":\"AT&T\",\"network_type\":\"LTE\",\"locale_raw\":\"en_US\",\"brand\":\"google\",\"os_version\":\"7.1.1\",\"product\":\"bullhead\",\"model\":\"Nexus 5X\",\"locale_language_code\":\"en\",\"custom_data\":{}}"),
+ new PersonPayload("{\"custom_data\":{}}"),
+ MessageFactory.fromJson("{\"nonce\":\"a68d606c-083a-4496-a5e0-f07bcdff52a4\",\"client_created_at\":1.492737257565E9,\"client_created_at_utc_offset\":-25200,\"type\":\"CompoundMessage\",\"body\":\"Test message\",\"text_only\":false}")
+ };
+ }
+
+ private static void replaceDbFile(Context context, String filename) throws IOException {
+ InputStream input = context.getAssets().open(filename);
+ try {
+ OutputStream output = new FileOutputStream(getDatabaseFile(context));
+ try {
+ byte[] buffer = new byte[1024];
+ int read;
+ while ((read = input.read(buffer)) != -1) {
+ output.write(buffer, 0, read);
+ }
+ } finally {
+ output.close();
+ }
+ } finally {
+ input.close();
+ }
+ }
+
+ private static void deleteDbFile(Context context) throws IOException {
+ getDatabaseFile(context).delete();
+ }
+
+ private static File getDatabaseFile(Context context) {
+ return context.getDatabasePath("apptentive");
+ }
+
+ class ApptentiveDatabaseMockHelper extends ApptentiveDatabaseHelper {
+ private final String SQL_QUERY_PAYLOAD_LIST =
+ "SELECT * FROM " + PayloadEntry.TABLE_NAME +
+ " ORDER BY " + PayloadEntry.COLUMN_PRIMARY_KEY;
+
+ ApptentiveDatabaseMockHelper(Context context) {
+ super(context);
+ }
+
+ List listPayloads(SQLiteDatabase db) throws JSONException {
+ Cursor cursor = null;
+ try {
+ cursor = db.rawQuery(SQL_QUERY_PAYLOAD_LIST, null);
+ List payloads = new ArrayList<>(cursor.getCount());
+
+ Payload payload;
+ while (cursor.moveToNext()) {
+ // TODO: get data
+ }
+
+ return payloads;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/apptentive/src/androidTest/java/com/apptentive/android/sdk/storage/EncryptedPayloadSenderTest.java b/apptentive/src/androidTest/java/com/apptentive/android/sdk/storage/EncryptedPayloadSenderTest.java
new file mode 100644
index 000000000..1d4fc4707
--- /dev/null
+++ b/apptentive/src/androidTest/java/com/apptentive/android/sdk/storage/EncryptedPayloadSenderTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.storage;
+
+import com.apptentive.android.sdk.TestCaseBase;
+import com.apptentive.android.sdk.encryption.Encryptor;
+import com.apptentive.android.sdk.model.EventPayload;
+
+import org.json.JSONObject;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class EncryptedPayloadSenderTest extends TestCaseBase {
+
+ private static final String AUTH_TOKEN = "auth_token";
+ private static final String ENCRYPTION_KEY = "5C5361D08DA7AD6CD70ACEB572D387BB713A312DE8CE6128B8A42F62A7B381DB";
+ private static final String EVENT_LABEL = "com.apptentive#app#launch";
+
+ @Test
+ public void testEncryptedPayload() throws Exception {
+
+ final EventPayload original = new EventPayload(EVENT_LABEL, "trigger");
+ original.setToken(AUTH_TOKEN);
+ original.setEncryptionKey(ENCRYPTION_KEY);
+
+ byte[] cipherText = original.renderData();
+
+ Encryptor encryptor = new Encryptor(ENCRYPTION_KEY);
+
+ try {
+ byte[] plainText = encryptor.decrypt(cipherText);
+ JSONObject result = new JSONObject(new String(plainText));
+ String label = result.getJSONObject("event").getString("label");
+ assertEquals(label, EVENT_LABEL);
+ } catch (Exception e) {
+ fail(e.getMessage());
+ }
+ }
+}
\ No newline at end of file
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 fb405b4e2..51d9ebec7 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
@@ -21,12 +21,14 @@ public class ConcurrentDispatchQueueTest extends TestCaseBase {
@Before
public void setUp() {
+ super.setUp();
dispatchQueue = DispatchQueue.createBackgroundQueue("Test Queue", DispatchQueueType.Concurrent);
}
@After
public void tearDown() {
dispatchQueue.stop();
+ super.tearDown();
}
@Test
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 b8dbd1ebc..b37af3b25 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
@@ -22,12 +22,14 @@ public class SerialDispatchQueueTest extends TestCaseBase {
@Before
public void setUp() {
+ super.setUp();
dispatchQueue = DispatchQueue.createBackgroundQueue("Test Queue", DispatchQueueType.Serial);
}
@After
public void tearDown() {
dispatchQueue.stop();
+ super.tearDown();
}
@Test
@@ -63,6 +65,7 @@ public void testStoppingDispatch() {
protected void execute() {
dispatchQueue.stop();
sleep(500);
+ dispatchQueue.stop();
addResult("task-1");
}
});
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 98e065672..814824b6a 100755
--- a/apptentive/src/main/java/com/apptentive/android/sdk/Apptentive.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/Apptentive.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, Apptentive, Inc. All Rights Reserved.
+ * 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.
*/
@@ -17,8 +17,9 @@
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
+import com.apptentive.android.sdk.conversation.Conversation;
import com.apptentive.android.sdk.model.CommerceExtendedData;
-import com.apptentive.android.sdk.model.CustomData;
+import com.apptentive.android.sdk.model.CompoundMessage;
import com.apptentive.android.sdk.model.ExtendedData;
import com.apptentive.android.sdk.model.LocationExtendedData;
import com.apptentive.android.sdk.model.StoredFile;
@@ -26,47 +27,53 @@
import com.apptentive.android.sdk.module.engagement.EngagementModule;
import com.apptentive.android.sdk.module.messagecenter.MessageManager;
import com.apptentive.android.sdk.module.messagecenter.UnreadMessagesListener;
-import com.apptentive.android.sdk.module.messagecenter.model.CompoundMessage;
import com.apptentive.android.sdk.module.metric.MetricModule;
import com.apptentive.android.sdk.module.rating.IRatingProvider;
import com.apptentive.android.sdk.module.survey.OnSurveyFinishedListener;
-import com.apptentive.android.sdk.storage.DeviceManager;
-import com.apptentive.android.sdk.storage.PersonManager;
+import com.apptentive.android.sdk.storage.Person;
import com.apptentive.android.sdk.util.Constants;
+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.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
+import java.io.Serializable;
import java.util.ArrayList;
import java.util.Map;
/**
- * This class contains the complete API for accessing Apptentive features from within your app.
+ * This class contains the complete public API for accessing Apptentive features from within your app.
*/
public class Apptentive {
-
/**
* Must be called from the {@link Application#onCreate()} method in the {@link Application} object defined in your app's manifest.
*
* @param application The {@link Application} object for this app.
*/
public static void register(Application application) {
- Apptentive.register(application, null);
+ register(application, null, null);
}
- public static void register(Application application, String apptentiveApiKey) {
- ApptentiveLog.i("Registering Apptentive.");
- ApptentiveInternal.createInstance(application, apptentiveApiKey);
- ApptentiveInternal.setLifeCycleCallback();
+ public static void register(Application application, String apptentiveKey, String apptentiveSignature) {
+ register(application, apptentiveKey, apptentiveSignature, null);
}
- // ****************************************************************************************
- // GLOBAL DATA METHODS
- // ****************************************************************************************
+ private static void register(Application application, String apptentiveKey, String apptentiveSignature, String serverUrl) {
+ try {
+ ApptentiveLog.i("Registering Apptentive.");
+ ApptentiveInternal.createInstance(application, apptentiveKey, apptentiveSignature, serverUrl);
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while registering Apptentive");
+ }
+ }
+
+ //region Global Data Methods
/**
* Sets the user's email address. This email address will be sent to the Apptentive server to allow out of app
@@ -78,8 +85,16 @@ public static void register(Application application, String apptentiveApiKey) {
* @param email The user's email address.
*/
public static void setPersonEmail(String email) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- PersonManager.storePersonEmail(email);
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ conversation.getPerson().setEmail(email);
+ conversation.schedulePersonUpdate();
+ }
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while setting person email");
}
}
@@ -90,8 +105,15 @@ public static void setPersonEmail(String email) {
* @return The person's email if set, else null.
*/
public static String getPersonEmail() {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- return PersonManager.loadPersonEmail();
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ return conversation.getPerson().getEmail();
+ }
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e("Exception while getting person email");
}
return null;
}
@@ -106,8 +128,16 @@ public static String getPersonEmail() {
* @param name The user's name.
*/
public static void setPersonName(String name) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- PersonManager.storePersonName(name);
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ conversation.getPerson().setName(name);
+ conversation.schedulePersonUpdate();
+ }
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e("Exception while setting person name");
}
}
@@ -118,34 +148,17 @@ public static void setPersonName(String name) {
* @return The person's name if set, else null.
*/
public static String getPersonName() {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- return PersonManager.loadPersonName();
- }
- return null;
- }
-
-
- /**
- *
Allows you to pass arbitrary string data to the server along with this device's info. This method will replace all
- * custom device data that you have set for this app. Calls to this method are idempotent.
- *
To add a single piece of custom device data, use {@link #addCustomDeviceData}
- *
To remove a single piece of custom device data, use {@link #removeCustomDeviceData}
- *
- * @param customDeviceData A Map of key/value pairs to send to the server.
- * @deprecated
- */
- public static void setCustomDeviceData(Map customDeviceData) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- try {
- CustomData customData = new CustomData();
- for (String key : customDeviceData.keySet()) {
- customData.put(key, customDeviceData.get(key));
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ return conversation.getPerson().getName();
}
- DeviceManager.storeCustomDeviceData(customData);
- } catch (JSONException e) {
- ApptentiveLog.w("Unable to set custom device data.", e);
}
+ } catch (Exception e) {
+ ApptentiveLog.e("Exception while getting person name");
}
+ return null;
}
/**
@@ -157,11 +170,19 @@ public static void setCustomDeviceData(Map customDeviceData) {
* @param value A String value.
*/
public static void addCustomDeviceData(String key, String value) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- if (value != null) {
- value = value.trim();
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ if (value != null) {
+ value = value.trim();
+ }
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ conversation.getDevice().getCustomData().put(key, value);
+ conversation.scheduleDeviceUpdate();
+ }
}
- ApptentiveInternal.getInstance().addCustomDeviceData(key, value);
+ } catch (Exception e) {
+ ApptentiveLog.e("Exception while adding custom device data");
}
}
@@ -174,8 +195,15 @@ public static void addCustomDeviceData(String key, String value) {
* @param value A Number value.
*/
public static void addCustomDeviceData(String key, Number value) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- ApptentiveInternal.getInstance().addCustomDeviceData(key, value);
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ conversation.getDevice().getCustomData().put(key, value);
+ }
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e("Exception while adding custom device data");
}
}
@@ -188,20 +216,41 @@ public static void addCustomDeviceData(String key, Number value) {
* @param value A Boolean value.
*/
public static void addCustomDeviceData(String key, Boolean value) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- ApptentiveInternal.getInstance().addCustomDeviceData(key, value);
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ conversation.getDevice().getCustomData().put(key, value);
+ }
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e("Exception while adding custom device data");
}
}
private static void addCustomDeviceData(String key, Version version) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- ApptentiveInternal.getInstance().addCustomDeviceData(key, version);
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ conversation.getDevice().getCustomData().put(key, version);
+ }
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e("Exception while adding custom device data");
}
}
private static void addCustomDeviceData(String key, DateTime dateTime) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- ApptentiveInternal.getInstance().addCustomDeviceData(key, dateTime);
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ conversation.getDevice().getCustomData().put(key, dateTime);
+ }
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e("Exception while adding custom device data");
}
}
@@ -211,36 +260,16 @@ private static void addCustomDeviceData(String key, DateTime dateTime) {
* @param key The key to remove.
*/
public static void removeCustomDeviceData(String key) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- CustomData customData = DeviceManager.loadCustomDeviceData();
- if (customData != null) {
- customData.remove(key);
- DeviceManager.storeCustomDeviceData(customData);
- }
- }
- }
-
- /**
- *
Allows you to pass arbitrary string data to the server along with this person's info. This method will replace all
- * custom person data that you have set for this app. Calls to this method are idempotent.
- *
To add a single piece of custom person data, use {@link #addCustomPersonData}
- *
To remove a single piece of custom person data, use {@link #removeCustomPersonData}
- *
- * @param customPersonData A Map of key/value pairs to send to the server.
- * @deprecated
- */
- public static void setCustomPersonData(Map customPersonData) {
- ApptentiveLog.w("Setting custom person data: %s", customPersonData.toString());
- if (ApptentiveInternal.isApptentiveRegistered()) {
- try {
- CustomData customData = new CustomData();
- for (String key : customPersonData.keySet()) {
- customData.put(key, customPersonData.get(key));
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ conversation.getDevice().getCustomData().remove(key);
+ conversation.scheduleDeviceUpdate();
}
- PersonManager.storeCustomPersonData(customData);
- } catch (JSONException e) {
- ApptentiveLog.e("Unable to set custom person data.", e);
}
+ } catch (Exception e) {
+ ApptentiveLog.e("Exception while removing custom device data");
}
}
@@ -253,11 +282,19 @@ public static void setCustomPersonData(Map customPersonData) {
* @param value A String value.
*/
public static void addCustomPersonData(String key, String value) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- if (value != null) {
- value = value.trim();
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ if (value != null) {
+ value = value.trim();
+ }
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ conversation.getPerson().getCustomData().put(key, value);
+ conversation.schedulePersonUpdate();
+ }
}
- ApptentiveInternal.getInstance().addCustomPersonData(key, value);
+ } catch (Exception e) {
+ ApptentiveLog.e("Exception while adding custom person data");
}
}
@@ -270,8 +307,15 @@ public static void addCustomPersonData(String key, String value) {
* @param value A Number value.
*/
public static void addCustomPersonData(String key, Number value) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- ApptentiveInternal.getInstance().addCustomPersonData(key, value);
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ conversation.getPerson().getCustomData().put(key, value);
+ }
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e("Exception while adding custom person data");
}
}
@@ -284,20 +328,44 @@ public static void addCustomPersonData(String key, Number value) {
* @param value A Boolean value.
*/
public static void addCustomPersonData(String key, Boolean value) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- ApptentiveInternal.getInstance().addCustomPersonData(key, value);
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ conversation.getPerson().getCustomData().put(key, value);
+ conversation.schedulePersonUpdate();
+ }
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e("Exception while adding custom person data");
}
}
private static void addCustomPersonData(String key, Version version) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- ApptentiveInternal.getInstance().addCustomPersonData(key, version);
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ conversation.getPerson().getCustomData().put(key, version);
+ conversation.schedulePersonUpdate();
+ }
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e("Exception while adding custom person data");
}
}
private static void addCustomPersonData(String key, DateTime dateTime) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- ApptentiveInternal.getInstance().addCustomPersonData(key, dateTime);
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ conversation.getPerson().getCustomData().remove(key);
+ conversation.schedulePersonUpdate();
+ }
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e("Exception while adding custom person data");
}
}
@@ -307,54 +375,27 @@ private static void addCustomPersonData(String key, DateTime dateTime) {
* @param key The key to remove.
*/
public static void removeCustomPersonData(String key) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- CustomData customData = PersonManager.loadCustomPersonData();
- if (customData != null) {
- customData.remove(key);
- PersonManager.storeCustomPersonData(customData);
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ conversation.getPerson().getCustomData().remove(key);
+ conversation.schedulePersonUpdate();
+ }
}
+ } catch (Exception e) {
+ ApptentiveLog.e("Exception while removing custom person data");
}
}
+ //endregion
- // ****************************************************************************************
- // THIRD PARTY INTEGRATIONS
- // ****************************************************************************************
+ //region Third Party Integrations
- private static final String INTEGRATION_APPTENTIVE_PUSH = "apptentive_push";
- private static final String INTEGRATION_PARSE = "parse";
- private static final String INTEGRATION_URBAN_AIRSHIP = "urban_airship";
- private static final String INTEGRATION_AWS_SNS = "aws_sns";
-
- private static final String INTEGRATION_PUSH_TOKEN = "token";
-
- private static void addIntegration(String integration, Map config) {
- if (integration == null || config == null) {
- return;
- }
- if (!ApptentiveInternal.isApptentiveRegistered()) {
- return;
- }
-
- CustomData integrationConfig = DeviceManager.loadIntegrationConfig();
- try {
- JSONObject configJson = null;
- if (!integrationConfig.isNull(integration)) {
- configJson = integrationConfig.getJSONObject(integration);
- } else {
- configJson = new JSONObject();
- integrationConfig.put(integration, configJson);
- }
- for (String key : config.keySet()) {
- configJson.put(key, config.get(key));
- }
- ApptentiveLog.d("Adding integration config: %s", config.toString());
- DeviceManager.storeIntegrationConfig(integrationConfig);
- ApptentiveInternal.getInstance().syncDevice();
- } catch (JSONException e) {
- ApptentiveLog.e("Error adding integration: %s, %s", e, integration, config.toString());
- }
- }
+ /**
+ * For internal use only.
+ */
+ public static final String INTEGRATION_PUSH_TOKEN = "token";
/**
* Call {@link #setPushNotificationIntegration(int, String)} with this value to allow Apptentive to send pushes
@@ -412,53 +453,42 @@ private static void addIntegration(String integration, Map confi
*
*
*/
- public static void setPushNotificationIntegration(int pushProvider, String token) {
+ public static void setPushNotificationIntegration(final int pushProvider, final String token) {
try {
+ // we only access the active conversation on the main thread to avoid concurrency issues
+ if (!DispatchQueue.isMainQueue()) {
+ DispatchQueue.mainQueue().dispatchAsync(new DispatchTask() {
+ @Override
+ protected void execute() {
+ setPushNotificationIntegration(pushProvider, token);
+ }
+ });
+ return;
+ }
+
if (!ApptentiveInternal.isApptentiveRegistered()) {
+ ApptentiveLog.w("Unable to set push notification integration: Apptentive instance is not initialized");
return;
}
- CustomData integrationConfig = getIntegrationConfigurationWithoutPushProviders();
- JSONObject pushObject = new JSONObject();
- pushObject.put(INTEGRATION_PUSH_TOKEN, token);
- switch (pushProvider) {
- case PUSH_PROVIDER_APPTENTIVE:
- integrationConfig.put(INTEGRATION_APPTENTIVE_PUSH, pushObject);
- break;
- case PUSH_PROVIDER_PARSE:
- integrationConfig.put(INTEGRATION_PARSE, pushObject);
- break;
- case PUSH_PROVIDER_URBAN_AIRSHIP:
- integrationConfig.put(INTEGRATION_URBAN_AIRSHIP, pushObject);
- break;
- case PUSH_PROVIDER_AMAZON_AWS_SNS:
- integrationConfig.put(INTEGRATION_AWS_SNS, pushObject);
- break;
- default:
- ApptentiveLog.e("Invalid pushProvider: %d", pushProvider);
- return;
- }
- DeviceManager.storeIntegrationConfig(integrationConfig);
- ApptentiveInternal.getInstance().syncDevice();
- } catch (JSONException e) {
- ApptentiveLog.e("Error setting push integration.", e);
- return;
+ // Store the push stuff globally
+ SharedPreferences prefs = ApptentiveInternal.getInstance().getGlobalSharedPrefs();
+ prefs.edit().putInt(Constants.PREF_KEY_PUSH_PROVIDER, pushProvider)
+ .putString(Constants.PREF_KEY_PUSH_TOKEN, token)
+ .apply();
+
+ // Also set it on the active Conversation, if there is one.
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ conversation.setPushIntegration(pushProvider, token);
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while setting push notification integration");
}
}
- private static CustomData getIntegrationConfigurationWithoutPushProviders() {
- CustomData integrationConfig = DeviceManager.loadIntegrationConfig();
- if (integrationConfig != null) {
- integrationConfig.remove(INTEGRATION_APPTENTIVE_PUSH);
- integrationConfig.remove(INTEGRATION_PARSE);
- integrationConfig.remove(INTEGRATION_URBAN_AIRSHIP);
- integrationConfig.remove(INTEGRATION_AWS_SNS);
- }
- return integrationConfig;
- }
+ //endregion
- // ****************************************************************************************
- // PUSH NOTIFICATIONS
- // ****************************************************************************************
+ //region Push Notifications
/**
* Determines whether this Intent is a push notification sent from Apptentive.
@@ -467,10 +497,15 @@ private static CustomData getIntegrationConfigurationWithoutPushProviders() {
* @return True if the Intent came from, and should be handled by Apptentive.
*/
public static boolean isApptentivePushNotification(Intent intent) {
- if (!ApptentiveInternal.checkRegistered()) {
- return false;
+ try {
+ if (!ApptentiveInternal.checkRegistered()) {
+ return false;
+ }
+ return ApptentiveInternal.getApptentivePushNotificationData(intent) != null;
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while checking for Apptentive push notification intent");
}
- return ApptentiveInternal.getApptentivePushNotificationData(intent) != null;
+ return false;
}
/**
@@ -481,10 +516,15 @@ public static boolean isApptentivePushNotification(Intent intent) {
* @return True if the push came from, and should be handled by Apptentive.
*/
public static boolean isApptentivePushNotification(Bundle bundle) {
- if (!ApptentiveInternal.checkRegistered()) {
- return false;
+ try {
+ if (!ApptentiveInternal.checkRegistered()) {
+ return false;
+ }
+ return ApptentiveInternal.getApptentivePushNotificationData(bundle) != null;
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while checking for Apptentive push notification bundle");
}
- return ApptentiveInternal.getApptentivePushNotificationData(bundle) != null;
+ return false;
}
/**
@@ -494,18 +534,22 @@ public static boolean isApptentivePushNotification(Bundle bundle) {
* @return True if the push came from, and should be handled by Apptentive.
*/
public static boolean isApptentivePushNotification(Map data) {
- if (!ApptentiveInternal.checkRegistered()) {
- return false;
+ try {
+ if (!ApptentiveInternal.checkRegistered()) {
+ return false;
+ }
+ return ApptentiveInternal.getApptentivePushNotificationData(data) != null;
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while checking for Apptentive push notification data");
}
- return ApptentiveInternal.getApptentivePushNotificationData(data) != null;
+ return false;
}
/**
*
Use this method in your push receiver to build a pending Intent when an Apptentive push
* notification is received. Pass the generated PendingIntent to
* {@link android.support.v4.app.NotificationCompat.Builder#setContentIntent} to allow Apptentive
- * to display Interactions such as Message Center. This method replaces the deprecated
- * {@link #setPendingPushNotification(Intent)}. Calling this method for a push {@link Intent} that did
+ * to display Interactions such as Message Center. Calling this method for a push {@link Intent} that did
* not come from Apptentive will return a null object. If you receive a null object, your app will
* need to handle this notification itself.
*
This is the method you will likely need if you integrated using:
@@ -520,19 +564,23 @@ public static boolean isApptentivePushNotification(Map data) {
* @return a valid {@link PendingIntent} to launch an Apptentive Interaction if the push data came from Apptentive, or null.
*/
public static PendingIntent buildPendingIntentFromPushNotification(@NonNull Intent intent) {
- if (!ApptentiveInternal.checkRegistered()) {
- return null;
+ try {
+ if (!ApptentiveInternal.checkRegistered()) {
+ return null;
+ }
+ String apptentivePushData = ApptentiveInternal.getApptentivePushNotificationData(intent);
+ return ApptentiveInternal.generatePendingIntentFromApptentivePushData(apptentivePushData);
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while building pending intent from push notification");
}
- String apptentivePushData = ApptentiveInternal.getApptentivePushNotificationData(intent);
- return ApptentiveInternal.generatePendingIntentFromApptentivePushData(apptentivePushData);
+ return null;
}
/**
*
Use this method in your push receiver to build a pending Intent when an Apptentive push
* notification is received. Pass the generated PendingIntent to
* {@link android.support.v4.app.NotificationCompat.Builder#setContentIntent} to allow Apptentive
- * to display Interactions such as Message Center. This method replaces the deprecated
- * {@link #setPendingPushNotification(Bundle)}. Calling this method for a push {@link Bundle} that
+ * to display Interactions such as Message Center. Calling this method for a push {@link Bundle} that
* did not come from Apptentive will return a null object. If you receive a null object, your app
* will need to handle this notification itself.
*
This is the method you will likely need if you integrated using:
@@ -545,19 +593,23 @@ public static PendingIntent buildPendingIntentFromPushNotification(@NonNull Inte
* @return a valid {@link PendingIntent} to launch an Apptentive Interaction if the push data came from Apptentive, or null.
*/
public static PendingIntent buildPendingIntentFromPushNotification(@NonNull Bundle bundle) {
- if (!ApptentiveInternal.checkRegistered()) {
- return null;
+ try {
+ if (!ApptentiveInternal.checkRegistered()) {
+ return null;
+ }
+ String apptentivePushData = ApptentiveInternal.getApptentivePushNotificationData(bundle);
+ return ApptentiveInternal.generatePendingIntentFromApptentivePushData(apptentivePushData);
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while building pending intent form a push notification");
}
- String apptentivePushData = ApptentiveInternal.getApptentivePushNotificationData(bundle);
- return ApptentiveInternal.generatePendingIntentFromApptentivePushData(apptentivePushData);
+ return null;
}
/**
*
Use this method in your push receiver to build a pending Intent when an Apptentive push
* notification is received. Pass the generated PendingIntent to
* {@link android.support.v4.app.NotificationCompat.Builder#setContentIntent} to allow Apptentive
- * to display Interactions such as Message Center. This method replaces the deprecated
- * {@link #setPendingPushNotification(Bundle)}. Calling this method for a push {@link Bundle} that
+ * to display Interactions such as Message Center. Calling this method for a push {@link Bundle} that
* did not come from Apptentive will return a null object. If you receive a null object, your app
* will need to handle this notification itself.
*
This is the method you will likely need if you integrated using:
@@ -571,11 +623,16 @@ public static PendingIntent buildPendingIntentFromPushNotification(@NonNull Bund
* @return a valid {@link PendingIntent} to launch an Apptentive Interaction if the push data came from Apptentive, or null.
*/
public static PendingIntent buildPendingIntentFromPushNotification(@NonNull Map data) {
- if (!ApptentiveInternal.checkRegistered()) {
- return null;
+ try {
+ if (!ApptentiveInternal.checkRegistered()) {
+ return null;
+ }
+ String apptentivePushData = ApptentiveInternal.getApptentivePushNotificationData(data);
+ return ApptentiveInternal.generatePendingIntentFromApptentivePushData(apptentivePushData);
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while building pending intent form a push notification");
}
- String apptentivePushData = ApptentiveInternal.getApptentivePushNotificationData(data);
- return ApptentiveInternal.generatePendingIntentFromApptentivePushData(apptentivePushData);
+ return null;
}
/**
@@ -623,31 +680,35 @@ public static String getBodyFromApptentivePush(Intent intent) {
* @return a String value, or null.
*/
public static String getTitleFromApptentivePush(Bundle bundle) {
- if (!ApptentiveInternal.checkRegistered()) {
- return null;
- }
- if (bundle == null) {
- return null;
- }
- if (bundle.containsKey(ApptentiveInternal.TITLE_DEFAULT)) {
- return bundle.getString(ApptentiveInternal.TITLE_DEFAULT);
- }
- if (bundle.containsKey(ApptentiveInternal.PUSH_EXTRA_KEY_PARSE)) {
- String parseDataString = bundle.getString(ApptentiveInternal.PUSH_EXTRA_KEY_PARSE);
- if (parseDataString != null) {
- try {
- JSONObject parseJson = new JSONObject(parseDataString);
- return parseJson.optString(ApptentiveInternal.TITLE_DEFAULT, null);
- } catch (JSONException e) {
- return null;
- }
+ try {
+ if (!ApptentiveInternal.checkRegistered()) {
+ return null;
}
- } else if (bundle.containsKey(ApptentiveInternal.PUSH_EXTRA_KEY_UA)) {
- Bundle uaPushBundle = bundle.getBundle(ApptentiveInternal.PUSH_EXTRA_KEY_UA);
- if (uaPushBundle == null) {
+ if (bundle == null) {
return null;
}
- return uaPushBundle.getString(ApptentiveInternal.TITLE_DEFAULT);
+ if (bundle.containsKey(ApptentiveInternal.TITLE_DEFAULT)) {
+ return bundle.getString(ApptentiveInternal.TITLE_DEFAULT);
+ }
+ if (bundle.containsKey(ApptentiveInternal.PUSH_EXTRA_KEY_PARSE)) {
+ String parseDataString = bundle.getString(ApptentiveInternal.PUSH_EXTRA_KEY_PARSE);
+ if (parseDataString != null) {
+ try {
+ JSONObject parseJson = new JSONObject(parseDataString);
+ return parseJson.optString(ApptentiveInternal.TITLE_DEFAULT, null);
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+ } else if (bundle.containsKey(ApptentiveInternal.PUSH_EXTRA_KEY_UA)) {
+ Bundle uaPushBundle = bundle.getBundle(ApptentiveInternal.PUSH_EXTRA_KEY_UA);
+ if (uaPushBundle == null) {
+ return null;
+ }
+ return uaPushBundle.getString(ApptentiveInternal.TITLE_DEFAULT);
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while getting title from Apptentive push");
}
return null;
}
@@ -661,33 +722,37 @@ public static String getTitleFromApptentivePush(Bundle bundle) {
* @return a String value, or null.
*/
public static String getBodyFromApptentivePush(Bundle bundle) {
- if (!ApptentiveInternal.checkRegistered()) {
- return null;
- }
- if (bundle == null) {
- return null;
- }
- if (bundle.containsKey(ApptentiveInternal.BODY_DEFAULT)) {
- return bundle.getString(ApptentiveInternal.BODY_DEFAULT);
- }
- if (bundle.containsKey(ApptentiveInternal.PUSH_EXTRA_KEY_PARSE)) {
- String parseDataString = bundle.getString(ApptentiveInternal.PUSH_EXTRA_KEY_PARSE);
- if (parseDataString != null) {
- try {
- JSONObject parseJson = new JSONObject(parseDataString);
- return parseJson.optString(ApptentiveInternal.BODY_PARSE, null);
- } catch (JSONException e) {
- return null;
- }
+ try {
+ if (!ApptentiveInternal.checkRegistered()) {
+ return null;
}
- } else if (bundle.containsKey(ApptentiveInternal.PUSH_EXTRA_KEY_UA)) {
- Bundle uaPushBundle = bundle.getBundle(ApptentiveInternal.PUSH_EXTRA_KEY_UA);
- if (uaPushBundle == null) {
+ if (bundle == null) {
return null;
}
- return uaPushBundle.getString(ApptentiveInternal.BODY_UA);
- } else if (bundle.containsKey(ApptentiveInternal.BODY_UA)) {
- return bundle.getString(ApptentiveInternal.BODY_UA);
+ if (bundle.containsKey(ApptentiveInternal.BODY_DEFAULT)) {
+ return bundle.getString(ApptentiveInternal.BODY_DEFAULT);
+ }
+ if (bundle.containsKey(ApptentiveInternal.PUSH_EXTRA_KEY_PARSE)) {
+ String parseDataString = bundle.getString(ApptentiveInternal.PUSH_EXTRA_KEY_PARSE);
+ if (parseDataString != null) {
+ try {
+ JSONObject parseJson = new JSONObject(parseDataString);
+ return parseJson.optString(ApptentiveInternal.BODY_PARSE, null);
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+ } else if (bundle.containsKey(ApptentiveInternal.PUSH_EXTRA_KEY_UA)) {
+ Bundle uaPushBundle = bundle.getBundle(ApptentiveInternal.PUSH_EXTRA_KEY_UA);
+ if (uaPushBundle == null) {
+ return null;
+ }
+ return uaPushBundle.getString(ApptentiveInternal.BODY_UA);
+ } else if (bundle.containsKey(ApptentiveInternal.BODY_UA)) {
+ return bundle.getString(ApptentiveInternal.BODY_UA);
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while getting body from Apptentive push");
}
return null;
}
@@ -702,13 +767,18 @@ public static String getBodyFromApptentivePush(Bundle bundle) {
* @return a String value, or null.
*/
public static String getTitleFromApptentivePush(Map data) {
- if (!ApptentiveInternal.checkRegistered()) {
- return null;
- }
- if (data == null) {
- return null;
+ try {
+ if (!ApptentiveInternal.checkRegistered()) {
+ return null;
+ }
+ if (data == null) {
+ return null;
+ }
+ return data.get(ApptentiveInternal.TITLE_DEFAULT);
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while getting title from Apptentive push");
}
- return data.get(ApptentiveInternal.TITLE_DEFAULT);
+ return null;
}
/**
@@ -721,110 +791,23 @@ public static String getTitleFromApptentivePush(Map data) {
* @return a String value, or null.
*/
public static String getBodyFromApptentivePush(Map data) {
- if (!ApptentiveInternal.checkRegistered()) {
- return null;
- }
- if (data == null) {
- return null;
- }
- return data.get(ApptentiveInternal.BODY_DEFAULT);
- }
-
- /**
- *
Saves Apptentive specific data from a push notification Intent. In your BroadcastReceiver, if the push notification
- * came from Apptentive, it will have data that needs to be saved before you launch your Activity. You must call this
- * method every time you get a push opened Intent, and before you launch your Activity. If the push
- * notification did not come from Apptentive, this method has no effect.
- *
Use this method when using Parse and Amazon SNS as push providers.
- *
- * @param intent The Intent that you received when the user opened a push notification.
- * @return true if the push data came from Apptentive.
- * @deprecated
- */
- @Deprecated
- public static boolean setPendingPushNotification(Intent intent) {
- if (!ApptentiveInternal.checkRegistered()) {
- return false;
- }
- String apptentive = ApptentiveInternal.getApptentivePushNotificationData(intent);
- if (apptentive != null) {
- return ApptentiveInternal.getInstance().setPendingPushNotification(apptentive);
- }
- return false;
- }
-
- /**
- * Saves off the data contained in a push notification sent to this device from Apptentive. Use
- * this method when a push notification is opened, and you only have access to a push data
- * Bundle containing an "apptentive" key. This will generally be used with direct Apptentive Push
- * notifications, or when using Urban Airship as a push provider. Calling this method for a push
- * that did not come from Apptentive has no effect.
- *
- * @param data A Bundle containing the GCM data object from the push notification.
- * @return true if the push data came from Apptentive.
- * @deprecated
- */
- @Deprecated
- public static boolean setPendingPushNotification(Bundle data) {
- if (!ApptentiveInternal.checkRegistered()) {
- return false;
- }
- String apptentive = ApptentiveInternal.getApptentivePushNotificationData(data);
- if (apptentive != null) {
- return ApptentiveInternal.getInstance().setPendingPushNotification(apptentive);
- }
- return false;
- }
-
- /**
- * Launches Apptentive features based on a push notification Intent. Before you call this, you
- * must call {@link #setPendingPushNotification(Intent)} or
- * {@link #setPendingPushNotification(Bundle)} in your Broadcast receiver when
- * a push notification is opened by the user. This method must be called from the Activity that
- * you launched from the BroadcastReceiver. This method will only handle Apptentive originated
- * push notifications, so you can and should call it any time your push notification launches an
- * Activity.
- *
- * @param context The context from which this method is called.
- * @return True if a call to this method resulted in Apptentive displaying a View.
- * @deprecated
- */
- @Deprecated
- public static boolean handleOpenedPushNotification(Context context) {
- if (!ApptentiveInternal.checkRegistered()) {
- return false;
- }
-
- SharedPreferences prefs = context.getSharedPreferences(Constants.PREF_NAME, Context.MODE_PRIVATE);
- String pushData = prefs.getString(Constants.PREF_KEY_PENDING_PUSH_NOTIFICATION, null);
- prefs.edit().remove(Constants.PREF_KEY_PENDING_PUSH_NOTIFICATION).apply(); // Remove our data so this won't run twice.
- if (pushData != null) {
- ApptentiveLog.i("Handling opened Apptentive push notification.");
- try {
- JSONObject pushJson = new JSONObject(pushData);
- ApptentiveInternal.PushAction action = ApptentiveInternal.PushAction.unknown;
- if (pushJson.has(ApptentiveInternal.PUSH_ACTION)) {
- action = ApptentiveInternal.PushAction.parse(pushJson.getString(ApptentiveInternal.PUSH_ACTION));
- }
- switch (action) {
- case pmc:
- Apptentive.showMessageCenter(context);
- return true;
- default:
- ApptentiveLog.v("Unknown Apptentive push notification action: \"%s\"", action.name());
- }
- } catch (JSONException e) {
- ApptentiveLog.w("Error parsing JSON from push notification.", e);
- MetricModule.sendError(e, "Parsing Push notification", pushData);
+ try {
+ if (!ApptentiveInternal.checkRegistered()) {
+ return null;
}
+ if (data == null) {
+ return null;
+ }
+ return data.get(ApptentiveInternal.BODY_DEFAULT);
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while getting body from Apptentive push");
}
- return false;
+ return null;
}
+ //endregion
- // ****************************************************************************************
- // RATINGS
- // ****************************************************************************************
+ //region Rating
/**
* Use this to choose where to send the user when they are prompted to rate the app. This should be the same place
@@ -834,8 +817,12 @@ public static boolean handleOpenedPushNotification(Context context) {
*/
public static void setRatingProvider(IRatingProvider ratingProvider) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- ApptentiveInternal.getInstance().setRatingProvider(ratingProvider);
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ ApptentiveInternal.getInstance().setRatingProvider(ratingProvider);
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while setting rating provider");
}
}
@@ -847,15 +834,18 @@ public static void setRatingProvider(IRatingProvider ratingProvider) {
* @param value A String
*/
public static void putRatingProviderArg(String key, String value) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- ApptentiveInternal.getInstance().putRatingProviderArg(key, value);
+ try {
+ if (ApptentiveInternal.isApptentiveRegistered()) {
+ ApptentiveInternal.getInstance().putRatingProviderArg(key, value);
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while putting rating provider arg");
}
}
- // ****************************************************************************************
- // MESSAGE CENTER
- // ****************************************************************************************
+ //endregion
+ //region Message Center
/**
* Opens the Apptentive Message Center UI Activity
@@ -882,11 +872,13 @@ public static boolean showMessageCenter(Context context) {
*/
public static boolean showMessageCenter(Context context, Map customData) {
try {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- return ApptentiveInternal.getInstance().showMessageCenterInternal(context, customData);
+ if (!ApptentiveInternal.isConversationActive()) {
+ ApptentiveLog.v(ApptentiveLogTag.MESSAGES, "Unable to show message center: no active conversation.");
+ return false;
}
+ return ApptentiveInternal.getInstance().showMessageCenterInternal(context, customData);
} catch (Exception e) {
- ApptentiveLog.w("Error starting Apptentive Activity.", e);
+ ApptentiveLog.w(e, "Error in Apptentive.showMessageCenter()");
MetricModule.sendError(e, null, null);
}
return false;
@@ -899,8 +891,15 @@ public static boolean showMessageCenter(Context context, Map cus
* @return true if a call to {@link #showMessageCenter(Context)} will display Message Center, else false.
*/
public static boolean canShowMessageCenter() {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- return ApptentiveInternal.getInstance().canShowMessageCenterInternal();
+ try {
+ if (!ApptentiveInternal.isConversationActive()) {
+ ApptentiveLog.v(ApptentiveLogTag.MESSAGES, "Unable to show message center: no active conversation.");
+ return false;
+ }
+ return ApptentiveInternal.getInstance().canShowMessageCenterInternal(ApptentiveInternal.getInstance().getConversation());
+ } catch (Exception e) {
+ ApptentiveLog.w(e, "Error in Apptentive.canShowMessageCenter()");
+ MetricModule.sendError(e, null, null);
}
return false;
}
@@ -917,8 +916,15 @@ public static boolean canShowMessageCenter() {
*/
@Deprecated
public static void setUnreadMessagesListener(UnreadMessagesListener listener) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
+ try {
+ if (!ApptentiveInternal.isConversationActive()) {
+ ApptentiveLog.v(ApptentiveLogTag.MESSAGES, "Unable to set unread messages listener: no active conversation.");
+ return;
+ }
ApptentiveInternal.getInstance().getMessageManager().setHostUnreadMessagesListener(listener);
+ } catch (Exception e) {
+ ApptentiveLog.w(e, "Error in Apptentive.setUnreadMessagesListener()");
+ MetricModule.sendError(e, null, null);
}
}
@@ -930,8 +936,18 @@ public static void setUnreadMessagesListener(UnreadMessagesListener listener) {
* allows us to keep a weak reference to avoid memory leaks.
*/
public static void addUnreadMessagesListener(UnreadMessagesListener listener) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- ApptentiveInternal.getInstance().getMessageManager().addHostUnreadMessagesListener(listener);
+ try {
+ if (!ApptentiveInternal.isConversationActive()) {
+ ApptentiveLog.v(ApptentiveLogTag.MESSAGES, "Unable to add unread messages listener: no active conversation.");
+ return;
+ }
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation != null) {
+ conversation.getMessageManager().addHostUnreadMessagesListener(listener);
+ }
+ } catch (Exception e) {
+ ApptentiveLog.w(e, "Error in Apptentive.addUnreadMessagesListener()");
+ MetricModule.sendError(e, null, null);
}
}
@@ -942,10 +958,14 @@ public static void addUnreadMessagesListener(UnreadMessagesListener listener) {
*/
public static int getUnreadMessageCount() {
try {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- return ApptentiveInternal.getInstance().getMessageManager().getUnreadMessageCount();
+ if (!ApptentiveInternal.isConversationActive()) {
+ ApptentiveLog.v(ApptentiveLogTag.MESSAGES, "Unable to get unread message count: no active conversation.");
+ return 0;
}
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ return conversation.getMessageManager().getUnreadMessageCount();
} catch (Exception e) {
+ ApptentiveLog.w(e, "Error in Apptentive.getUnreadMessageCount()");
MetricModule.sendError(e, null, null);
}
return 0;
@@ -958,22 +978,25 @@ public static int getUnreadMessageCount() {
* @param text The message you wish to send.
*/
public static void sendAttachmentText(String text) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
- try {
- CompoundMessage message = new CompoundMessage();
- message.setBody(text);
- message.setRead(true);
- message.setHidden(true);
- message.setSenderId(ApptentiveInternal.getInstance().getPersonId());
- message.setAssociatedFiles(null);
- MessageManager mgr = ApptentiveInternal.getInstance().getMessageManager();
- if (mgr != null) {
- mgr.sendMessage(message);
- }
- } catch (Exception e) {
- ApptentiveLog.w("Error sending attachment text.", e);
- MetricModule.sendError(e, null, null);
+ try {
+ if (!ApptentiveInternal.isConversationActive()) {
+ ApptentiveLog.w(ApptentiveLogTag.MESSAGES, "Can't send attachment: No active Conversation.");
+ return;
}
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ CompoundMessage message = new CompoundMessage();
+ message.setBody(text);
+ message.setRead(true);
+ message.setHidden(true);
+ message.setSenderId(conversation.getPerson().getId());
+ message.setAssociatedFiles(null);
+ MessageManager mgr = conversation.getMessageManager();
+ if (mgr != null) {
+ mgr.sendMessage(message);
+ }
+ } catch (Exception e) {
+ ApptentiveLog.w(e, "Error in Apptentive.sendAttachmentText(String)");
+ MetricModule.sendError(e, null, null);
}
}
@@ -986,16 +1009,20 @@ public static void sendAttachmentText(String text) {
*/
public static void sendAttachmentFile(String uri) {
try {
- if (TextUtils.isEmpty(uri) || !ApptentiveInternal.isApptentiveRegistered()) {
+ if (!ApptentiveInternal.isConversationActive()) {
+ ApptentiveLog.w(ApptentiveLogTag.MESSAGES, "Can't send attachment: No active Conversation.");
return;
}
-
+ if (TextUtils.isEmpty(uri)) {
+ return;
+ }
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
CompoundMessage message = new CompoundMessage();
// No body, just attachment
message.setBody(null);
message.setRead(true);
message.setHidden(true);
- message.setSenderId(ApptentiveInternal.getInstance().getPersonId());
+ message.setSenderId(conversation.getPerson().getId());
ArrayList attachmentStoredFiles = new ArrayList();
/* Make a local copy in the cache dir. By default the file name is "apptentive-api-file + nonce"
@@ -1031,9 +1058,8 @@ public static void sendAttachmentFile(String uri) {
if (mgr != null) {
mgr.sendMessage(message);
}
-
} catch (Exception e) {
- ApptentiveLog.w("Error sending attachment file.", e);
+ ApptentiveLog.w(e, "Error in Apptentive.sendAttachmentFile(String)");
MetricModule.sendError(e, null, null);
}
}
@@ -1047,7 +1073,11 @@ public static void sendAttachmentFile(String uri) {
* @param mimeType The mime type of the file.
*/
public static void sendAttachmentFile(byte[] content, String mimeType) {
- if (ApptentiveInternal.isApptentiveRegistered()) {
+ try {
+ if (!ApptentiveInternal.isConversationActive()) {
+ ApptentiveLog.i(ApptentiveLogTag.MESSAGES, "Can't send attachment: No active Conversation.");
+ return;
+ }
ByteArrayInputStream is = null;
try {
is = new ByteArrayInputStream(content);
@@ -1055,6 +1085,9 @@ public static void sendAttachmentFile(byte[] content, String mimeType) {
} finally {
Util.ensureClosed(is);
}
+ } catch (Exception e) {
+ ApptentiveLog.w(e, "Error in Apptentive.sendAttachmentFile(byte[], String)");
+ MetricModule.sendError(e, null, null);
}
}
@@ -1068,16 +1101,20 @@ public static void sendAttachmentFile(byte[] content, String mimeType) {
*/
public static void sendAttachmentFile(InputStream is, String mimeType) {
try {
- if (is == null || !ApptentiveInternal.isApptentiveRegistered()) {
+ if (!ApptentiveInternal.isConversationActive()) {
+ ApptentiveLog.w(ApptentiveLogTag.MESSAGES, "Can't send attachment: No active Conversation.");
return;
}
-
+ if (is == null) {
+ return;
+ }
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
CompoundMessage message = new CompoundMessage();
// No body, just attachment
message.setBody(null);
message.setRead(true);
message.setHidden(true);
- message.setSenderId(ApptentiveInternal.getInstance().getPersonId());
+ message.setSenderId(conversation.getPerson().getId());
ArrayList attachmentStoredFiles = new ArrayList();
String localFilePath = Util.generateCacheFilePathFromNonceOrPrefix(ApptentiveInternal.getInstance().getApplicationContext(), message.getNonce(), null);
@@ -1097,11 +1134,13 @@ public static void sendAttachmentFile(InputStream is, String mimeType) {
message.setAssociatedFiles(attachmentStoredFiles);
ApptentiveInternal.getInstance().getMessageManager().sendMessage(message);
} catch (Exception e) {
- ApptentiveLog.w("Error sending attachment file.", e);
+ ApptentiveLog.w(e, "Error in Apptentive.sendAttachmentFile(InputStream, String)");
MetricModule.sendError(e, null, null);
}
}
+ //endregion
+
/**
* This method takes a unique event string, stores a record of that event having been visited, determines
* if there is an interaction that is able to run for this event, and then runs it. If more than one interaction
@@ -1117,7 +1156,7 @@ public static void sendAttachmentFile(InputStream is, String mimeType) {
* @return true if the an interaction was shown, else false.
*/
public static synchronized boolean engage(Context context, String event) {
- return EngagementModule.engage(context, "local", "app", null, event, null, null, (ExtendedData[]) null);
+ return engage(context, event, null, (ExtendedData[]) null);
}
/**
@@ -1137,7 +1176,7 @@ public static synchronized boolean engage(Context context, String event) {
* @return true if the an interaction was shown, else false.
*/
public static synchronized boolean engage(Context context, String event, Map customData) {
- return EngagementModule.engage(context, "local", "app", null, event, null, customData, (ExtendedData[]) null);
+ return engage(context, event, customData, (ExtendedData[]) null);
}
/**
@@ -1161,7 +1200,30 @@ public static synchronized boolean engage(Context context, String event, Map customData, ExtendedData... extendedData) {
- return EngagementModule.engage(context, "local", "app", null, event, null, customData, extendedData);
+ try {
+ if (StringUtils.isNullOrEmpty(event)) {
+ ApptentiveLog.e("Unable to engage event: name is null or empty"); // TODO: throw an IllegalArgumentException instead?
+ return false;
+ }
+ if (context == null) {
+ ApptentiveLog.e("Unable to engage '%s' event: context is null", event); // TODO: throw an IllegalArgumentException instead?
+ return false;
+ }
+ if (!ApptentiveInternal.isApptentiveRegistered()) {
+ ApptentiveLog.e("Unable to engage '%s' event: Apptentive SDK is not initialized", event);
+ return false;
+ }
+ Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+ if (conversation == null) {
+ ApptentiveLog.w("Unable to engage '%s' event: no active conversation", event);
+ return false;
+ }
+
+ return EngagementModule.engage(context, conversation, "local", "app", null, event, null, customData, extendedData);
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while engaging '%s' event", event);
+ return false;
+ }
}
/**
@@ -1188,8 +1250,11 @@ public static synchronized boolean willShowInteraction(String event) {
*/
public static synchronized boolean canShowInteraction(String event) {
try {
- return EngagementModule.canShowInteraction("local", "app", event);
+ if (ApptentiveInternal.isConversationActive()) {
+ return EngagementModule.canShowInteraction(ApptentiveInternal.getInstance().getConversation(), "app", event, "local");
+ }
} catch (Exception e) {
+ ApptentiveLog.w(e, "Error in Apptentive.canShowInteraction()");
MetricModule.sendError(e, null, null);
}
return false;
@@ -1205,12 +1270,217 @@ public static synchronized boolean canShowInteraction(String event) {
* to call when the survey is finished.
*/
public static void setOnSurveyFinishedListener(OnSurveyFinishedListener listener) {
- ApptentiveInternal internal = ApptentiveInternal.getInstance();
- if (internal != null) {
- internal.setOnSurveyFinishedListener(listener);
+ try {
+ ApptentiveInternal internal = ApptentiveInternal.getInstance();
+ if (internal != null) {
+ internal.setOnSurveyFinishedListener(listener);
+ }
+ } catch (Exception e) {
+ ApptentiveLog.w(e, "Error in Apptentive.setOnSurveyFinishedListener()");
+ MetricModule.sendError(e, null, null);
+ }
+ }
+
+ //region Login/Logout
+
+ /**
+ * Starts login process asynchronously. This call returns immediately. Using this method requires
+ * you to implement JWT generation on your server. Please read about it in Apptentive's Android
+ * Integration Reference Guide.
+ *
+ * @param token A JWT signed by your server using the secret from your app's Apptentive settings.
+ * @param callback A LoginCallback, which will be called asynchronously when the login succeeds
+ * or fails.
+ */
+ public static void login(String token, LoginCallback callback) {
+ try {
+ if (token == null) {
+ if (callback != null) {
+ callback.onLoginFail("token is null");
+ }
+ return;
+ }
+
+ final ApptentiveInternal instance = ApptentiveInternal.getInstance();
+ if (instance == null) {
+ ApptentiveLog.e("Unable to login: Apptentive instance is not properly initialized");
+ if (callback != null) {
+ callback.onLoginFail("Apptentive instance is not properly initialized");
+ }
+ } else {
+ ApptentiveInternal.getInstance().login(token, callback);
+ }
+ } catch (Exception e) {
+ ApptentiveLog.w(e, "Error in Apptentive.login()");
+ MetricModule.sendError(e, null, null);
+ }
+ }
+
+ /**
+ * Callback interface login().
+ */
+ public interface LoginCallback {
+ /**
+ * Called when a login attempt has completed successfully.
+ */
+ void onLoginFinish();
+
+ /**
+ * Called when a login attempt has failed. May be called synchronously, for example, if your JWT
+ * is missing the "sub" claim.
+ *
+ * @param errorMessage failure cause message
+ */
+ void onLoginFail(String errorMessage);
+ }
+
+ public static void logout() {
+ try {
+ final ApptentiveInternal instance = ApptentiveInternal.getInstance();
+ if (instance == null) {
+ ApptentiveLog.e("Unable to logout: Apptentive instance is not properly initialized");
+ } else {
+ instance.logout();
+ }
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception in Apptentive.logout()");
+ MetricModule.sendError(e, null, null);
+ }
+ }
+
+ /**
+ * Registers your listener with Apptentive. This listener is stored with a WeakReference, which
+ * means that you must store a static reference to the listener as long as you want it to live.
+ * One possible way to do this is to implement this listener with your Application class, or store
+ * one on your Application.
+ *
+ * This listener will alert you to authentication failures, so that you can either recover from
+ * expired or revoked JWTs, or fix your authentication implementation.
+ * @param listener A listener that will be called when there is an authentication failure other
+ * for the current logged in conversation. If the failure is for another
+ * conversation, or there is no active conversation, the listener is not called.
+ */
+ public static void setAuthenticationFailedListener(AuthenticationFailedListener listener) {
+ try {
+ if (!ApptentiveInternal.checkRegistered()) {
+ return;
+ }
+ ApptentiveInternal.getInstance().setAuthenticationFailedListener(listener);
+ } catch (Exception e) {
+ ApptentiveLog.w(e, "Error in Apptentive.setUnreadMessagesListener()");
+ MetricModule.sendError(e, null, null);
+ }
+ }
+
+ public static void clearAuthenticationFailedListener() {
+ try {
+ if (!ApptentiveInternal.checkRegistered()) {
+ return;
+ }
+ ApptentiveInternal.getInstance().setAuthenticationFailedListener(null);
+ } catch (Exception e) {
+ ApptentiveLog.w(e, "Error in Apptentive.clearUnreadMessagesListener()");
+ MetricModule.sendError(e, null, null);
+ }
+ }
+
+ /**
+ * A Listener you can register globally for the app, that will be called when requests other than
+ * login fail for the active conversation. This includes failure to send queued data to
+ * Apptentive, and failure to fetch app configuration from Apptentive.
+ */
+ public interface AuthenticationFailedListener {
+ void onAuthenticationFailed(AuthenticationFailedReason reason);
+ }
+
+ /**
+ * A list of error codes you will encounter when a JWT failure for logged in conversations occurs.
+ */
+ public enum AuthenticationFailedReason {
+ /**
+ * This should not happen.
+ */
+ UNKNOWN,
+ /**
+ * Currently only the HS512 signature algorithm is supported.
+ */
+ INVALID_ALGORITHM,
+ /**
+ * The JWT structure is constructed improperly (missing a part, etc.)
+ */
+ MALFORMED_TOKEN,
+ /**
+ * The token is not signed properly, or can't be decoded.
+ */
+ INVALID_TOKEN,
+ /**
+ * There is no "sub" property in the JWT claims. The "sub" is required, and should be an
+ * immutable, unique id for your user.
+ */
+ MISSING_SUB_CLAIM,
+ /**
+ * The JWT "sub" claim does not match the one previously registered to the internal Apptentive
+ * conversation. Internal use only.
+ */
+ MISMATCHED_SUB_CLAIM,
+ /**
+ * Internal use only.
+ */
+ INVALID_SUB_CLAIM,
+ /**
+ * The expiration "exp" claim is expired. The "exp" claim is a UNIX timestamp in milliseconds.
+ * The JWT will receive this authentication failure when the "exp" time has elapsed.
+ */
+ EXPIRED_TOKEN,
+ /**
+ * The JWT has been revoked. This happens after a successful logout. In such cases, you will
+ * need a new JWT to login.
+ */
+ REVOKED_TOKEN,
+ /**
+ * The Apptentive Key field was not specified during registration. You can get this from your app's Apptentive
+ * settings.
+ */
+ MISSING_APP_KEY,
+ /**
+ * The Apptentive Signature field was not specified during registration. You can get this from your app's Apptentive
+ * settings.
+ */
+ MISSING_APP_SIGNATURE,
+ /**
+ * The Apptentive Key and Apptentive Signature fields do not match. Make sure you got them from
+ * the same app's Apptentive settings page.
+ */
+ INVALID_KEY_SIGNATURE_PAIR;
+
+ private String error;
+
+ public String error() {
+ return error;
+ }
+
+ public static AuthenticationFailedReason parse(String errorType, String error) {
+ try {
+ AuthenticationFailedReason ret = AuthenticationFailedReason.valueOf(errorType);
+ ret.error = error;
+ return ret;
+ } catch (Exception e) {
+ ApptentiveLog.w("Error parsing unknown Apptentive.AuthenticationFailedReason: %s", errorType);
+ }
+ return UNKNOWN;
+ }
+
+ @Override
+ public String toString() {
+ return "AuthenticationFailedReason{" +
+ "error='" + error + '\'' +
+ "errorType='" + name() + '\'' +
+ '}';
}
}
+ //endregion
+
/**
*
This type represents a semantic version. It can be initialized
* with a string or a long, and there is no limit to the number of parts your semantic version can
@@ -1236,29 +1506,28 @@ public static void setOnSurveyFinishedListener(OnSurveyFinishedListener listener
*
FF01
*
*/
- public static class Version extends JSONObject implements Comparable {
+ public static class Version implements Serializable, Comparable {
+
+ private static final long serialVersionUID = 1L;
+
public static final String KEY_TYPE = "_type";
public static final String TYPE = "version";
+ private String version;
+
public Version() {
}
- public Version(String json) throws JSONException {
- super(json);
+ public Version(JSONObject json) throws JSONException {
+ this.version = json.optString(TYPE, null);
}
public Version(long version) {
- super();
- setVersion(version);
+ this.version = Long.toString(version);
}
public void setVersion(String version) {
- try {
- put(KEY_TYPE, TYPE);
- put(TYPE, version);
- } catch (JSONException e) {
- ApptentiveLog.e("Error creating Apptentive.Version.", e);
- }
+ this.version = version;
}
public void setVersion(long version) {
@@ -1266,7 +1535,17 @@ public void setVersion(long version) {
}
public String getVersion() {
- return optString(TYPE, null);
+ return version;
+ }
+
+ public void toJsonObject() {
+ JSONObject ret = new JSONObject();
+ try {
+ ret.put(KEY_TYPE, TYPE);
+ ret.put(TYPE, version);
+ } catch (JSONException e) {
+ ApptentiveLog.e(e, "Error creating Apptentive.Version.");
+ }
}
@Override
@@ -1313,31 +1592,38 @@ public String toString() {
}
}
- public static class DateTime extends JSONObject implements Comparable {
+ public static class DateTime implements Serializable, Comparable {
public static final String KEY_TYPE = "_type";
public static final String TYPE = "datetime";
public static final String SEC = "sec";
- public DateTime(String json) throws JSONException {
- super(json);
+ private String sec;
+
+ public DateTime(JSONObject json) throws JSONException {
+ this.sec = json.optString(SEC);
}
public DateTime(double dateTime) {
- super();
setDateTime(dateTime);
}
public void setDateTime(double dateTime) {
- try {
- put(KEY_TYPE, TYPE);
- put(SEC, dateTime);
- } catch (JSONException e) {
- ApptentiveLog.e("Error creating Apptentive.DateTime.", e);
- }
+ sec = String.valueOf(dateTime);
}
public double getDateTime() {
- return optDouble(SEC);
+ return Double.valueOf(sec);
+ }
+
+ public JSONObject toJSONObject() {
+ JSONObject ret = new JSONObject();
+ try {
+ ret.put(KEY_TYPE, TYPE);
+ ret.put(SEC, sec);
+ } catch (JSONException e) {
+ ApptentiveLog.e(e, "Error creating Apptentive.DateTime.");
+ }
+ return ret;
}
@Override
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveBaseActivity.java b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveBaseActivity.java
index 1e31ea496..5a84645fe 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveBaseActivity.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveBaseActivity.java
@@ -8,10 +8,10 @@
import com.apptentive.android.sdk.notifications.ApptentiveNotificationCenter;
import com.apptentive.android.sdk.notifications.ApptentiveNotificationObserver;
-import static com.apptentive.android.sdk.ApptentiveInternal.NOTIFICATION_INTERACTIONS_SHOULD_DISMISS;
+import static com.apptentive.android.sdk.ApptentiveNotifications.*;
/** A base class for any SDK activity */
-public class ApptentiveBaseActivity extends AppCompatActivity implements ApptentiveNotificationObserver {
+public abstract class ApptentiveBaseActivity extends AppCompatActivity implements ApptentiveNotificationObserver {
//region Activity life cycle
@@ -31,11 +31,13 @@ protected void onDestroy() {
//region Notifications
- private void registerNotifications() {
- ApptentiveNotificationCenter.defaultCenter().addObserver(NOTIFICATION_INTERACTIONS_SHOULD_DISMISS, this);
+ protected void registerNotifications() {
+ ApptentiveNotificationCenter.defaultCenter()
+ .addObserver(NOTIFICATION_INTERACTIONS_SHOULD_DISMISS, this)
+ .addObserver(NOTIFICATION_CONVERSATION_STATE_DID_CHANGE, this);
}
- private void unregisterNotification() {
+ protected void unregisterNotification() {
ApptentiveNotificationCenter.defaultCenter().removeObserver(this);
}
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 830257903..be0396373 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInternal.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInternal.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, Apptentive, Inc. All Rights Reserved.
+ * 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.
*/
@@ -19,25 +19,19 @@
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
-import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
-import android.provider.Settings;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
-import com.apptentive.android.sdk.comm.ApptentiveClient;
-import com.apptentive.android.sdk.comm.ApptentiveHttpResponse;
+import com.apptentive.android.sdk.Apptentive.LoginCallback;
+import com.apptentive.android.sdk.comm.ApptentiveHttpClient;
+import com.apptentive.android.sdk.conversation.Conversation;
+import com.apptentive.android.sdk.conversation.ConversationManager;
import com.apptentive.android.sdk.lifecycle.ApptentiveActivityLifecycleCallbacks;
-import com.apptentive.android.sdk.model.AppRelease;
-import com.apptentive.android.sdk.model.CodePointStore;
import com.apptentive.android.sdk.model.Configuration;
-import com.apptentive.android.sdk.model.ConversationTokenRequest;
-import com.apptentive.android.sdk.model.CustomData;
-import com.apptentive.android.sdk.model.Device;
-import com.apptentive.android.sdk.model.Event;
-import com.apptentive.android.sdk.model.Person;
-import com.apptentive.android.sdk.model.Sdk;
+import com.apptentive.android.sdk.model.EventPayload;
+import com.apptentive.android.sdk.model.LogoutPayload;
import com.apptentive.android.sdk.module.engagement.EngagementModule;
import com.apptentive.android.sdk.module.engagement.interaction.InteractionManager;
import com.apptentive.android.sdk.module.engagement.interaction.model.MessageCenterInteraction;
@@ -46,16 +40,17 @@
import com.apptentive.android.sdk.module.rating.IRatingProvider;
import com.apptentive.android.sdk.module.rating.impl.GooglePlayRatingProvider;
import com.apptentive.android.sdk.module.survey.OnSurveyFinishedListener;
+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.storage.AppRelease;
import com.apptentive.android.sdk.storage.AppReleaseManager;
import com.apptentive.android.sdk.storage.ApptentiveTaskManager;
-import com.apptentive.android.sdk.storage.DeviceManager;
-import com.apptentive.android.sdk.storage.PayloadSendWorker;
-import com.apptentive.android.sdk.storage.PersonManager;
+import com.apptentive.android.sdk.storage.Sdk;
import com.apptentive.android.sdk.storage.SdkManager;
-import com.apptentive.android.sdk.storage.VersionHistoryEntry;
-import com.apptentive.android.sdk.storage.VersionHistoryStore;
+import com.apptentive.android.sdk.storage.VersionHistoryItem;
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;
@@ -69,60 +64,66 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.atomic.AtomicBoolean;
+
+import static com.apptentive.android.sdk.ApptentiveLogTag.CONVERSATION;
+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_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_CONVERSATION_WILL_LOGOUT;
+import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_INTERACTIONS_FETCHED;
+import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_INTERACTIONS_SHOULD_DISMISS;
+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_CONVERSATION;
+import static com.apptentive.android.sdk.ApptentiveNotifications.NOTIFICATION_KEY_CONVERSATION_ID;
+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;
/**
* This class contains only internal methods. These methods should not be access directly by the host app.
*/
-public class ApptentiveInternal {
+public class ApptentiveInternal implements ApptentiveNotificationObserver {
- /**
- * Sent if user requested to close all interactions.
- */
- public static final String NOTIFICATION_INTERACTIONS_SHOULD_DISMISS = "NOTIFICATION_INTERACTIONS_SHOULD_DISMISS";
+ private final ApptentiveTaskManager taskManager;
- static AtomicBoolean isApptentiveInitialized = new AtomicBoolean(false);
- InteractionManager interactionManager;
- MessageManager messageManager;
- PayloadSendWorker payloadWorker;
- ApptentiveTaskManager taskManager;
- CodePointStore codePointStore;
- ApptentiveActivityLifecycleCallbacks lifecycleCallbacks;
+ private final ApptentiveActivityLifecycleCallbacks lifecycleCallbacks;
+ private final ApptentiveHttpClient apptentiveHttpClient;
+ private final ConversationManager conversationManager;
// These variables are initialized in Apptentive.register(), and so they are freely thereafter. If they are unexpectedly null, then if means the host app did not register Apptentive.
- Context appContext;
- int currentVersionCode;
- String currentVersionName;
-
- boolean appIsInForeground;
- boolean isAppDebuggable;
- SharedPreferences prefs;
- String apiKey;
- String conversationToken;
- String conversationId;
- String personId;
- String androidId;
- String appPackageName;
+ private final Context appContext;
+
+ // We keep a readonly reference to AppRelease object since it won't change at runtime
+ private final AppRelease appRelease;
+
+ private boolean appIsInForeground;
+ private final SharedPreferences globalSharedPrefs;
+ private final String apptentiveKey;
+ private final String apptentiveSignature;
+ private String serverUrl;
+ private String androidId; // FIXME: remove this field (never used)
+ private String appPackageName;
// toolbar theme specified in R.attr.apptentiveToolbarTheme
- Resources.Theme apptentiveToolbarTheme;
+ private Resources.Theme apptentiveToolbarTheme;
// app default appcompat theme res id, if specified in app AndroidManifest
- int appDefaultAppCompatThemeId;
+ private int appDefaultAppCompatThemeId;
- int statusBarColorDefault;
- String defaultAppDisplayName = "this app";
+ private int statusBarColorDefault;
+ private String defaultAppDisplayName = "this app";
// booleans to prevent starting multiple fetching asyncTasks simultaneously
- AtomicBoolean isConversationTokenFetchPending = new AtomicBoolean(false);
- AtomicBoolean isConfigurationFetchPending = new AtomicBoolean(false);
- IRatingProvider ratingProvider;
- Map ratingProviderArgs;
- WeakReference onSurveyFinishedListener;
+ private IRatingProvider ratingProvider;
+ private Map ratingProviderArgs;
+ private WeakReference onSurveyFinishedListener;
- final LinkedBlockingQueue interactionUpdateListeners = new LinkedBlockingQueue();
+ private final LinkedBlockingQueue interactionUpdateListeners = new LinkedBlockingQueue();
- ExecutorService cachedExecutor;
+ private WeakReference authenticationFailedListenerRef = null;
// Holds reference to the current foreground activity of the host app
private WeakReference currentTaskStackTopActivity;
@@ -130,9 +131,10 @@ public class ApptentiveInternal {
// Used for temporarily holding customData that needs to be sent on the next message the consumer sends.
private Map customData;
- public static final String PUSH_ACTION = "action";
+ private static final String PUSH_ACTION = "action";
+ private static final String PUSH_CONVERSATION_ID = "conversation_id";
- public enum PushAction {
+ private enum PushAction {
pmc, // Present Message Center.
unknown; // Anything unknown will not be handled.
@@ -149,59 +151,103 @@ public static PushAction parse(String name) {
@SuppressLint("StaticFieldLeak")
private static volatile ApptentiveInternal sApptentiveInternal;
+ // for unit testing
+ protected ApptentiveInternal() {
+ taskManager = null;
+ globalSharedPrefs = null;
+ apptentiveKey = null;
+ apptentiveSignature = null;
+ apptentiveHttpClient = null;
+ conversationManager = null;
+ appContext = null;
+ appRelease = null;
+ lifecycleCallbacks = null;
+ }
+
+ private ApptentiveInternal(Application application, String apptentiveKey, String apptentiveSignature, String serverUrl) {
+ 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;
+ this.apptentiveSignature = apptentiveSignature;
+ this.serverUrl = serverUrl;
+
+ appContext = application.getApplicationContext();
+
+ 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);
+ taskManager = new ApptentiveTaskManager(appContext, apptentiveHttpClient);
+
+ lifecycleCallbacks = new ApptentiveActivityLifecycleCallbacks();
+ ApptentiveNotificationCenter.defaultCenter()
+ .addObserver(NOTIFICATION_CONVERSATION_WILL_LOGOUT, this)
+ .addObserver(NOTIFICATION_AUTHENTICATION_FAILED, this);
+ }
public static boolean isApptentiveRegistered() {
- return (sApptentiveInternal != null);
+ return sApptentiveInternal != null;
+ }
+
+ public static boolean isConversationActive() {
+ return sApptentiveInternal != null && sApptentiveInternal.getConversation() != null;
}
/**
* Create a new or return a existing thread-safe instance of the Apptentive SDK. If this
* or any other {@link #getInstance()} has already been called in the application's lifecycle, the
- * API key will be ignored and the current instance will be returned.
+ * App key will be ignored and the current instance will be returned.
*
* This will be called from the application's onCreate(), before any other application objects have been
* created. Since the time spent in this function directly impacts the performance of starting the first activity,
* service, or receiver in the hosting app's process, the initialization of Apptentive is deferred to the first time
* {@link #getInstance()} is called.
*
- * @param context the context of the app that is creating the instance
- * @return An non-null instance of the Apptentive SDK
+ * @param application the context of the app that is creating the instance
*/
- public static ApptentiveInternal createInstance(Context context, final String apptentiveApiKey) {
- if (sApptentiveInternal == null) {
- synchronized (ApptentiveInternal.class) {
- if (sApptentiveInternal == null && context != null) {
- sApptentiveInternal = new ApptentiveInternal();
- isApptentiveInitialized.set(false);
- sApptentiveInternal.appContext = context.getApplicationContext();
- sApptentiveInternal.prefs = sApptentiveInternal.appContext.getSharedPreferences(Constants.PREF_NAME, Context.MODE_PRIVATE);
-
- MessageManager msgManager = new MessageManager();
- PayloadSendWorker payloadWorker = new PayloadSendWorker();
- InteractionManager interactionMgr = new InteractionManager();
- ApptentiveTaskManager worker = new ApptentiveTaskManager(sApptentiveInternal.appContext);
-
- sApptentiveInternal.messageManager = msgManager;
- sApptentiveInternal.payloadWorker = payloadWorker;
- sApptentiveInternal.interactionManager = interactionMgr;
- sApptentiveInternal.taskManager = worker;
- sApptentiveInternal.codePointStore = new CodePointStore();
- sApptentiveInternal.cachedExecutor = Executors.newCachedThreadPool();
- sApptentiveInternal.apiKey = Util.trim(apptentiveApiKey);
+ static void createInstance(Application application, String apptentiveKey, String apptentiveSignature, final String serverUrl) {
+ if (application == null) {
+ throw new IllegalArgumentException("Application is null");
+ }
+
+ synchronized (ApptentiveInternal.class) {
+ if (sApptentiveInternal == null) {
+
+ // trim spaces
+ apptentiveKey = Util.trim(apptentiveKey);
+ apptentiveSignature = Util.trim(apptentiveSignature);
+
+ // 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
+ }
+
+ // 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 {
+ ApptentiveLog.v("Initializing Apptentive instance: apptentiveKey=%s apptentiveSignature=%s", apptentiveKey, apptentiveSignature);
+ sApptentiveInternal = new ApptentiveInternal(application, apptentiveKey, apptentiveSignature, serverUrl);
+ sApptentiveInternal.start(); // TODO: check the result of this call
+ application.registerActivityLifecycleCallbacks(sApptentiveInternal.lifecycleCallbacks);
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while initializing ApptentiveInternal instance");
}
+ } else {
+ ApptentiveLog.w("Apptentive instance is already initialized");
}
}
- return sApptentiveInternal;
- }
-
- /**
- * Retrieve the existing instance of the Apptentive class. If {@link Apptentive#register(Application)} is
- * not called prior to this, it will only return null if context is null
- *
- * @return the existing instance of the Apptentive SDK fully initialized with API key, or a new instance if context is not null
- */
- public static ApptentiveInternal getInstance(Context context) {
- return createInstance((context == null) ? null : context, null);
}
/**
@@ -211,18 +257,9 @@ public static ApptentiveInternal getInstance(Context context) {
* @return the existing instance of the Apptentive SDK fully initialized with API key, or null
*/
public static ApptentiveInternal getInstance() {
- // Lazy initialization, only once for each application launch when getInstance() is called for the 1st time
- if (sApptentiveInternal != null && !isApptentiveInitialized.get()) {
- synchronized (ApptentiveInternal.class) {
- if (sApptentiveInternal != null && !isApptentiveInitialized.get()) {
- isApptentiveInitialized.set(true);
- if (!sApptentiveInternal.init()) {
- ApptentiveLog.e("Apptentive init() failed");
- }
- }
- }
+ synchronized (ApptentiveInternal.class) {
+ return sApptentiveInternal;
}
- return sApptentiveInternal;
}
/**
@@ -233,47 +270,15 @@ public static ApptentiveInternal getInstance() {
*/
public static void setInstance(ApptentiveInternal instance) {
sApptentiveInternal = instance;
- isApptentiveInitialized.set(false);
- }
-
- /**
- * Use this method to set or clear the internal app context (pass in null)
- * Note: designed to be used for unit testing only
- *
- * @param appContext the new application context to be set to
- */
- public static void setApplicationContext(Context appContext) {
- synchronized (ApptentiveInternal.class) {
- ApptentiveInternal internal = ApptentiveInternal.getInstance();
- if (internal != null) {
- internal.appContext = appContext;
- }
- }
- }
-
- /* Called by {@link #Apptentive.register()} to register global lifecycle
- * callbacks, only if the callback hasn't been set yet.
- */
- static void setLifeCycleCallback() {
- if (sApptentiveInternal != null && sApptentiveInternal.lifecycleCallbacks == null) {
- synchronized (ApptentiveInternal.class) {
- if (sApptentiveInternal != null && sApptentiveInternal.lifecycleCallbacks == null &&
- sApptentiveInternal.appContext instanceof Application) {
- sApptentiveInternal.lifecycleCallbacks = new ApptentiveActivityLifecycleCallbacks();
- ((Application) sApptentiveInternal.appContext).registerActivityLifecycleCallbacks(sApptentiveInternal.lifecycleCallbacks);
- }
- }
- }
}
-
/*
* Set default theme whom Apptentive UI will inherit theme attributes from. Apptentive will only
* inherit from an AppCompat theme
* @param themeResId : resource id of the theme style definition, such as R.style.MyAppTheme
* @return true if the theme is set for inheritance successfully.
*/
- public boolean setApplicationDefaultTheme(int themeResId) {
+ private boolean setApplicationDefaultTheme(int themeResId) {
try {
if (themeResId != 0) {
// If passed theme res id does not exist, an exception would be thrown and caught
@@ -316,18 +321,17 @@ public Context getApplicationContext() {
}
public int getApplicationVersionCode() {
- return currentVersionCode;
+ return appRelease.getVersionCode();
}
public String getApplicationVersionName() {
- return currentVersionName;
+ return appRelease.getVersionName();
}
public ApptentiveActivityLifecycleCallbacks getRegisteredLifecycleCallbacks() {
return lifecycleCallbacks;
}
-
/* Get the foreground activity from the current application, i.e. at the top of the task
* It is tracked through {@link #onActivityStarted(Activity)} and {@link #onActivityStopped(Activity)}
*
@@ -342,151 +346,113 @@ public Activity getCurrentTaskStackTopActivity() {
}
public MessageManager getMessageManager() {
- return messageManager;
- }
-
- public InteractionManager getInteractionManager() {
- return interactionManager;
- }
-
- public PayloadSendWorker getPayloadWorker() {
- return payloadWorker;
+ final Conversation conversation = getConversation();
+ return conversation != null ? conversation.getMessageManager() : null;
}
public ApptentiveTaskManager getApptentiveTaskManager() {
return taskManager;
}
- public CodePointStore getCodePointStore() {
- return codePointStore;
+ public ConversationManager getConversationManager() {
+ return conversationManager;
}
public Resources.Theme getApptentiveToolbarTheme() {
return apptentiveToolbarTheme;
}
- public int getDefaultStatusBarColor() {
+ int getDefaultStatusBarColor() {
return statusBarColorDefault;
}
- public String getApptentiveConversationToken() {
- return conversationToken;
- }
-
- public String getApptentiveApiKey() {
- return apiKey;
+ public Conversation getConversation() {
+ return conversationManager.getActiveConversation();
}
- public String getDefaultAppDisplayName() {
- return defaultAppDisplayName;
+ public String getApptentiveKey() {
+ return apptentiveKey;
}
- public boolean isApptentiveDebuggable() {
- return isAppDebuggable;
+ public String getApptentiveSignature() {
+ return apptentiveSignature;
}
- public String getPersonId() {
- return personId;
- }
-
- public String getAndroidId() {
- return androidId;
- }
-
- public SharedPreferences getSharedPrefs() {
- return prefs;
+ public String getServerUrl() {
+ if (serverUrl == null) {
+ return Constants.CONFIG_DEFAULT_SERVER_URL;
+ }
+ return serverUrl;
}
- public void addCustomDeviceData(String key, Object value) {
- if (key == null || key.trim().length() == 0) {
- return;
- }
- key = key.trim();
- CustomData customData = DeviceManager.loadCustomDeviceData();
- if (customData != null) {
- try {
- customData.put(key, value);
- DeviceManager.storeCustomDeviceData(customData);
- } catch (JSONException e) {
- ApptentiveLog.w("Unable to add custom device data.", e);
- }
- }
+ public String getDefaultAppDisplayName() {
+ return defaultAppDisplayName;
}
- public void addCustomPersonData(String key, Object value) {
- if (key == null || key.trim().length() == 0) {
- return;
- }
- CustomData customData = PersonManager.loadCustomPersonData();
- if (customData != null) {
- try {
- customData.put(key, value);
- PersonManager.storeCustomPersonData(customData);
- } catch (JSONException e) {
- ApptentiveLog.w("Unable to add custom person data.", e);
- }
- }
+ public boolean isApptentiveDebuggable() {
+ return appRelease.isDebug();
}
- public void runOnWorkerThread(Runnable r) {
- cachedExecutor.execute(r);
+ public SharedPreferences getGlobalSharedPrefs() {
+ return globalSharedPrefs;
}
- public void scheduleOnWorkerThread(Runnable r) {
- cachedExecutor.submit(r);
+ // FIXME: remove app release from this class
+ public AppRelease getAppRelease() {
+ return appRelease;
}
- public void checkAndUpdateApptentiveConfigurations() {
- // Initialize the Conversation Token, or fetch if needed. Fetch config it the token is available.
- if (conversationToken == null || personId == null) {
- asyncFetchConversationToken();
- } else {
- asyncFetchAppConfigurationAndInteractions();
- }
+ public ApptentiveHttpClient getApptentiveHttpClient() {
+ return apptentiveHttpClient;
}
public void onAppLaunch(final Context appContext) {
- EngagementModule.engageInternal(appContext, Event.EventLabel.app__launch.getLabelName());
+ if (isConversationActive()) {
+ engageInternal(appContext, EventPayload.EventLabel.app__launch.getLabelName());
+ }
}
public void onAppExit(final Context appContext) {
- EngagementModule.engageInternal(appContext, Event.EventLabel.app__exit.getLabelName());
+ if (isConversationActive()) {
+ engageInternal(appContext, EventPayload.EventLabel.app__exit.getLabelName());
+ }
}
public void onActivityStarted(Activity activity) {
if (activity != null) {
// Set current foreground activity reference whenever a new activity is started
- currentTaskStackTopActivity = new WeakReference(activity);
- messageManager.setCurrentForegroundActivity(activity);
- }
+ currentTaskStackTopActivity = new WeakReference<>(activity);
- checkAndUpdateApptentiveConfigurations();
-
- syncDevice();
- syncPerson();
+ // Post a notification
+ ApptentiveNotificationCenter.defaultCenter().postNotification(NOTIFICATION_ACTIVITY_STARTED,
+ NOTIFICATION_KEY_ACTIVITY, activity);
+ }
}
public void onActivityResumed(Activity activity) {
if (activity != null) {
// Set current foreground activity reference whenever a new activity is started
- currentTaskStackTopActivity = new WeakReference(activity);
- messageManager.setCurrentForegroundActivity(activity);
- }
+ currentTaskStackTopActivity = new WeakReference<>(activity);
+ // Post a notification
+ ApptentiveNotificationCenter.defaultCenter().postNotification(NOTIFICATION_ACTIVITY_RESUMED,
+ NOTIFICATION_KEY_ACTIVITY, activity);
+ }
}
public void onAppEnterForeground() {
appIsInForeground = true;
- payloadWorker.appWentToForeground();
- messageManager.appWentToForeground();
+
+ // Post a notification
+ ApptentiveNotificationCenter.defaultCenter().postNotification(NOTIFICATION_APP_ENTERED_FOREGROUND);
}
public void onAppEnterBackground() {
appIsInForeground = false;
currentTaskStackTopActivity = null;
- messageManager.setCurrentForegroundActivity(null);
- payloadWorker.appWentToBackground();
- messageManager.appWentToBackground();
+
+ // Post a notification
+ ApptentiveNotificationCenter.defaultCenter().postNotification(NOTIFICATION_APP_ENTERED_BACKGROUND);
}
/* Apply Apptentive styling layers to the theme to be used by interaction. The layers include
@@ -546,27 +512,32 @@ public void updateApptentiveInteractionTheme(Resources.Theme interactionTheme, C
apptentiveToolbarTheme.applyStyle(toolbarThemeId, true);
}
- public boolean init() {
+ private boolean start() {
boolean bRet = true;
- codePointStore.init();
/* If Message Center feature has never been used before, don't initialize message polling thread.
* Message Center feature will be seen as used, if one of the following conditions has been met:
* 1. Message Center has been opened for the first time
* 2. The first Push is received which would open Message Center
* 3. An unreadMessageCountListener() is set up
*/
- boolean featureEverUsed = prefs.getBoolean(Constants.PREF_KEY_MESSAGE_CENTER_FEATURE_USED, false);
- if (featureEverUsed) {
- messageManager.init();
+
+ 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();
+ // FIXME: don't accept the pull request before this one is resolved
+ // boolean featureEverUsed = activeConversation.isMessageCenterFeatureUsed();
+ // if (featureEverUsed) {
+ // messageManager.init();
+ // }
}
- conversationToken = prefs.getString(Constants.PREF_KEY_CONVERSATION_TOKEN, null);
- conversationId = prefs.getString(Constants.PREF_KEY_CONVERSATION_ID, null);
- personId = prefs.getString(Constants.PREF_KEY_PERSON_ID, null);
+
apptentiveToolbarTheme = appContext.getResources().newTheme();
boolean apptentiveDebug = false;
String logLevelOverride = null;
- String manifestApiKey = null;
try {
appPackageName = appContext.getPackageName();
PackageManager packageManager = appContext.getPackageManager();
@@ -575,7 +546,6 @@ public boolean init() {
Bundle metaData = ai.metaData;
if (metaData != null) {
- manifestApiKey = Util.trim(metaData.getString(Constants.MANIFEST_KEY_APPTENTIVE_API_KEY));
logLevelOverride = Util.trim(metaData.getString(Constants.MANIFEST_KEY_APPTENTIVE_LOG_LEVEL));
apptentiveDebug = metaData.getBoolean(Constants.MANIFEST_KEY_APPTENTIVE_DEBUG);
}
@@ -583,24 +553,11 @@ public boolean init() {
// Used for application theme inheritance if the theme is an AppCompat theme.
setApplicationDefaultTheme(ai.theme);
- AppRelease appRelease = AppRelease.generateCurrentAppRelease(appContext);
-
- isAppDebuggable = appRelease.getDebug();
- currentVersionCode = appRelease.getVersionCode();
- currentVersionName = appRelease.getVersionName();
-
- VersionHistoryEntry lastVersionEntrySeen = VersionHistoryStore.getLastVersionSeen();
-
- if (lastVersionEntrySeen == null) {
- onVersionChanged(null, currentVersionCode, null, currentVersionName, appRelease);
- } else {
- int lastSeenVersionCode = lastVersionEntrySeen.getVersionCode();
- Apptentive.Version lastSeenVersionNameVersion = new Apptentive.Version();
- lastSeenVersionNameVersion.setVersion(lastVersionEntrySeen.getVersionName());
- if (!(currentVersionCode == lastSeenVersionCode) || !currentVersionName.equals(lastSeenVersionNameVersion.getVersion())) {
- onVersionChanged(lastVersionEntrySeen.getVersionCode(), currentVersionCode, lastVersionEntrySeen.getVersionName(), currentVersionName, appRelease);
- }
+ Conversation conversation = getConversation();
+ if (conversation != null) {
+ checkSendVersionChanges(conversation);
}
+
defaultAppDisplayName = packageManager.getApplicationLabel(packageManager.getApplicationInfo(packageInfo.packageName, 0)).toString();
// Prevent delayed run-time exception if the app upgrades from pre-2.0 and doesn't remove NetworkStateReceiver from manifest
@@ -615,7 +572,7 @@ public boolean init() {
}
} catch (Exception e) {
- ApptentiveLog.e("Unexpected error while reading application or package info.", e);
+ ApptentiveLog.e(e, "Unexpected error while reading application or package info.");
bRet = false;
}
@@ -628,266 +585,112 @@ public boolean init() {
ApptentiveLog.i("Overriding log level: %s", logLevelOverride);
setMinimumLogLevel(ApptentiveLog.Level.parse(logLevelOverride));
} else {
- if (isAppDebuggable) {
+ if (appRelease.isDebug()) {
setMinimumLogLevel(ApptentiveLog.Level.VERBOSE);
}
}
- ApptentiveLog.i("Debug mode enabled? %b", isAppDebuggable);
-
- String lastSeenSdkVersion = prefs.getString(Constants.PREF_KEY_LAST_SEEN_SDK_VERSION, "");
- if (!lastSeenSdkVersion.equals(Constants.APPTENTIVE_SDK_VERSION)) {
- onSdkVersionChanged(appContext, lastSeenSdkVersion, Constants.APPTENTIVE_SDK_VERSION);
- }
-
- // The apiKey can be passed in programmatically, or we can fallback to checking in the manifest.
- if (TextUtils.isEmpty(apiKey) && !TextUtils.isEmpty(manifestApiKey)) {
- apiKey = manifestApiKey;
+ 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");
}
- if (TextUtils.isEmpty(apiKey) || apiKey.contains(Constants.EXAMPLE_API_KEY_VALUE)) {
- String errorMessage = "The Apptentive API Key is not defined. You may provide your Apptentive API Key in Apptentive.register(), or in as meta-data in your AndroidManifest.xml.\n" +
- "";
- if (isAppDebuggable) {
+ 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 API Key");
+ ApptentiveLog.d("Using cached Apptentive App Signature");
}
- ApptentiveLog.d("Apptentive API Key: %s", apiKey);
+ ApptentiveLog.d("Apptentive App Signature: %s", apptentiveSignature);
// Grab app info we need to access later on.
- androidId = Settings.Secure.getString(appContext.getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
- ApptentiveLog.d("Android ID: ", androidId);
ApptentiveLog.d("Default Locale: %s", Locale.getDefault().toString());
- ApptentiveLog.d("Conversation id: %s", prefs.getString(Constants.PREF_KEY_CONVERSATION_ID, "null"));
return bRet;
}
- private void onVersionChanged(Integer previousVersionCode, Integer currentVersionCode, String previousVersionName, String currentVersionName, AppRelease currentAppRelease) {
- ApptentiveLog.i("Version changed: Name: %s => %s, Code: %d => %d", previousVersionName, currentVersionName, previousVersionCode, currentVersionCode);
- VersionHistoryStore.updateVersionHistory(currentVersionCode, currentVersionName);
- if (previousVersionCode != null) {
- AppReleaseManager.storeAppRelease(currentAppRelease);
- taskManager.addPayload(currentAppRelease);
+ private void checkSendVersionChanges(Conversation conversation) {
+ if (conversation == null) {
+ ApptentiveLog.e("Can't check session data changes: session data is not initialized");
+ return;
}
- invalidateCaches();
- }
- private void onSdkVersionChanged(Context context, String previousSdkVersion, String currentSdkVersion) {
- ApptentiveLog.i("SDK version changed: %s => %s", previousSdkVersion, currentSdkVersion);
- context.getSharedPreferences(Constants.PREF_NAME, Context.MODE_PRIVATE).edit().putString(Constants.PREF_KEY_LAST_SEEN_SDK_VERSION, currentSdkVersion).apply();
- syncSdk();
- invalidateCaches();
- }
+ boolean appReleaseChanged = false;
+ boolean sdkChanged = false;
- /**
- * 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() {
- interactionManager.updateCacheExpiration(0);
- Configuration config = Configuration.load();
- config.setConfigurationCacheExpirationMillis(System.currentTimeMillis());
- config.save();
- }
+ final VersionHistoryItem lastVersionItemSeen = conversation.getVersionHistory().getLastVersionSeen();
+ final int currentVersionCode = appRelease.getVersionCode();
+ final String currentVersionName = appRelease.getVersionName();
- private synchronized void asyncFetchConversationToken() {
- if (isConversationTokenFetchPending.compareAndSet(false, true)) {
- AsyncTask fetchConversationTokenTask = new AsyncTask() {
- private Exception e = null;
+ Integer previousVersionCode = null;
+ String previousVersionName = null;
- @Override
- protected Boolean doInBackground(Void... params) {
- try {
- return fetchConversationToken();
- } catch (Exception e) {
- // Hold onto the unhandled exception from fetchConversationToken() for later handling in UI thread
- this.e = e;
- }
- return false;
- }
-
- @Override
- protected void onPostExecute(Boolean successful) {
- if (e == null) {
- // Update pending state on UI thread after finishing the task
- ApptentiveLog.d("Fetching conversation token asyncTask finished. Successful? %b", successful);
- isConversationTokenFetchPending.set(false);
- if (successful) {
- // Once token is fetched successfully, start asyncTasks to fetch global configuration, then interaction
- asyncFetchAppConfigurationAndInteractions();
- }
- } else {
- ApptentiveLog.w("Unhandled Exception thrown from fetching conversation token asyncTask", e);
- MetricModule.sendError(e, null, null);
- }
- }
-
- };
-
- ApptentiveLog.i("Fetching conversation token asyncTask scheduled.");
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
- fetchConversationTokenTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } else {
- fetchConversationTokenTask.execute();
- }
+ if (lastVersionItemSeen == null) {
+ appReleaseChanged = true;
} else {
- ApptentiveLog.v("Fetching Configuration pending");
- }
- }
-
-
- private boolean fetchConversationToken() {
- ApptentiveLog.i("Fetching Configuration token task started.");
- // Try to fetch a new one from the server.
- ConversationTokenRequest request = new ConversationTokenRequest();
-
- // Send the Device and Sdk now, so they are available on the server from the start.
- request.setDevice(DeviceManager.storeDeviceAndReturnIt());
- request.setSdk(SdkManager.storeSdkAndReturnIt());
- request.setPerson(PersonManager.storePersonAndReturnIt());
- AppRelease currentAppRelease = AppRelease.generateCurrentAppRelease(appContext);
- AppReleaseManager.storeAppRelease(currentAppRelease);
- request.setAppRelease(currentAppRelease);
+ previousVersionCode = lastVersionItemSeen.getVersionCode();
+ Apptentive.Version lastSeenVersionNameVersion = new Apptentive.Version();
- ApptentiveHttpResponse response = ApptentiveClient.getConversationToken(request);
- if (response == null) {
- ApptentiveLog.w("Got null response fetching ConversationToken.");
- return false;
- }
- if (response.isSuccessful()) {
- try {
- JSONObject root = new JSONObject(response.getContent());
- String conversationToken = root.getString("token");
- ApptentiveLog.d("ConversationToken: " + conversationToken);
- String conversationId = root.getString("id");
- ApptentiveLog.d("New Conversation id: %s", conversationId);
-
- if (conversationToken != null && !conversationToken.equals("")) {
- setConversationToken(conversationToken);
- setConversationId(conversationId);
- }
- String personId = root.getString("person_id");
- ApptentiveLog.d("PersonId: " + personId);
- if (personId != null && !personId.equals("")) {
- setPersonId(personId);
- }
- return true;
- } catch (JSONException e) {
- ApptentiveLog.e("Error parsing ConversationToken response json.", e);
+ previousVersionName = lastVersionItemSeen.getVersionName();
+ lastSeenVersionNameVersion.setVersion(previousVersionName);
+ if (!(currentVersionCode == previousVersionCode) || !currentVersionName.equals(lastSeenVersionNameVersion.getVersion())) {
+ appReleaseChanged = true;
}
}
- return false;
- }
- /**
- * Fetches the global app configuration from the server and stores the keys into our SharedPreferences.
- */
- private void fetchAppConfiguration() {
- ApptentiveLog.i("Fetching new Configuration task started.");
- ApptentiveHttpResponse response = ApptentiveClient.getAppConfiguration();
- try {
- Map headers = response.getHeaders();
- if (headers != null) {
- String cacheControl = headers.get("Cache-Control");
- Integer cacheSeconds = Util.parseCacheControlHeader(cacheControl);
- if (cacheSeconds == null) {
- cacheSeconds = Constants.CONFIG_DEFAULT_APP_CONFIG_EXPIRATION_DURATION_SECONDS;
- }
- ApptentiveLog.d("Caching configuration for %d seconds.", cacheSeconds);
- Configuration config = new Configuration(response.getContent());
- config.setConfigurationCacheExpirationMillis(System.currentTimeMillis() + cacheSeconds * 1000);
- config.save();
- }
- } catch (JSONException e) {
- ApptentiveLog.e("Error parsing app configuration from server.", e);
+ // TODO: Move this into a session became active handler.
+ final String lastSeenSdkVersion = conversation.getLastSeenSdkVersion();
+ final String currentSdkVersion = Constants.APPTENTIVE_SDK_VERSION;
+ if (!TextUtils.equals(lastSeenSdkVersion, currentSdkVersion)) {
+ sdkChanged = true;
}
- }
-
- private void asyncFetchAppConfigurationAndInteractions() {
- boolean force = isAppDebuggable;
-
- // Don't get the app configuration unless no pending fetch AND either forced, or the cache has expired.
- if (isConfigurationFetchPending.compareAndSet(false, true) && (force || Configuration.load().hasConfigurationCacheExpired())) {
- AsyncTask fetchConfigurationTask = new AsyncTask() {
- // Hold onto the exception from the AsyncTask instance for later handling in UI thread
- private Exception e = null;
- @Override
- protected Void doInBackground(Void... params) {
- try {
- fetchAppConfiguration();
- } catch (Exception e) {
- this.e = e;
- }
- return null;
- }
-
- @Override
- protected void onPostExecute(Void v) {
- // Update pending state on UI thread after finishing the task
- ApptentiveLog.i("Fetching new Configuration asyncTask finished.");
- isConfigurationFetchPending.set(false);
- if (e != null) {
- ApptentiveLog.w("Unhandled Exception thrown from fetching configuration asyncTask", e);
- MetricModule.sendError(e, null, null);
- } else {
- // Check if need to start another asyncTask to fetch interaction after successfully fetching configuration
- interactionManager.asyncFetchAndStoreInteractions();
- }
- }
- };
-
- ApptentiveLog.i("Fetching new Configuration asyncTask scheduled.");
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
- fetchConfigurationTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } else {
- fetchConfigurationTask.execute();
- }
- } else {
- ApptentiveLog.v("Using cached Configuration.");
- // If configuration hasn't expire, then check if need to start another asyncTask to fetch interaction
- interactionManager.asyncFetchAndStoreInteractions();
+ if (appReleaseChanged) {
+ ApptentiveLog.i("Version changed: Name: %s => %s, Code: %d => %d", previousVersionName, currentVersionName, previousVersionCode, currentVersionCode);
+ conversation.getVersionHistory().updateVersionHistory(Util.currentTimeSeconds(), currentVersionCode, currentVersionName);
}
- }
- /**
- * Sends current Device to the server if it differs from the last time it was sent.
- */
- void syncDevice() {
- Device deviceInfo = DeviceManager.storeDeviceAndReturnDiff();
- if (deviceInfo != null) {
- ApptentiveLog.d("Device info was updated.");
- ApptentiveLog.v(deviceInfo.toString());
- taskManager.addPayload(deviceInfo);
- } else {
- ApptentiveLog.d("Device info was not updated.");
+ Sdk sdk = SdkManager.generateCurrentSdk();
+ if (sdkChanged) {
+ ApptentiveLog.i("SDK version changed: %s => %s", lastSeenSdkVersion, currentSdkVersion);
+ conversation.setLastSeenSdkVersion(currentSdkVersion);
+ conversation.setSdk(sdk);
}
- }
- /**
- * Sends current SDK to the server.
- */
- private void syncSdk() {
- Sdk sdk = SdkManager.generateCurrentSdk();
- SdkManager.storeSdk(sdk);
- ApptentiveLog.v(sdk.toString());
- taskManager.addPayload(sdk);
+ if (appReleaseChanged || sdkChanged) {
+ conversation.addPayload(AppReleaseManager.getPayload(sdk, appRelease));
+ invalidateCaches();
+ }
}
/**
- * Sends current Person to the server if it differs from the last time it was sent.
+ * We want to make sure the app is using the latest configuration from the server if the app or sdk version changes.
*/
- private void syncPerson() {
- Person person = PersonManager.storePersonAndReturnDiff();
- if (person != null) {
- ApptentiveLog.d("Person was updated.");
- ApptentiveLog.v(person.toString());
- taskManager.addPayload(person);
- } else {
- ApptentiveLog.d("Person was not updated.");
+ private void invalidateCaches() {
+ Conversation conversation = getConversation();
+ if (conversation != null) {
+ conversation.setInteractionExpiration(0L);
}
+ Configuration config = Configuration.load();
+ config.setConfigurationCacheExpirationMillis(System.currentTimeMillis());
+ config.save();
}
public IRatingProvider getRatingProvider() {
@@ -897,7 +700,7 @@ public IRatingProvider getRatingProvider() {
return ratingProvider;
}
- public void setRatingProvider(IRatingProvider ratingProvider) {
+ void setRatingProvider(IRatingProvider ratingProvider) {
this.ratingProvider = ratingProvider;
}
@@ -905,16 +708,16 @@ public Map getRatingProviderArgs() {
return ratingProviderArgs;
}
- public void putRatingProviderArg(String key, String value) {
+ void putRatingProviderArg(String key, String value) {
if (ratingProviderArgs == null) {
- ratingProviderArgs = new HashMap();
+ ratingProviderArgs = new HashMap<>();
}
ratingProviderArgs.put(key, value);
}
- public void setOnSurveyFinishedListener(OnSurveyFinishedListener onSurveyFinishedListener) {
+ void setOnSurveyFinishedListener(OnSurveyFinishedListener onSurveyFinishedListener) {
if (onSurveyFinishedListener != null) {
- this.onSurveyFinishedListener = new WeakReference(onSurveyFinishedListener);
+ this.onSurveyFinishedListener = new WeakReference<>(onSurveyFinishedListener);
} else {
this.onSurveyFinishedListener = null;
}
@@ -932,10 +735,26 @@ public void removeInteractionUpdateListener(InteractionManager.InteractionUpdate
interactionUpdateListeners.remove(listener);
}
+ public void setAuthenticationFailedListener(Apptentive.AuthenticationFailedListener listener) {
+ authenticationFailedListenerRef = new WeakReference<>(listener);
+ }
+
+ public void notifyAuthenticationFailedListener(Apptentive.AuthenticationFailedReason reason, String conversationIdOfFailedRequest) {
+ if (isConversationActive()) {
+ String activeConversationId = getConversation().getConversationId();
+ if (StringUtils.equal(activeConversationId, conversationIdOfFailedRequest)) {
+ Apptentive.AuthenticationFailedListener listener = authenticationFailedListenerRef != null ? authenticationFailedListenerRef.get() : null;
+ if (listener != null) {
+ listener.onAuthenticationFailed(reason);
+ }
+ }
+ }
+ }
+
/**
* Pass in a log level to override the default, which is {@link ApptentiveLog.Level#INFO}
*/
- public void setMinimumLogLevel(ApptentiveLog.Level level) {
+ private void setMinimumLogLevel(ApptentiveLog.Level level) {
ApptentiveLog.overrideLogLevel(level);
}
@@ -1022,22 +841,6 @@ static String getApptentivePushNotificationData(Map pushData) {
return null;
}
- boolean setPendingPushNotification(String apptentivePushData) {
- if (apptentivePushData != null) {
- ApptentiveLog.d("Saving Apptentive push notification data.");
- prefs.edit().putString(Constants.PREF_KEY_PENDING_PUSH_NOTIFICATION, apptentivePushData).apply();
- messageManager.startMessagePreFetchTask();
- return true;
- }
- return false;
- }
-
- boolean clearPendingPushNotification() {
- ApptentiveLog.d("Clearing Apptentive push notification data.");
- prefs.edit().remove(Constants.PREF_KEY_PENDING_PUSH_NOTIFICATION).apply();
- return true;
- }
-
public void showAboutInternal(Context context, boolean showBrandingBand) {
Intent intent = new Intent();
intent.setClass(context, ApptentiveViewActivity.class);
@@ -1054,6 +857,25 @@ static PendingIntent generatePendingIntentFromApptentivePushData(String apptenti
if (!TextUtils.isEmpty(apptentivePushData)) {
try {
JSONObject pushJson = new JSONObject(apptentivePushData);
+
+ // we need to check if current user is actually the receiver of this notification
+ final String conversationId = pushJson.optString(PUSH_CONVERSATION_ID, null);
+ if (conversationId != null) {
+ final Conversation conversation = ApptentiveInternal.getInstance().getConversation();
+
+ // do we have a conversation right now?
+ if (conversation == null) {
+ ApptentiveLog.w("Can't generate pending intent from Apptentive push data: no active conversation");
+ return 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");
+ return null;
+ }
+ }
+
ApptentiveInternal.PushAction action = ApptentiveInternal.PushAction.unknown;
if (pushJson.has(ApptentiveInternal.PUSH_ACTION)) {
action = ApptentiveInternal.PushAction.parse(pushJson.getString(ApptentiveInternal.PUSH_ACTION));
@@ -1072,7 +894,7 @@ static PendingIntent generatePendingIntentFromApptentivePushData(String apptenti
ApptentiveLog.w("Unknown Apptentive push notification action: \"%s\"", action.name());
}
} catch (JSONException e) {
- ApptentiveLog.e("Error parsing JSON from push notification.", e);
+ ApptentiveLog.e(e, "Error parsing JSON from push notification.");
MetricModule.sendError(e, "Parsing Apptentive Push", apptentivePushData);
}
}
@@ -1089,12 +911,12 @@ public boolean showMessageCenterInternal(Context context, Map cu
Object value = customData.get(key);
if (value != null) {
if (!(value instanceof String ||
- value instanceof Boolean ||
- value instanceof Long ||
- value instanceof Double ||
- value instanceof Float ||
- value instanceof Integer ||
- value instanceof Short)) {
+ value instanceof Boolean ||
+ value instanceof Long ||
+ value instanceof Double ||
+ value instanceof Float ||
+ value instanceof Integer ||
+ value instanceof Short)) {
ApptentiveLog.w("Removing invalid customData type: %s", value.getClass().getSimpleName());
keysIterator.remove();
}
@@ -1102,7 +924,7 @@ public boolean showMessageCenterInternal(Context context, Map cu
}
}
this.customData = customData;
- interactionShown = EngagementModule.engageInternal(context, MessageCenterInteraction.DEFAULT_INTERNAL_EVENT_NAME);
+ interactionShown = engageInternal(context, MessageCenterInteraction.DEFAULT_INTERNAL_EVENT_NAME);
if (!interactionShown) {
this.customData = null;
}
@@ -1116,8 +938,14 @@ public void showMessageCenterFallback(Context context) {
EngagementModule.launchMessageCenterErrorActivity(context);
}
+ // FIXME: remove this method
public boolean canShowMessageCenterInternal() {
- return EngagementModule.canShowInteraction("com.apptentive", "app", MessageCenterInteraction.DEFAULT_INTERNAL_EVENT_NAME);
+ Conversation conversation = getConversation();
+ return conversation != null && canShowMessageCenterInternal(conversation);
+ }
+
+ public boolean canShowMessageCenterInternal(Conversation conversation) {
+ return EngagementModule.canShowInteraction(conversation, "app", MessageCenterInteraction.DEFAULT_INTERNAL_EVENT_NAME, "com.apptentive");
}
public Map getAndClearCustomData() {
@@ -1126,28 +954,13 @@ public Map getAndClearCustomData() {
return customData;
}
- private void setConversationToken(String newConversationToken) {
- conversationToken = newConversationToken;
- prefs.edit().putString(Constants.PREF_KEY_CONVERSATION_TOKEN, conversationToken).apply();
- }
-
- private void setConversationId(String newConversationId) {
- conversationId = newConversationId;
- prefs.edit().putString(Constants.PREF_KEY_CONVERSATION_ID, conversationId).apply();
- }
-
- private void setPersonId(String newPersonId) {
- personId = newPersonId;
- prefs.edit().putString(Constants.PREF_KEY_PERSON_ID, personId).apply();
- }
-
public void resetSdkState() {
- prefs.edit().clear().apply();
+ globalSharedPrefs.edit().clear().apply();
taskManager.reset(appContext);
- VersionHistoryStore.clear();
}
public void notifyInteractionUpdated(boolean successful) {
+ ApptentiveNotificationCenter.defaultCenter().postNotification(NOTIFICATION_INTERACTIONS_FETCHED);
Iterator it = interactionUpdateListeners.iterator();
while (it.hasNext()) {
@@ -1186,10 +999,110 @@ public static boolean checkRegistered() {
return true;
}
+ //region Helpers
+
+ private String getEndpointBase(SharedPreferences prefs) {
+ String url = prefs.getString(Constants.PREF_KEY_SERVER_URL, null);
+ if (url == null) {
+ url = Constants.CONFIG_DEFAULT_SERVER_URL;
+ prefs.edit().putString(Constants.PREF_KEY_SERVER_URL, url).apply();
+ }
+ return url;
+ }
+
+ //endregion
+
+ //region Login/Logout
+
+ /**
+ * Flag indicating if login request is currently active (used to avoid multiple competing
+ * requests
+ */
+ private boolean loginInProgress;
+
+ /**
+ * Mutex object for synchronizing login request flag
+ */
+ private final Object loginMutex = new Object();
+
+ void login(String token, final LoginCallback callback) {
+ synchronized (loginMutex) {
+ if (loginInProgress) {
+ if (callback != null) {
+ callback.onLoginFail("Another login request is currently in progress");
+ }
+ return;
+ }
+
+ loginInProgress = true;
+
+ LoginCallback wrapperCallback = new LoginCallback() {
+ @Override
+ public void onLoginFinish() {
+ synchronized (loginMutex) {
+ assertTrue(loginInProgress);
+ try {
+ engageInternal(getApplicationContext(), "login");
+ if (callback != null) {
+ callback.onLoginFinish();
+ }
+ } finally {
+ loginInProgress = false;
+ }
+ }
+ }
+
+ @Override
+ public void onLoginFail(String errorMessage) {
+ synchronized (loginMutex) {
+ assertTrue(loginInProgress);
+ try {
+ if (callback != null) {
+ callback.onLoginFail(errorMessage);
+ }
+ } finally {
+ loginInProgress = false;
+ }
+ }
+ }
+ };
+
+ conversationManager.login(token, wrapperCallback);
+ }
+ }
+
+ void logout() {
+ conversationManager.logout();
+ }
+
+ //endregion
+
/**
* Dismisses any currently-visible interactions. This method is for internal use and is subject to change.
*/
public static void dismissAllInteractions() {
ApptentiveNotificationCenter.defaultCenter().postNotification(NOTIFICATION_INTERACTIONS_SHOULD_DISMISS);
}
+
+ @Override
+ public void onReceiveNotification(ApptentiveNotification notification) {
+ if (notification.hasName(NOTIFICATION_CONVERSATION_WILL_LOGOUT)) {
+ Conversation conversation = notification.getRequiredUserInfo(NOTIFICATION_KEY_CONVERSATION, Conversation.class);
+ conversation.addPayload(new LogoutPayload());
+ } else if (notification.hasName(NOTIFICATION_AUTHENTICATION_FAILED)) {
+ String conversationIdOfFailedRequest = notification.getUserInfo(NOTIFICATION_KEY_CONVERSATION_ID, String.class);
+ Apptentive.AuthenticationFailedReason authenticationFailedReason = notification.getUserInfo(NOTIFICATION_KEY_AUTHENTICATION_FAILED_REASON, Apptentive.AuthenticationFailedReason.class);
+ notifyAuthenticationFailedListener(authenticationFailedReason, conversationIdOfFailedRequest);
+ }
+ }
+
+ //region Engagement
+
+ private boolean engageInternal(Context context, String eventName) {
+ Conversation conversation = getConversation();
+ assertNotNull(conversation, "Attempted to engage '%s' internal event without an active conversation", eventName);
+ return conversation != null && EngagementModule.engageInternal(context, conversation, eventName);
+ }
+
+ //endregion
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveLog.java b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveLog.java
index ab3d5c349..76ad854e0 100755
--- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveLog.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveLog.java
@@ -20,7 +20,7 @@ public static void overrideLogLevel(Level level) {
ApptentiveLog.logLevel = level;
}
- private static void doLog(Level level, Throwable throwable, String message, Object... args){
+ private static void doLog(Level level, ApptentiveLogTag tag, Throwable throwable, String message, Object... args){
if(canLog(level) && message != null){
if(args.length > 0){
try{
@@ -30,17 +30,34 @@ private static void doLog(Level level, Throwable throwable, String message, Obje
level = Level.ERROR;
}
}
+
+ String extra = null;
+
// add thread name if logging of the UI-thread
if (Looper.getMainLooper() != null && Looper.getMainLooper().getThread() != Thread.currentThread()) {
- message = String.format("[%s] %s", Thread.currentThread().getName(), message);
+ extra = '[' + Thread.currentThread().getName() + ']';
+ }
+
+ // custom tag
+ if (tag != null) {
+ if (extra == null) {
+ extra = '[' + tag.toString() + ']';
+ } else {
+ extra += " [" + tag.toString() + ']';
+ }
+ }
+
+ if (extra != null) {
+ message = extra + " " + message;
}
- android.util.Log.println(level.getLevel(), TAG, message);
+
+ android.util.Log.println(level.getAndroidLevel(), TAG, message);
if(throwable != null){
if(throwable.getMessage() != null){
- android.util.Log.println(level.getLevel(), TAG, throwable.getMessage());
+ android.util.Log.println(level.getAndroidLevel(), TAG, throwable.getMessage());
}
while(throwable != null) {
- android.util.Log.println(level.getLevel(), TAG, android.util.Log.getStackTraceString(throwable));
+ android.util.Log.println(level.getAndroidLevel(), TAG, android.util.Log.getStackTraceString(throwable));
throwable = throwable.getCause();
}
}
@@ -51,86 +68,145 @@ public static boolean canLog(Level level) {
return logLevel.canLog(level);
}
- public static void v(ApptentiveLogTag tag, String message, Object... args) {
+ public static void vv(ApptentiveLogTag tag, String message, Object... args) {
+ if (tag.enabled) {
+ doLog(Level.VERY_VERBOSE, tag, null, message, args);
+ }
+ }
+ public static void vv(ApptentiveLogTag tag, Throwable throwable, String message, Object... args){
if (tag.enabled) {
- doLog(Level.VERBOSE, null, message, args);
+ doLog(Level.VERY_VERBOSE, tag, throwable, message, args);
}
}
+ 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 void v(ApptentiveLogTag tag, String message, Object... args) {
+ if (tag.enabled) {
+ doLog(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(String message, Object... args){
- doLog(Level.VERBOSE, null, message, args);
+ doLog(Level.VERBOSE, null, null, message, args);
}
- public static void v(String message, Throwable throwable, Object... args){
- doLog(Level.VERBOSE, throwable, message, args);
+ public static void v(Throwable throwable, String message, Object... args){
+ doLog(Level.VERBOSE, null, throwable, message, args);
}
public static void d(ApptentiveLogTag tag, String message, Object... args){
if (tag.enabled) {
- doLog(Level.DEBUG, null, message, args);
+ doLog(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(String message, Object... args){
- doLog(Level.DEBUG, null, message, args);
+ doLog(Level.DEBUG, null, null, message, args);
}
- public static void d(String message, Throwable throwable, Object... args){
- doLog(Level.DEBUG, throwable, message, args);
+ public static void d(Throwable throwable, String message, Object... args){
+ doLog(Level.DEBUG, null, throwable, message, args);
}
public static void i(ApptentiveLogTag tag, String message, Object... args){
if (tag.enabled) {
- doLog(Level.INFO, null, message, args);
+ doLog(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(String message, Object... args){
- doLog(Level.INFO, null, message, args);
+ doLog(Level.INFO, null, null, message, args);
}
- public static void i(String message, Throwable throwable, Object... args){
- doLog(Level.INFO, throwable, message, args);
+ public static void i(Throwable throwable, String message, Object... args){
+ doLog(Level.INFO, null, throwable, message, args);
}
public static void w(ApptentiveLogTag tag, String message, Object... args){
if (tag.enabled) {
- doLog(Level.WARN, null, message, args);
+ doLog(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(String message, Object... args){
- doLog(Level.WARN, null, message, args);
+ doLog(Level.WARN, null, null, message, args);
}
- public static void w(String message, Throwable throwable, Object... args){
- doLog(Level.WARN, throwable, message, args);
+ public static void w(Throwable throwable, String message, Object... args){
+ doLog(Level.WARN, null, throwable, message, args);
}
- public static void e(String message, Object... args){
- doLog(Level.ERROR, null, 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(String message, Throwable throwable, Object... args){
- doLog(Level.ERROR, throwable, 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(String message, Object... args){
+ doLog(Level.ERROR, null, null, message, args);
}
public static void e(Throwable throwable, String message, Object... args){
- doLog(Level.ERROR, throwable, message, args);
+ doLog(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, Throwable throwable, String message, Object... args){
+ if (tag.enabled) {
+ doLog(Level.ASSERT, tag, throwable, message, args);
+ }
+ }
public static void a(String message, Object... args){
- doLog(Level.ASSERT, null, message, args);
+ doLog(Level.ASSERT, null, null, message, args);
}
- public static void a(String message, Throwable throwable, Object... args){
- doLog(Level.ASSERT, throwable, message, args);
+ public static void a(Throwable throwable, String message, Object... args){
+ doLog(Level.ASSERT, null, throwable, message, args);
}
public enum Level {
- VERBOSE(Log.VERBOSE),
- DEBUG(Log.DEBUG),
- INFO(Log.INFO),
- WARN(Log.WARN),
- ERROR(Log.ERROR),
- ASSERT(Log.ASSERT),
- DEFAULT(Log.INFO);
+ 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) {
+ private Level(int level, int androidLevel) {
this.level = level;
+ this.androidLevel = androidLevel;
+ }
+
+ public int getAndroidLevel() {
+ return androidLevel;
}
public int getLevel() {
@@ -151,7 +227,7 @@ public static Level parse(String level) {
* @return true if "level" can be logged.
*/
public boolean canLog(Level level) {
- return level.getLevel() >= getLevel();
+ return level.level >= this.level;
}
}
}
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 d9d5b0339..69a54a7fa 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveLogTag.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveLogTag.java
@@ -1,8 +1,17 @@
package com.apptentive.android.sdk;
public enum ApptentiveLogTag {
- NETWORK,
- CONVERSATION;
+ NETWORK(true),
+ CONVERSATION(true),
+ NOTIFICATIONS(true),
+ MESSAGES(true),
+ DATABASE(true),
+ PAYLOADS(true),
+ TESTER_COMMANDS(true);
- public boolean enabled = true;
+ ApptentiveLogTag(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean enabled;
}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveNotifications.java b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveNotifications.java
new file mode 100644
index 000000000..8a5e315df
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveNotifications.java
@@ -0,0 +1,75 @@
+/*
+ * 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;
+
+public class ApptentiveNotifications {
+
+ /**
+ * Sent when conversation state changes (user logs out, etc)
+ */
+ public static final String NOTIFICATION_CONVERSATION_STATE_DID_CHANGE = "CONVERSATION_STATE_DID_CHANGE"; // { conversation : Conversation }
+
+ /**
+ * 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 if a new activity is started.
+ */
+ public static final String NOTIFICATION_ACTIVITY_STARTED = "NOTIFICATION_ACTIVITY_STARTED"; // { activity : Activity }
+
+ /**
+ * Sent if activity is resumed.
+ */
+ public static final String NOTIFICATION_ACTIVITY_RESUMED = "NOTIFICATION_ACTIVITY_RESUMED"; // { activity : Activity }
+
+ /**
+ * Sent if app entered foreground
+ */
+ public static final String NOTIFICATION_APP_ENTERED_FOREGROUND = "NOTIFICATION_APP_ENTERED_FOREGROUND";
+
+ /**
+ * Sent if app entered background
+ */
+ public static final String NOTIFICATION_APP_ENTERED_BACKGROUND = "NOTIFICATION_APP_ENTERED_BACKGROUND";
+
+ /**
+ * 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 }
+
+ /**
+ * 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 }
+
+ /**
+ * Sent if user requested to close all interactions.
+ */
+ public static final String NOTIFICATION_INTERACTIONS_SHOULD_DISMISS = "NOTIFICATION_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 }
+
+ /**
+ * 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";
+
+ // 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_AUTHENTICATION_FAILED_REASON = "authenticationFailedReason";// type: AuthenticationFailedReason
+ public static final String NOTIFICATION_KEY_PAYLOAD = "payload";
+ public static final String NOTIFICATION_KEY_RESPONSE_CODE = "responseCode";
+ public static final String NOTIFICATION_KEY_RESPONSE_DATA = "responseData";
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveViewActivity.java b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveViewActivity.java
index 4790e25f2..f66f7ddd7 100644
--- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveViewActivity.java
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveViewActivity.java
@@ -18,7 +18,6 @@
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
-
import android.support.v4.content.ContextCompat;
import android.support.v4.content.IntentCompat;
import android.support.v4.content.res.ResourcesCompat;
@@ -31,6 +30,8 @@
import android.view.WindowManager;
import com.apptentive.android.sdk.adapter.ApptentiveViewPagerAdapter;
+import com.apptentive.android.sdk.conversation.Conversation;
+import com.apptentive.android.sdk.debug.Assert;
import com.apptentive.android.sdk.model.FragmentFactory;
import com.apptentive.android.sdk.module.engagement.EngagementModule;
import com.apptentive.android.sdk.module.engagement.interaction.fragment.ApptentiveBaseFragment;
@@ -39,9 +40,11 @@
import com.apptentive.android.sdk.util.Constants;
import com.apptentive.android.sdk.util.Util;
+import static com.apptentive.android.sdk.ApptentiveNotifications.*;
+import static com.apptentive.android.sdk.debug.Assert.notNull;
-public class ApptentiveViewActivity extends ApptentiveBaseActivity implements ApptentiveBaseFragment.OnFragmentTransitionListener{
+public class ApptentiveViewActivity extends ApptentiveBaseActivity implements ApptentiveBaseFragment.OnFragmentTransitionListener {
private static final String FRAGMENT_TAG = "fragmentTag";
private int fragmentType;
@@ -59,6 +62,12 @@ public class ApptentiveViewActivity extends ApptentiveBaseActivity implements Ap
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ Conversation conversation = notNull(ApptentiveInternal.getInstance().getConversation());
+ if (conversation == null) {
+ finish();
+ return;
+ }
+
Bundle bundle = FragmentFactory.addDisplayModeToFragmentBundle(getIntent().getExtras());
boolean isInteractionModal = bundle.getBoolean(Constants.FragmentConfigKeys.MODAL);
@@ -80,8 +89,8 @@ protected void onCreate(Bundle savedInstanceState) {
if (fragmentType != Constants.FragmentTypes.UNKNOWN) {
if (fragmentType == Constants.FragmentTypes.INTERACTION ||
- fragmentType == Constants.FragmentTypes.MESSAGE_CENTER_ERROR ||
- fragmentType == Constants.FragmentTypes.ABOUT) {
+ fragmentType == Constants.FragmentTypes.MESSAGE_CENTER_ERROR ||
+ fragmentType == Constants.FragmentTypes.ABOUT) {
bundle.putInt("toolbarLayoutId", R.id.apptentive_toolbar);
if (newFragment == null) {
newFragment = FragmentFactory.createFragmentInstance(bundle);
@@ -97,7 +106,7 @@ protected void onCreate(Bundle savedInstanceState) {
if (fragmentType == Constants.FragmentTypes.ENGAGE_INTERNAL_EVENT) {
String eventName = getIntent().getStringExtra(Constants.FragmentConfigKeys.EXTRA);
if (eventName != null) {
- EngagementModule.engageInternal(this, eventName);
+ EngagementModule.engageInternal(this, conversation, eventName);
}
}
finish();
@@ -106,7 +115,7 @@ protected void onCreate(Bundle savedInstanceState) {
}
} catch (Exception e) {
- ApptentiveLog.e("Error creating ApptentiveViewActivity.", e);
+ ApptentiveLog.e(e, "Error creating ApptentiveViewActivity.");
MetricModule.sendError(e, null, null);
}
@@ -126,14 +135,14 @@ protected void onCreate(Bundle savedInstanceState) {
actionBar.setDisplayHomeAsUpEnabled(true);
int navIconResId = newFragment.getToolbarNavigationIconResourceId(getTheme());
// Check if fragment may show an alternative navigation icon
- if ( navIconResId != 0) {
+ if (navIconResId != 0) {
/* In order for the alternative icon has the same color used by toolbar icon,
* need to apply the same color in toolbar theme
* By default colorControlNormal has same value as textColorPrimary defined in toolbar theme overlay
*/
final Drawable alternateUpArrow = ResourcesCompat.getDrawable(getResources(),
- navIconResId,
- getTheme());
+ navIconResId,
+ getTheme());
int colorControlNormal = Util.getThemeColor(ApptentiveInternal.getInstance().getApptentiveToolbarTheme(), R.attr.colorControlNormal);
alternateUpArrow.setColorFilter(colorControlNormal, PorterDuff.Mode.SRC_ATOP);
@@ -144,6 +153,7 @@ protected void onCreate(Bundle savedInstanceState) {
//current_tab = extra.getInt(SELECTED_TAB_EXTRA_KEY, 0);
current_tab = 0;
+ newFragment.setConversation(conversation);
addFragmentToAdapter(newFragment, newFragment.getTitle());
// Get the ViewPager and set it's PagerAdapter so that it can display items
@@ -272,7 +282,7 @@ public void onFragmentTransition(ApptentiveBaseFragment currentFragment) {
}
private void applyApptentiveTheme(boolean isModalInteraction) {
- // Update the activity theme to reflect current attributes
+ // Update the activity theme to reflect current attributes
try {
ApptentiveInternal.getInstance().updateApptentiveInteractionTheme(getTheme(), this);
@@ -288,7 +298,7 @@ private void applyApptentiveTheme(boolean isModalInteraction) {
setTaskDescription(taskDes);
}
} catch (Exception e) {
- ApptentiveLog.e("Error apply Apptentive Theme.", e);
+ ApptentiveLog.e(e, "Error apply Apptentive Theme.");
}
}
@@ -408,12 +418,26 @@ private void setStatusBarColor() {
@Override
public void onReceiveNotification(ApptentiveNotification notification) {
- if (notification.getName().equals(ApptentiveInternal.NOTIFICATION_INTERACTIONS_SHOULD_DISMISS)) {
- if (!isFinishing()) {
- exitActivity(ApptentiveViewExitType.NOTIFICATION);
+ if (notification.hasName(NOTIFICATION_INTERACTIONS_SHOULD_DISMISS)) {
+ dismissActivity();
+ } else if (notification.hasName(NOTIFICATION_CONVERSATION_STATE_DID_CHANGE)) {
+ final Conversation conversation = notification.getUserInfo(NOTIFICATION_KEY_CONVERSATION, Conversation.class);
+ Assert.assertNotNull(conversation, "Conversation expected to be not null");
+ if (conversation != null && !conversation.hasActiveState()) {
+ dismissActivity();
}
}
}
//endregion
+
+ //region Helpers
+
+ private void dismissActivity() {
+ if (!isFinishing()) {
+ exitActivity(ApptentiveViewExitType.NOTIFICATION); // TODO: different exit types for different notifications?
+ }
+ }
+
+ //endregion
}
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 b07adab94..ae156ef60 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
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, Apptentive, Inc. All Rights Reserved.
+ * 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.
*/
@@ -11,12 +11,10 @@
import com.apptentive.android.sdk.ApptentiveInternal;
import com.apptentive.android.sdk.ApptentiveLog;
-import com.apptentive.android.sdk.model.*;
-import com.apptentive.android.sdk.module.messagecenter.model.ApptentiveMessage;
-import com.apptentive.android.sdk.module.messagecenter.model.CompoundMessage;
+import com.apptentive.android.sdk.conversation.Conversation;
import com.apptentive.android.sdk.util.Constants;
+import com.apptentive.android.sdk.util.StringUtils;
import com.apptentive.android.sdk.util.Util;
-import com.apptentive.android.sdk.util.image.ImageUtil;
import java.io.*;
import java.net.HttpURLConnection;
@@ -26,101 +24,69 @@
import java.util.*;
import java.util.zip.GZIPInputStream;
-public class ApptentiveClient {
+import static com.apptentive.android.sdk.debug.Assert.notNull;
- public static final int API_VERSION = 7;
+public class ApptentiveClient {
private 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;
-
// Active API
- private static final String ENDPOINT_CONVERSATION = "/conversation";
- private static final String ENDPOINT_CONVERSATION_FETCH = ENDPOINT_CONVERSATION + "?count=%s&after_id=%s&before_id=%s";
- private static final String ENDPOINT_MESSAGES = "/messages";
- private static final String ENDPOINT_EVENTS = "/events";
- private static final String ENDPOINT_DEVICES = "/devices";
- private static final String ENDPOINT_PEOPLE = "/people";
- private static final String ENDPOINT_CONFIGURATION = ENDPOINT_CONVERSATION + "/configuration";
- private static final String ENDPOINT_SURVEYS_POST = "/surveys/%s/respond";
+ private static final String ENDPOINT_MESSAGES = "/conversations/%s/messages?count=%s&starts_after=%s&before_id=%s";
+ private static final String ENDPOINT_CONFIGURATION = "/conversations/%s/configuration";
- private static final String ENDPOINT_INTERACTIONS = "/interactions";
+ private static final String ENDPOINT_INTERACTIONS = "/conversations/%s/interactions";
// Deprecated API
// private static final String ENDPOINT_RECORDS = ENDPOINT_BASE + "/records";
// private static final String ENDPOINT_SURVEYS_FETCH = ENDPOINT_BASE + "/surveys";
- public static ApptentiveHttpResponse getConversationToken(ConversationTokenRequest conversationTokenRequest) {
- return performHttpRequest(ApptentiveInternal.getInstance().getApptentiveApiKey(), ENDPOINT_CONVERSATION, Method.POST, conversationTokenRequest.toString());
- }
-
- public static ApptentiveHttpResponse getAppConfiguration() {
- return performHttpRequest(ApptentiveInternal.getInstance().getApptentiveConversationToken(), ENDPOINT_CONFIGURATION, Method.GET, null);
- }
-
/**
* Gets all messages since the message specified by GUID was sent.
*
* @return An ApptentiveHttpResponse object with the HTTP response code, reason, and content.
*/
- public static ApptentiveHttpResponse getMessages(Integer count, String afterId, String beforeId) {
- String uri = String.format(ENDPOINT_CONVERSATION_FETCH, count == null ? "" : count.toString(), afterId == null ? "" : afterId, beforeId == null ? "" : beforeId);
- return performHttpRequest(ApptentiveInternal.getInstance().getApptentiveConversationToken(), uri, Method.GET, null);
- }
-
- public static ApptentiveHttpResponse postMessage(ApptentiveMessage apptentiveMessage) {
- switch (apptentiveMessage.getType()) {
- case CompoundMessage: {
- CompoundMessage compoundMessage = (CompoundMessage) apptentiveMessage;
- List associatedFiles = compoundMessage.getAssociatedFiles();
- return performMultipartFilePost(ApptentiveInternal.getInstance().getApptentiveConversationToken(), ENDPOINT_MESSAGES, apptentiveMessage.marshallForSending(), associatedFiles);
- }
- case unknown:
- break;
+ public static ApptentiveHttpResponse getMessages(Conversation conversation, String afterId, String beforeId, Integer count) {
+ if (conversation == null) {
+ throw new IllegalStateException("Conversation is null");
}
- return new ApptentiveHttpResponse();
- }
-
- public static ApptentiveHttpResponse postEvent(Event event) {
- return performHttpRequest(ApptentiveInternal.getInstance().getApptentiveConversationToken(), ENDPOINT_EVENTS, Method.POST, event.marshallForSending());
- }
-
- public static ApptentiveHttpResponse putDevice(Device device) {
- return performHttpRequest(ApptentiveInternal.getInstance().getApptentiveConversationToken(), ENDPOINT_DEVICES, Method.PUT, device.marshallForSending());
- }
- public static ApptentiveHttpResponse putSdk(Sdk sdk) {
- return performHttpRequest(ApptentiveInternal.getInstance().getApptentiveConversationToken(), ENDPOINT_CONVERSATION, Method.PUT, sdk.marshallForSending());
- }
+ final String conversationId = conversation.getConversationId();
+ if (conversationId == null) {
+ throw new IllegalStateException("Conversation id is null");
+ }
- public static ApptentiveHttpResponse putAppRelease(AppRelease appRelease) {
- return performHttpRequest(ApptentiveInternal.getInstance().getApptentiveConversationToken(), ENDPOINT_CONVERSATION, Method.PUT, appRelease.marshallForSending());
- }
+ final String conversationToken = conversation.getConversationToken();
+ if (conversationToken == null) {
+ throw new IllegalStateException("Conversation token is null");
+ }
- public static ApptentiveHttpResponse putPerson(Person person) {
- return performHttpRequest(ApptentiveInternal.getInstance().getApptentiveConversationToken(), ENDPOINT_PEOPLE, Method.PUT, person.marshallForSending());
+ String uri = String.format(ENDPOINT_MESSAGES, conversationId, count == null ? "" : count.toString(), afterId == null ? "" : afterId, beforeId == null ? "" : beforeId);
+ return performHttpRequest(conversationToken, true, uri, Method.GET, null);
}
- public static ApptentiveHttpResponse postSurvey(SurveyResponse survey) {
- String endpoint = String.format(ENDPOINT_SURVEYS_POST, survey.getId());
- return performHttpRequest(ApptentiveInternal.getInstance().getApptentiveConversationToken(), endpoint, Method.POST, survey.marshallForSending());
- }
+ public static ApptentiveHttpResponse getInteractions(String conversationToken, String conversationId) {
+ if (StringUtils.isNullOrEmpty(conversationToken)) {
+ throw new IllegalArgumentException("Conversation token is null or empty");
+ }
- public static ApptentiveHttpResponse getInteractions() {
- return performHttpRequest(ApptentiveInternal.getInstance().getApptentiveConversationToken(), ENDPOINT_INTERACTIONS, Method.GET, null);
+ if (StringUtils.isNullOrEmpty(conversationId)) {
+ throw new IllegalArgumentException("Conversation id is null or empty");
+ }
+ final String endPoint = StringUtils.format(ENDPOINT_INTERACTIONS, conversationId);
+ return performHttpRequest(conversationToken, true, endPoint, Method.GET, null);
}
/**
* Perform a Http request.
*
- * @param oauthToken authorization token for the current connection
+ * @param authToken authorization token for the current connection. Might be an OAuth token for legacy conversations, or Bearer JWT for modern conversations.
+ * @param bearer If true, the token is a bearer JWT, else it is an OAuth token.
* @param uri server url.
* @param method Get/Post/Put
* @param body Data to be POSTed/Put, not used for GET
* @return ApptentiveHttpResponse containing content and response returned from the server.
*/
- private static ApptentiveHttpResponse performHttpRequest(String oauthToken, String uri, Method method, String body) {
+ private static ApptentiveHttpResponse performHttpRequest(String authToken, boolean bearer, String uri, Method method, String body) {
uri = getEndpointBase() + uri;
ApptentiveLog.d("Performing %s request to %s", method.name(), uri);
//ApptentiveLog.e("OAUTH Token: %s", oauthToken);
@@ -138,13 +104,21 @@ private static ApptentiveHttpResponse performHttpRequest(String oauthToken, Stri
connection.setRequestProperty("User-Agent", getUserAgentString());
connection.setRequestProperty("Connection", "Keep-Alive");
- connection.setConnectTimeout(DEFAULT_HTTP_CONNECT_TIMEOUT);
- connection.setReadTimeout(DEFAULT_HTTP_SOCKET_TIMEOUT);
- connection.setRequestProperty("Authorization", "OAuth " + oauthToken);
+ connection.setConnectTimeout(Constants.DEFAULT_CONNECT_TIMEOUT_MILLIS);
+ connection.setReadTimeout(Constants.DEFAULT_READ_TIMEOUT_MILLIS);
+ if (bearer) {
+ connection.setRequestProperty("Authorization", "Bearer " + authToken);
+ } else {
+ connection.setRequestProperty("Authorization", "OAuth " + authToken);
+ }
connection.setRequestProperty("Accept-Encoding", "gzip");
connection.setRequestProperty("Accept", "application/json");
- connection.setRequestProperty("X-API-Version", String.valueOf(API_VERSION));
+ connection.setRequestProperty("X-API-Version", String.valueOf(Constants.API_VERSION));
+ connection.setRequestProperty("APPTENTIVE-KEY", notNull(ApptentiveInternal.getInstance().getApptentiveKey()));
+ connection.setRequestProperty("APPTENTIVE-SIGNATURE", notNull(ApptentiveInternal.getInstance().getApptentiveSignature()));
+
+ ApptentiveLog.vv("Headers: %s", connection.getRequestProperties());
switch (method) {
case GET:
connection.setRequestMethod("GET");
@@ -183,19 +157,19 @@ private static ApptentiveHttpResponse performHttpRequest(String oauthToken, Stri
ApptentiveLog.w("Response: %s", ret.getContent());
}
} catch (IllegalArgumentException e) {
- ApptentiveLog.w("Error communicating with server.", e);
+ ApptentiveLog.w(e, "Error communicating with server.");
} catch (SocketTimeoutException e) {
- ApptentiveLog.w("Timeout communicating with server.", e);
+ ApptentiveLog.w(e, "Timeout communicating with server.");
} catch (final MalformedURLException e) {
- ApptentiveLog.w("MalformedUrlException", e);
+ ApptentiveLog.w(e, "MalformedUrlException");
} catch (final IOException e) {
- ApptentiveLog.w("IOException", e);
+ ApptentiveLog.w(e, "IOException");
// Read the error response.
try {
ret.setContent(getErrorResponse(connection, ret.isZipped()));
ApptentiveLog.w("Response: " + ret.getContent());
} catch (IOException ex) {
- ApptentiveLog.w("Can't read error stream.", ex);
+ ApptentiveLog.w(ex, "Can't read error stream.");
}
}
return ret;
@@ -225,165 +199,6 @@ private static void sendPostPutRequest(final HttpURLConnection connection, final
}
}
- private static ApptentiveHttpResponse performMultipartFilePost(String oauthToken, String uri, String postBody, List associatedFiles) {
- uri = getEndpointBase() + uri;
- ApptentiveLog.d("Performing multipart POST to %s", uri);
- ApptentiveLog.d("Multipart POST body: %s", postBody);
-
- ApptentiveHttpResponse ret = new ApptentiveHttpResponse();
- if (!Util.isNetworkConnectionPresent()) {
- ApptentiveLog.d("Network unavailable.");
- return ret;
- }
-
- String lineEnd = "\r\n";
- String twoHyphens = "--";
- String boundary = UUID.randomUUID().toString();
-
- HttpURLConnection connection = null;
- DataOutputStream os = null;
-
- try {
-
- // Set up the request.
- URL url = new URL(uri);
- connection = (HttpURLConnection) url.openConnection();
- connection.setDoInput(true);
- connection.setDoOutput(true);
- connection.setUseCaches(false);
- connection.setConnectTimeout(DEFAULT_HTTP_CONNECT_TIMEOUT);
- connection.setReadTimeout(DEFAULT_HTTP_SOCKET_TIMEOUT);
- connection.setRequestMethod("POST");
-
- connection.setRequestProperty("Content-Type", "multipart/mixed;boundary=" + boundary);
- connection.setRequestProperty("Authorization", "OAuth " + oauthToken);
- connection.setRequestProperty("Accept", "application/json");
- connection.setRequestProperty("X-API-Version", String.valueOf(API_VERSION));
- connection.setRequestProperty("User-Agent", getUserAgentString());
-
- // Open an output stream.
- os = new DataOutputStream(connection.getOutputStream());
- os.writeBytes(twoHyphens + boundary + lineEnd);
-
- // Write text message
- os.writeBytes("Content-Disposition: form-data; name=\"message\"" + lineEnd);
- // Indicate the character encoding is UTF-8
- os.writeBytes("Content-Type: text/plain;charset=UTF-8" + lineEnd);
-
- os.writeBytes(lineEnd);
- // Explicitly encode message json in utf-8
- os.write(postBody.getBytes("UTF-8"));
- os.writeBytes(lineEnd);
-
-
- // Send associated files
- if (associatedFiles != null) {
- for (StoredFile storedFile : associatedFiles) {
- FileInputStream fis = null;
- try {
- String cachedImagePathString = storedFile.getLocalFilePath();
- String originalFilePath = storedFile.getSourceUriOrPath();
- File cachedImageFile = new File(cachedImagePathString);
- // No local cache found
- if (!cachedImageFile.exists()) {
- boolean bCachedCreated = false;
- if (Util.isMimeTypeImage(storedFile.getMimeType())) {
- // Create a scaled down version of original image
- bCachedCreated = ImageUtil.createScaledDownImageCacheFile(originalFilePath, cachedImagePathString);
- } else {
- // For non-image file, just copy to a cache file
- if (Util.createLocalStoredFile(originalFilePath, cachedImagePathString, null) != null) {
- bCachedCreated = true;
- }
- }
-
- if (!bCachedCreated) {
- continue;
- }
- }
- os.writeBytes(twoHyphens + boundary + lineEnd);
- StringBuilder requestText = new StringBuilder();
- String fileFullPathName = originalFilePath;
- if (TextUtils.isEmpty(fileFullPathName)) {
- fileFullPathName = cachedImagePathString;
- }
- requestText.append(String.format("Content-Disposition: form-data; name=\"file[]\"; filename=\"%s\"", fileFullPathName)).append(lineEnd);
- requestText.append("Content-Type: ").append(storedFile.getMimeType()).append(lineEnd);
- // Write file attributes
- os.writeBytes(requestText.toString());
- os.writeBytes(lineEnd);
-
- fis = new FileInputStream(cachedImageFile);
-
- int bytesAvailable = fis.available();
- int maxBufferSize = 512 * 512;
- int bufferSize = Math.min(bytesAvailable, maxBufferSize);
- byte[] buffer = new byte[bufferSize];
-
- // read image data 0.5MB at a time and write it into buffer
- int bytesRead = fis.read(buffer, 0, bufferSize);
- while (bytesRead > 0) {
- os.write(buffer, 0, bufferSize);
- bytesAvailable = fis.available();
- bufferSize = Math.min(bytesAvailable, maxBufferSize);
- bytesRead = fis.read(buffer, 0, bufferSize);
- }
- } catch (IOException e) {
- ApptentiveLog.d("Error writing file bytes to HTTP connection.", e);
- ret.setBadPayload(true);
- throw e;
- } finally {
- Util.ensureClosed(fis);
- }
- os.writeBytes(lineEnd);
- }
- }
- os.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
-
- os.flush();
- os.close();
-
- ret.setCode(connection.getResponseCode());
- ret.setReason(connection.getResponseMessage());
-
- // Read the normal response.
- InputStream responseInputStream = null;
- ByteArrayOutputStream byteArrayOutputStream = null;
- try {
- responseInputStream = connection.getInputStream();
- byteArrayOutputStream = new ByteArrayOutputStream();
- byte[] eBuf = new byte[1024];
- int eRead;
- while (responseInputStream != null && (eRead = responseInputStream.read(eBuf, 0, 1024)) > 0) {
- byteArrayOutputStream.write(eBuf, 0, eRead);
- }
- ret.setContent(byteArrayOutputStream.toString());
- } finally {
- Util.ensureClosed(responseInputStream);
- Util.ensureClosed(byteArrayOutputStream);
- }
-
- ApptentiveLog.d("HTTP %d: %s", connection.getResponseCode(), connection.getResponseMessage());
- ApptentiveLog.v("Response: %s", ret.getContent());
- } catch (FileNotFoundException e) {
- ApptentiveLog.e("Error getting file to upload.", e);
- } catch (MalformedURLException e) {
- ApptentiveLog.e("Error constructing url for file upload.", e);
- } catch (SocketTimeoutException e) {
- ApptentiveLog.w("Timeout communicating with server.");
- } catch (IOException e) {
- ApptentiveLog.e("Error executing file upload.", e);
- try {
- ret.setContent(getErrorResponse(connection, ret.isZipped()));
- } catch (IOException ex) {
- ApptentiveLog.w("Can't read error stream.", ex);
- }
- } finally {
- Util.ensureClosed(os);
- }
- return ret;
- }
-
private enum Method {
GET,
PUT,
@@ -395,10 +210,10 @@ public static String getUserAgentString() {
}
private static String getEndpointBase() {
- SharedPreferences prefs = ApptentiveInternal.getInstance().getSharedPrefs();
+ SharedPreferences prefs = ApptentiveInternal.getInstance().getGlobalSharedPrefs();
String url = prefs.getString(Constants.PREF_KEY_SERVER_URL, null);
if (url == null) {
- url = Constants.CONFIG_DEFAULT_SERVER_URL;
+ url = ApptentiveInternal.getInstance().getServerUrl();
prefs.edit().putString(Constants.PREF_KEY_SERVER_URL, url).apply();
}
return url;
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
new file mode 100644
index 000000000..fe30a945c
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/comm/ApptentiveHttpClient.java
@@ -0,0 +1,240 @@
+/*
+ * 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.comm;
+
+import com.apptentive.android.sdk.model.ConversationTokenRequest;
+import com.apptentive.android.sdk.model.PayloadData;
+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.RawHttpRequest;
+import com.apptentive.android.sdk.storage.AppRelease;
+import com.apptentive.android.sdk.storage.AppReleaseManager;
+import com.apptentive.android.sdk.storage.Device;
+import com.apptentive.android.sdk.storage.DeviceManager;
+import com.apptentive.android.sdk.storage.PayloadRequestSender;
+import com.apptentive.android.sdk.storage.Sdk;
+import com.apptentive.android.sdk.storage.SdkManager;
+import com.apptentive.android.sdk.util.Constants;
+import com.apptentive.android.sdk.util.StringUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import static com.apptentive.android.sdk.debug.Assert.notNull;
+
+/**
+ * Class responsible for all client-server network communications using asynchronous HTTP requests
+ */
+public class ApptentiveHttpClient implements PayloadRequestSender {
+
+ private static final String USER_AGENT_STRING = "Apptentive/%s (Android)"; // Format with SDK version string.
+
+ 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";
+ private static final String ENDPOINT_LEGACY_CONVERSATION = "/conversation/token";
+ private static final String ENDPOINT_LOG_IN_TO_EXISTING_CONVERSATION = "/conversations/%s/session";
+ private static final String ENDPOINT_LOG_IN_TO_NEW_CONVERSATION = "/conversations";
+
+ private final String apptentiveKey;
+ 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)) {
+ throw new IllegalArgumentException("Illegal Apptentive Key: '" + apptentiveKey + "'");
+ }
+
+ if (StringUtils.isNullOrEmpty(apptentiveSignature)) {
+ throw new IllegalArgumentException("Illegal Apptentive Signature: '" + apptentiveSignature + "'");
+ }
+
+ if (StringUtils.isNullOrEmpty(serverURL)) {
+ 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);
+ }
+
+ //region API Requests
+
+ public HttpJsonRequest createConversationTokenRequest(ConversationTokenRequest conversationTokenRequest, HttpRequest.Listener listener) {
+ HttpJsonRequest request = createJsonRequest(ENDPOINT_CONVERSATION, conversationTokenRequest, HttpRequestMethod.POST);
+ request.addListener(listener);
+ return request;
+ }
+
+ public HttpJsonRequest createLegacyConversationIdRequest(String conversationToken, HttpRequest.Listener listener) {
+ if (StringUtils.isNullOrEmpty(conversationToken)) {
+ throw new IllegalArgumentException("Conversation token is null or empty");
+ }
+
+ HttpJsonRequest request = createJsonRequest(ENDPOINT_LEGACY_CONVERSATION, new JSONObject(), HttpRequestMethod.GET);
+ request.setRequestProperty("Authorization", "OAuth " + conversationToken);
+ request.addListener(listener);
+ return request;
+ }
+
+ public HttpJsonRequest createLoginRequest(String conversationId, String token, HttpRequest.Listener listener) {
+ if (token == null) {
+ throw new IllegalArgumentException("Token is null");
+ }
+
+ JSONObject json = new JSONObject();
+ try {
+ json.put("token", token);
+ } catch (JSONException e) {
+ // Can't happen
+ }
+ String endPoint;
+ if (conversationId == null) {
+ endPoint = ENDPOINT_LOG_IN_TO_NEW_CONVERSATION;
+
+ }else {
+ endPoint = StringUtils.format(ENDPOINT_LOG_IN_TO_EXISTING_CONVERSATION, conversationId);
+ }
+ HttpJsonRequest request = createJsonRequest(endPoint, json, HttpRequestMethod.POST);
+ request.addListener(listener);
+ return request;
+ }
+
+ public HttpJsonRequest createFirstLoginRequest(String token, AppRelease appRelease, Sdk sdk, Device device, HttpRequest.Listener listener) {
+ if (token == null) {
+ throw new IllegalArgumentException("Token is null");
+ }
+
+ ConversationTokenRequest conversationTokenRequest = new ConversationTokenRequest();
+ conversationTokenRequest.setSdkAndAppRelease(SdkManager.getPayload(sdk), AppReleaseManager.getPayload(appRelease));
+ conversationTokenRequest.setDevice(DeviceManager.getDiffPayload(null, device));
+
+ try {
+ conversationTokenRequest.put("token", token);
+ } catch (JSONException e) {
+ // Can't happen
+ }
+
+ HttpJsonRequest request = createJsonRequest(ENDPOINT_LOG_IN_TO_NEW_CONVERSATION, conversationTokenRequest, HttpRequestMethod.POST);
+ request.addListener(listener);
+ return request;
+ }
+
+ /**
+ * Returns the first request with a given tag or null is not found
+ */
+ public HttpRequest findRequest(String tag) {
+ return httpRequestManager.findRequest(tag);
+ }
+
+ //endregion
+
+ //region PayloadRequestSender
+
+ @Override
+ public HttpRequest createPayloadSendRequest(PayloadData payload, HttpRequest.Listener listener) {
+ if (payload == null) {
+ throw new IllegalArgumentException("Payload is null");
+ }
+
+ HttpRequest request = createPayloadRequest(payload);
+ request.addListener(listener);
+ return request;
+ }
+
+ private HttpRequest createPayloadRequest(PayloadData payload) {
+ final String authToken = payload.getAuthToken();
+ final String httpPath = notNull(payload.getHttpRequestPath());
+ final HttpRequestMethod requestMethod = notNull(payload.getHttpRequestMethod());
+ final String contentType = notNull(payload.getContentType());
+
+ HttpRequest request = createRawRequest(httpPath, payload.getData(), requestMethod, contentType);
+
+ // Encrypted requests don't use an Auth token on the request. It's stored in the encrypted body.
+ if (!StringUtils.isNullOrEmpty(authToken)) {
+ request.setRequestProperty("Authorization", "Bearer " + authToken);
+ }
+
+ if (payload.isEncrypted()) {
+ request.setRequestProperty("APPTENTIVE-ENCRYPTED", Boolean.TRUE);
+ }
+
+ return request;
+ }
+
+ //endregion
+
+ //region Helpers
+
+ private HttpJsonRequest createJsonRequest(String endpoint, JSONObject json, HttpRequestMethod method) {
+ if (endpoint == null) {
+ throw new IllegalArgumentException("Endpoint is null");
+ }
+ if (json == null) {
+ throw new IllegalArgumentException("Json is null");
+ }
+ if (method == null) {
+ throw new IllegalArgumentException("Method is null");
+ }
+
+ String url = createEndpointURL(endpoint);
+ HttpJsonRequest request = new HttpJsonRequest(url, json);
+ setupRequestDefaults(request);
+ request.setMethod(method);
+ request.setRequestProperty("Content-Type", "application/json");
+ return request;
+ }
+
+ private RawHttpRequest createRawRequest(String endpoint, byte[] data, HttpRequestMethod method, String contentType) {
+ if (endpoint == null) {
+ throw new IllegalArgumentException("Endpoint is null");
+ }
+ if (data == null) {
+ throw new IllegalArgumentException("Payload is null");
+ }
+ if (method == null) {
+ throw new IllegalArgumentException("Method is null");
+ }
+ if (contentType == null) {
+ throw new IllegalArgumentException("ContentType is null");
+ }
+
+ String url = createEndpointURL(endpoint);
+ RawHttpRequest request = new RawHttpRequest(url, data);
+ setupRequestDefaults(request);
+ request.setMethod(method);
+ request.setRequestProperty("Content-Type", contentType);
+ return request;
+ }
+
+ private void setupRequestDefaults(HttpRequest request) {
+ request.setRequestManager(httpRequestManager);
+ request.setRequestProperty("User-Agent", userAgentString);
+ request.setRequestProperty("Connection", "Keep-Alive");
+ request.setRequestProperty("Accept-Encoding", "gzip");
+ request.setRequestProperty("Accept", "application/json");
+ request.setRequestProperty("APPTENTIVE-KEY", apptentiveKey);
+ request.setRequestProperty("APPTENTIVE-SIGNATURE", apptentiveSignature);
+ request.setRequestProperty("X-API-Version", String.valueOf(Constants.API_VERSION));
+ request.setConnectTimeout(DEFAULT_HTTP_CONNECT_TIMEOUT);
+ request.setReadTimeout(DEFAULT_HTTP_SOCKET_TIMEOUT);
+ }
+
+ private String createEndpointURL(String uri) {
+ return serverURL + uri;
+ }
+
+ //endregion
+}
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
new file mode 100644
index 000000000..eb826aa36
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/conversation/Conversation.java
@@ -0,0 +1,663 @@
+/*
+ * 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.conversation;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.apptentive.android.sdk.Apptentive;
+import com.apptentive.android.sdk.ApptentiveInternal;
+import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.comm.ApptentiveClient;
+import com.apptentive.android.sdk.comm.ApptentiveHttpResponse;
+import com.apptentive.android.sdk.debug.Assert;
+import com.apptentive.android.sdk.model.DevicePayload;
+import com.apptentive.android.sdk.model.Payload;
+import com.apptentive.android.sdk.model.PersonPayload;
+import com.apptentive.android.sdk.module.engagement.interaction.model.Interaction;
+import com.apptentive.android.sdk.module.engagement.interaction.model.InteractionManifest;
+import com.apptentive.android.sdk.module.engagement.interaction.model.Interactions;
+import com.apptentive.android.sdk.module.engagement.interaction.model.Targets;
+import com.apptentive.android.sdk.module.messagecenter.MessageManager;
+import com.apptentive.android.sdk.storage.AppRelease;
+import com.apptentive.android.sdk.storage.DataChangedListener;
+import com.apptentive.android.sdk.storage.Device;
+import com.apptentive.android.sdk.storage.DeviceManager;
+import com.apptentive.android.sdk.storage.EncryptedFileSerializer;
+import com.apptentive.android.sdk.storage.EventData;
+import com.apptentive.android.sdk.storage.FileSerializer;
+import com.apptentive.android.sdk.storage.IntegrationConfig;
+import com.apptentive.android.sdk.storage.IntegrationConfigItem;
+import com.apptentive.android.sdk.storage.Person;
+import com.apptentive.android.sdk.storage.PersonManager;
+import com.apptentive.android.sdk.storage.Sdk;
+import com.apptentive.android.sdk.storage.SerializerException;
+import com.apptentive.android.sdk.storage.VersionHistory;
+import com.apptentive.android.sdk.util.Constants;
+import com.apptentive.android.sdk.util.Destroyable;
+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.JSONException;
+
+import java.io.File;
+
+import static com.apptentive.android.sdk.debug.Assert.assertFail;
+import static com.apptentive.android.sdk.debug.Assert.assertNotNull;
+import static com.apptentive.android.sdk.debug.Assert.notNull;
+import static com.apptentive.android.sdk.debug.Tester.dispatchDebugEvent;
+import static com.apptentive.android.sdk.ApptentiveLogTag.*;
+import static com.apptentive.android.sdk.conversation.ConversationState.*;
+import static com.apptentive.android.sdk.debug.TesterEvent.*;
+
+public class Conversation implements DataChangedListener, Destroyable {
+
+ /**
+ * Conversation data for this class to manage
+ */
+ private ConversationData conversationData;
+
+ /**
+ * Encryption key for payloads. A hex encoded String.
+ */
+ private String encryptionKey;
+
+ /**
+ * Optional user id for logged-in conversations
+ */
+ private String userId;
+
+ /**
+ * Optional JWT for active conversations
+ */
+ private String JWT;
+
+ /**
+ * File which represents serialized conversation data on the disk
+ */
+ private final File conversationDataFile;
+
+ /**
+ * File which represents serialized messages data on the disk
+ */
+ private final File conversationMessagesFile;
+
+ /**
+ * Internal flag to turn interaction polling on and off fir testing.
+ */
+ private Boolean pollForInteractions;
+
+ private ConversationState state = ConversationState.UNDEFINED;
+
+ private final MessageManager messageManager;
+
+ // we keep references to the tasks in order to dispatch them only once
+ private final DispatchTask fetchInteractionsTask = new DispatchTask() {
+ @Override
+ protected void execute() {
+ final boolean updateSuccessful = fetchInteractionsSync();
+ dispatchDebugEvent(EVT_CONVERSATION_FETCH_INTERACTIONS, updateSuccessful);
+
+ // Update pending state on UI thread after finishing the task
+ DispatchQueue.mainQueue().dispatchAsync(new DispatchTask() {
+ @Override
+ protected void execute() {
+ if (hasActiveState()) {
+ ApptentiveInternal.getInstance().notifyInteractionUpdated(updateSuccessful);
+ }
+ }
+ });
+ }
+ };
+
+ // we keep references to the tasks in order to dispatch them only once
+ private final DispatchTask saveConversationTask = new DispatchTask() {
+ @Override
+ protected void execute() {
+ try {
+ saveConversationData();
+ } catch (Exception e) {
+ ApptentiveLog.e(e, "Exception while saving conversation data");
+ }
+ }
+ };
+
+ public Conversation(File conversationDataFile, File conversationMessagesFile) {
+ if (conversationDataFile == null) {
+ throw new IllegalArgumentException("Data file is null");
+ }
+ if (conversationMessagesFile == null) {
+ throw new IllegalArgumentException("Messages file is null");
+ }
+
+ this.conversationDataFile = conversationDataFile;
+ this.conversationMessagesFile = conversationMessagesFile;
+
+ conversationData = new ConversationData();
+
+ FileMessageStore messageStore = new FileMessageStore(conversationMessagesFile);
+ messageManager = new MessageManager(this, messageStore); // it's important to initialize message manager in a constructor since other SDK parts depend on it via Apptentive singleton
+ }
+
+ public void startListeningForChanges() {
+ conversationData.setDataChangedListener(this);
+ }
+
+ //region Payloads
+
+ public void addPayload(Payload payload) {
+ payload.setLocalConversationIdentifier(notNull(getLocalIdentifier()));
+ payload.setConversationId(getConversationId());
+ payload.setToken(getConversationToken());
+ payload.setEncryptionKey(getEncryptionKey());
+
+ // FIXME: don't use singleton here
+ ApptentiveInternal.getInstance().getApptentiveTaskManager().addPayload(payload);
+ }
+
+ //endregion
+
+ //region Interactions
+
+ /**
+ * Returns an Interaction for eventLabel if there is one that can be displayed.
+ */
+ public Interaction getApplicableInteraction(String eventLabel) {
+ String targetsString = getTargets();
+ if (targetsString != null) {
+ try {
+ Targets targets = new Targets(getTargets());
+ String interactionId = targets.getApplicableInteraction(eventLabel);
+ if (interactionId != null) {
+ String interactionsString = getInteractions();
+ if (interactionsString != null) {
+ Interactions interactions = new Interactions(interactionsString);
+ return interactions.getInteraction(interactionId);
+ }
+ }
+ } catch (JSONException e) {
+ ApptentiveLog.e(e, "Exception while getting applicable interaction: %s", eventLabel);
+ }
+ }
+ return null;
+ }
+
+ boolean fetchInteractions(Context context) {
+ boolean cacheExpired = getInteractionExpiration() > Util.currentTimeSeconds();
+ if (cacheExpired || RuntimeUtils.isAppDebuggable(context)) {
+ return DispatchQueue.backgroundQueue().dispatchAsyncOnce(fetchInteractionsTask); // do not allow multiple fetches at the same time
+ }
+
+ ApptentiveLog.v(CONVERSATION, "Interaction cache is still valid");
+ return false;
+ }
+
+ /**
+ * Fetches interaction synchronously. Returns true if succeed.
+ */
+ private boolean fetchInteractionsSync() {
+ ApptentiveLog.v(CONVERSATION, "Fetching Interactions");
+ ApptentiveHttpResponse response = ApptentiveClient.getInteractions(getConversationToken(), getConversationId());
+
+ SharedPreferences prefs = ApptentiveInternal.getInstance().getGlobalSharedPrefs();
+ boolean updateSuccessful = true;
+
+ // We weren't able to connect to the internet.
+ if (response.isException()) {
+ prefs.edit().putBoolean(Constants.PREF_KEY_MESSAGE_CENTER_SERVER_ERROR_LAST_ATTEMPT, false).apply();
+ updateSuccessful = false;
+ }
+ // We got a server error.
+ else if (!response.isSuccessful()) {
+ prefs.edit().putBoolean(Constants.PREF_KEY_MESSAGE_CENTER_SERVER_ERROR_LAST_ATTEMPT, true).apply();
+ updateSuccessful = false;
+ }
+
+ if (updateSuccessful) {
+ String interactionsPayloadString = response.getContent();
+
+ // Store new integration cache expiration.
+ String cacheControl = response.getHeaders().get("Cache-Control");
+ Integer cacheSeconds = Util.parseCacheControlHeader(cacheControl);
+ if (cacheSeconds == null) {
+ cacheSeconds = Constants.CONFIG_DEFAULT_INTERACTION_CACHE_EXPIRATION_DURATION_SECONDS;
+ }
+ setInteractionExpiration(Util.currentTimeSeconds() + cacheSeconds);
+ try {
+ InteractionManifest payload = new InteractionManifest(interactionsPayloadString);
+ Interactions interactions = payload.getInteractions();
+ Targets targets = payload.getTargets();
+ if (interactions != null && targets != null) {
+ setTargets(targets.toString());
+ setInteractions(interactions.toString());
+ } else {
+ ApptentiveLog.e(CONVERSATION, "Unable to save interactionManifest.");
+ }
+ } catch (JSONException e) {
+ ApptentiveLog.e(e, "Invalid InteractionManifest received.");
+ }
+ }
+ ApptentiveLog.v(CONVERSATION, "Fetching new Interactions task finished. Successful: %b", updateSuccessful);
+
+ return updateSuccessful;
+ }
+
+ public boolean isPollForInteractions() {
+ if (pollForInteractions == null) {
+ SharedPreferences prefs = ApptentiveInternal.getInstance().getGlobalSharedPrefs();
+ pollForInteractions = prefs.getBoolean(Constants.PREF_KEY_POLL_FOR_INTERACTIONS, true);
+ }
+ return pollForInteractions;
+ }
+
+ public void setPollForInteractions(boolean pollForInteractions) {
+ this.pollForInteractions = pollForInteractions;
+ SharedPreferences prefs = ApptentiveInternal.getInstance().getGlobalSharedPrefs();
+ prefs.edit().putBoolean(Constants.PREF_KEY_POLL_FOR_INTERACTIONS, pollForInteractions).apply();
+ }
+
+ /**
+ * Made public for testing. There is no other reason to use this method directly.
+ */
+ public void storeInteractionManifest(String interactionManifest) {
+ try {
+ InteractionManifest payload = new InteractionManifest(interactionManifest);
+ Interactions interactions = payload.getInteractions();
+ Targets targets = payload.getTargets();
+ if (interactions != null && targets != null) {
+ setTargets(targets.toString());
+ setInteractions(interactions.toString());
+ } else {
+ ApptentiveLog.e("Unable to save InteractionManifest.");
+ }
+ } catch (JSONException e) {
+ ApptentiveLog.w("Invalid InteractionManifest received.");
+ }
+ }
+
+ //endregion
+
+ //region Saving
+
+ public void scheduleSaveConversationData() {
+ boolean scheduled = DispatchQueue.backgroundQueue().dispatchAsyncOnce(saveConversationTask, 100L);
+ if (scheduled) {
+ ApptentiveLog.d(CONVERSATION, "Scheduling conversation save.");
+ } else {
+ ApptentiveLog.d(CONVERSATION, "Conversation save already scheduled.");
+ }
+ }
+
+ /**
+ * Saves conversation data to the disk synchronously. Returns true
+ * 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());
+ }
+ long start = System.currentTimeMillis();
+
+ FileSerializer serializer;
+ if (!StringUtils.isNullOrEmpty(encryptionKey)) {
+ Assert.assertFalse(hasState(ANONYMOUS, ANONYMOUS_PENDING, LEGACY_PENDING));
+ serializer = new EncryptedFileSerializer(conversationDataFile, encryptionKey);
+ } else {
+ Assert.assertTrue(hasState(ANONYMOUS, ANONYMOUS_PENDING, LEGACY_PENDING), "Unexpected conversation state: %s", getState());
+ serializer = new FileSerializer(conversationDataFile);
+ }
+
+ serializer.serialize(conversationData);
+ ApptentiveLog.vv(CONVERSATION, "Conversation data saved (took %d ms)", System.currentTimeMillis() - start);
+ }
+
+ synchronized void loadConversationData() throws SerializerException {
+ long start = System.currentTimeMillis();
+
+ FileSerializer serializer;
+ if (!StringUtils.isNullOrEmpty(encryptionKey)) {
+ serializer = new EncryptedFileSerializer(conversationDataFile, encryptionKey);
+ } else {
+ serializer = new FileSerializer(conversationDataFile);
+ }
+
+ ApptentiveLog.d(CONVERSATION, "Loading %sconversation data...", hasState(LOGGED_IN) ? "encrypted " : "");
+ conversationData = (ConversationData) serializer.deserialize();
+ ApptentiveLog.d(CONVERSATION, "Conversation data loaded (took %d ms)", System.currentTimeMillis() - start);
+ }
+
+ //endregion
+
+ //region Listeners
+
+ @Override
+ public void onDataChanged() {
+ scheduleSaveConversationData();
+ }
+
+ //endregion
+
+ //region Destroyable
+
+ @Override
+ public void destroy() {
+ messageManager.destroy();
+ }
+
+ //endregion
+
+ //region Diffs & Updates
+
+ private final DispatchTask personUpdateTask = new DispatchTask() {
+ @Override
+ protected void execute() {
+ Person lastSentPerson = getLastSentPerson();
+ Person currentPerson = getPerson();
+ assertNotNull(currentPerson, "Current person object is null");
+ PersonPayload personPayload = PersonManager.getDiffPayload(lastSentPerson, currentPerson);
+ if (personPayload != null) {
+ addPayload(personPayload);
+ setLastSentPerson(currentPerson != null ? currentPerson.clone() : null);
+ }
+ }
+ };
+
+ private final DispatchTask deviceUpdateTask = new DispatchTask() {
+ @Override
+ protected void execute() {
+ Device lastSentDevice = getLastSentDevice();
+ Device currentDevice = getDevice();
+ assertNotNull(currentDevice, "Current device object is null");
+ DevicePayload devicePayload = DeviceManager.getDiffPayload(lastSentDevice, currentDevice);
+ if (devicePayload != null) {
+ addPayload(devicePayload);
+ setLastSentDevice(currentDevice != null ? currentDevice.clone() : null);
+ }
+ }
+ };
+
+ public void schedulePersonUpdate() {
+ DispatchQueue.mainQueue().dispatchAsyncOnce(personUpdateTask);
+ }
+
+ public void scheduleDeviceUpdate() {
+ DispatchQueue.mainQueue().dispatchAsyncOnce(deviceUpdateTask);
+ }
+
+ //endregion
+
+ //region Getters & Setters
+
+ public String getLocalIdentifier() {
+ return getConversationData().getLocalIdentifier();
+ }
+
+ public ConversationState getState() {
+ return state;
+ }
+
+ public void setState(ConversationState state) {
+ // TODO: check if state transition would make sense (for example you should not be able to move from 'logged' state to 'anonymous', etc.)
+ this.state = state;
+ }
+
+ /**
+ * Returns true if conversation is in the given state
+ */
+ public boolean hasState(ConversationState s) {
+ return state.equals(s);
+ }
+
+ /**
+ * Returns true if conversation is in one of the given states
+ */
+ public boolean hasState(ConversationState... states) {
+ for (ConversationState s : states) {
+ if (s.equals(state)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if conversation is in "active" state (after receiving server response)
+ */
+ public boolean hasActiveState() {
+ return hasState(ConversationState.LOGGED_IN, ANONYMOUS);
+ }
+
+ public String getConversationToken() {
+ return getConversationData().getConversationToken();
+ }
+
+ public void setConversationToken(String conversationToken) {
+ getConversationData().setConversationToken(conversationToken);
+ }
+
+ public String getConversationId() {
+ return getConversationData().getConversationId();
+ }
+
+ public void setConversationId(String conversationId) {
+ getConversationData().setConversationId(conversationId);
+ }
+
+ public Device getDevice() {
+ return getConversationData().getDevice();
+ }
+
+ public void setDevice(Device device) {
+ getConversationData().setDevice(device);
+ }
+
+ public Device getLastSentDevice() {
+ return getConversationData().getLastSentDevice();
+ }
+
+ public void setLastSentDevice(Device lastSentDevice) {
+ getConversationData().setLastSentDevice(lastSentDevice);
+ }
+
+ public Person getPerson() {
+ return getConversationData().getPerson();
+ }
+
+ public void setPerson(Person person) {
+ getConversationData().setPerson(person);
+ }
+
+ public Person getLastSentPerson() {
+ return getConversationData().getLastSentPerson();
+ }
+
+ public void setLastSentPerson(Person lastSentPerson) {
+ getConversationData().setLastSentPerson(lastSentPerson);
+ }
+
+ public Sdk getSdk() {
+ return getConversationData().getSdk();
+ }
+
+ public void setSdk(Sdk sdk) {
+ getConversationData().setSdk(sdk);
+ }
+
+ public AppRelease getAppRelease() {
+ return getConversationData().getAppRelease();
+ }
+
+ public void setAppRelease(AppRelease appRelease) {
+ getConversationData().setAppRelease(appRelease);
+ }
+
+ public EventData getEventData() {
+ return getConversationData().getEventData();
+ }
+
+ public void setEventData(EventData eventData) {
+ getConversationData().setEventData(eventData);
+ }
+
+ public String getLastSeenSdkVersion() {
+ return getConversationData().getLastSeenSdkVersion();
+ }
+
+ public void setLastSeenSdkVersion(String lastSeenSdkVersion) {
+ getConversationData().setLastSeenSdkVersion(lastSeenSdkVersion);
+ }
+
+ public VersionHistory getVersionHistory() {
+ return getConversationData().getVersionHistory();
+ }
+
+ public void setVersionHistory(VersionHistory versionHistory) {
+ getConversationData().setVersionHistory(versionHistory);
+ }
+
+ public boolean isMessageCenterFeatureUsed() {
+ return getConversationData().isMessageCenterFeatureUsed();
+ }
+
+ public void setMessageCenterFeatureUsed(boolean messageCenterFeatureUsed) {
+ getConversationData().setMessageCenterFeatureUsed(messageCenterFeatureUsed);
+ }
+
+ public boolean isMessageCenterWhoCardPreviouslyDisplayed() {
+ return getConversationData().isMessageCenterWhoCardPreviouslyDisplayed();
+ }
+
+ public void setMessageCenterWhoCardPreviouslyDisplayed(boolean messageCenterWhoCardPreviouslyDisplayed) {
+ getConversationData().setMessageCenterWhoCardPreviouslyDisplayed(messageCenterWhoCardPreviouslyDisplayed);
+ }
+
+ public String getMessageCenterPendingMessage() {
+ return getConversationData().getMessageCenterPendingMessage();
+ }
+
+ public void setMessageCenterPendingMessage(String messageCenterPendingMessage) {
+ getConversationData().setMessageCenterPendingMessage(messageCenterPendingMessage);
+ }
+
+ public String getMessageCenterPendingAttachments() {
+ return getConversationData().getMessageCenterPendingAttachments();
+ }
+
+ public void setMessageCenterPendingAttachments(String messageCenterPendingAttachments) {
+ getConversationData().setMessageCenterPendingAttachments(messageCenterPendingAttachments);
+ }
+
+ public String getTargets() {
+ return getConversationData().getTargets();
+ }
+
+ public void setTargets(String targets) {
+ getConversationData().setTargets(targets);
+ }
+
+ public String getInteractions() {
+ return getConversationData().getInteractions();
+ }
+
+ public void setInteractions(String interactions) {
+ getConversationData().setInteractions(interactions);
+ }
+
+ public double getInteractionExpiration() {
+ return getConversationData().getInteractionExpiration();
+ }
+
+ public void setInteractionExpiration(double interactionExpiration) {
+ getConversationData().setInteractionExpiration(interactionExpiration);
+ }
+
+ // this is a synchronization hack: both save/load conversation data are synchronized so we can't
+ // modify conversation data while it's being serialized/deserialized
+ private synchronized ConversationData getConversationData() {
+ return conversationData;
+ }
+
+ public MessageManager getMessageManager() {
+ return messageManager;
+ }
+
+ synchronized File getConversationDataFile() {
+ return conversationDataFile;
+ }
+
+ synchronized File getConversationMessagesFile() {
+ return conversationMessagesFile;
+ }
+
+ public String getEncryptionKey() {
+ return encryptionKey;
+ }
+
+ void setEncryptionKey(String encryptionKey) {
+ this.encryptionKey = encryptionKey;
+ }
+
+ String getUserId() {
+ return userId;
+ }
+
+ void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public void setPushIntegration(int pushProvider, String token) {
+ ApptentiveLog.v(CONVERSATION, "Setting push provider: %d with token %s", pushProvider, token);
+ IntegrationConfig integrationConfig = getDevice().getIntegrationConfig();
+ IntegrationConfigItem item = new IntegrationConfigItem();
+ item.setToken(token);
+ switch (pushProvider) {
+ case Apptentive.PUSH_PROVIDER_APPTENTIVE:
+ integrationConfig.setApptentive(item);
+ break;
+ case Apptentive.PUSH_PROVIDER_PARSE:
+ integrationConfig.setParse(item);
+ break;
+ case Apptentive.PUSH_PROVIDER_URBAN_AIRSHIP:
+ integrationConfig.setUrbanAirship(item);
+ break;
+ case Apptentive.PUSH_PROVIDER_AMAZON_AWS_SNS:
+ integrationConfig.setAmazonAwsSns(item);
+ break;
+ default:
+ ApptentiveLog.e("Invalid pushProvider: %d", pushProvider);
+ break;
+ }
+ scheduleDeviceUpdate();
+ }
+
+ /**
+ * Checks the internal consistency of the conversation object (temporary solution)
+ */
+ void checkInternalConsistency() throws IllegalStateException {
+ switch (state) {
+ case LOGGED_IN:
+ if (StringUtils.isNullOrEmpty(encryptionKey)) {
+ assertFail("Missing encryption key");
+ throw new IllegalStateException("Missing encryption key");
+ }
+ if (StringUtils.isNullOrEmpty(userId)) {
+ assertFail("Missing user id");
+ throw new IllegalStateException("Missing user id");
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ //endregion
+}
diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/conversation/ConversationData.java b/apptentive/src/main/java/com/apptentive/android/sdk/conversation/ConversationData.java
new file mode 100644
index 000000000..85449a37c
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/conversation/ConversationData.java
@@ -0,0 +1,280 @@
+/*
+ * 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.conversation;
+
+import com.apptentive.android.sdk.debug.Assert;
+import com.apptentive.android.sdk.storage.AppRelease;
+import com.apptentive.android.sdk.storage.DataChangedListener;
+import com.apptentive.android.sdk.storage.Device;
+import com.apptentive.android.sdk.storage.EventData;
+import com.apptentive.android.sdk.storage.Person;
+import com.apptentive.android.sdk.storage.Saveable;
+import com.apptentive.android.sdk.storage.Sdk;
+import com.apptentive.android.sdk.storage.VersionHistory;
+import com.apptentive.android.sdk.util.StringUtils;
+
+import java.util.UUID;
+
+public class ConversationData implements Saveable, DataChangedListener {
+
+ private static final long serialVersionUID = 1L;
+ private String localIdentifier;
+ private String conversationToken;
+ private String conversationId;
+ private Device device;
+ private Device lastSentDevice;
+ private Person person;
+ private Person lastSentPerson;
+ private Sdk sdk;
+ private AppRelease appRelease;
+ private EventData eventData;
+ private String lastSeenSdkVersion;
+ private VersionHistory versionHistory;
+ private boolean messageCenterFeatureUsed;
+ private boolean messageCenterWhoCardPreviouslyDisplayed;
+ private String messageCenterPendingMessage;
+ private String messageCenterPendingAttachments;
+ private String targets;
+ private String interactions;
+ private double interactionExpiration;
+
+ public ConversationData() {
+ this.localIdentifier = UUID.randomUUID().toString();
+ this.device = new Device();
+ this.person = new Person();
+ this.sdk = new Sdk();
+ this.appRelease = new AppRelease();
+ this.eventData = new EventData();
+ this.versionHistory = new VersionHistory();
+ }
+
+ //region Listeners
+
+ private transient DataChangedListener listener;
+
+ @Override
+ public void setDataChangedListener(DataChangedListener listener) {
+ this.listener = listener;
+ device.setDataChangedListener(this);
+ person.setDataChangedListener(this);
+ eventData.setDataChangedListener(this);
+ versionHistory.setDataChangedListener(this);
+ }
+
+ @Override
+ public void notifyDataChanged() {
+ if (listener != null) {
+ listener.onDataChanged();
+ }
+ }
+
+ @Override
+ public void onDataChanged() {
+ notifyDataChanged();
+ }
+ //endregion
+
+ //region Getters & Setters
+
+ public String getLocalIdentifier() {
+ return localIdentifier;
+ }
+
+ public String getConversationToken() {
+ return conversationToken;
+ }
+
+ public void setConversationToken(String conversationToken) {
+ if (!StringUtils.equal(this.conversationToken, conversationToken)) {
+ this.conversationToken = conversationToken;
+ notifyDataChanged();
+ }
+ }
+
+ public String getConversationId() {
+ return conversationId;
+ }
+
+ public void setConversationId(String conversationId) {
+ if (conversationId == null) {
+ throw new IllegalArgumentException("Conversation id is null");
+ }
+
+ if (!StringUtils.equal(this.conversationId, conversationId)) {
+ this.conversationId = conversationId;
+ notifyDataChanged();
+ }
+ }
+
+ public Device getDevice() {
+ return device;
+ }
+
+ public void setDevice(Device device) {
+ Assert.assertNotNull(device, "Device may not be null.");
+ this.device = device;
+ device.setDataChangedListener(this);
+ notifyDataChanged();
+ }
+
+ public Device getLastSentDevice() {
+ return lastSentDevice;
+ }
+
+ public void setLastSentDevice(Device lastSentDevice) {
+ this.lastSentDevice = lastSentDevice;
+ this.lastSentDevice.setDataChangedListener(this);
+ notifyDataChanged();
+ }
+
+ public Person getPerson() {
+ return person;
+ }
+
+ public void setPerson(Person person) {
+ Assert.assertNotNull(person, "Person may not be null.");
+ this.person = person;
+ this.person.setDataChangedListener(this);
+ notifyDataChanged();
+ }
+
+ public Person getLastSentPerson() {
+ return lastSentPerson;
+ }
+
+ public void setLastSentPerson(Person lastSentPerson) {
+ this.lastSentPerson = lastSentPerson;
+ this.lastSentPerson.setDataChangedListener(this);
+ notifyDataChanged();
+ }
+
+ public Sdk getSdk() {
+ return sdk;
+ }
+
+ public void setSdk(Sdk sdk) {
+ this.sdk = sdk;
+ notifyDataChanged();
+ }
+
+ public AppRelease getAppRelease() {
+ return appRelease;
+ }
+
+ public void setAppRelease(AppRelease appRelease) {
+ this.appRelease = appRelease;
+ notifyDataChanged();
+ }
+
+ public EventData getEventData() {
+ return eventData;
+ }
+
+ public void setEventData(EventData eventData) {
+ this.eventData = eventData;
+ this.eventData.setDataChangedListener(this);
+ notifyDataChanged();
+ }
+
+ public String getLastSeenSdkVersion() {
+ return lastSeenSdkVersion;
+ }
+
+ public void setLastSeenSdkVersion(String lastSeenSdkVersion) {
+ this.lastSeenSdkVersion = lastSeenSdkVersion;
+ notifyDataChanged();
+ }
+
+ public VersionHistory getVersionHistory() {
+ return versionHistory;
+ }
+
+ public void setVersionHistory(VersionHistory versionHistory) {
+ this.versionHistory = versionHistory;
+ this.versionHistory.setDataChangedListener(this);
+ notifyDataChanged();
+ }
+
+ public boolean isMessageCenterFeatureUsed() {
+ return messageCenterFeatureUsed;
+ }
+
+ public void setMessageCenterFeatureUsed(boolean messageCenterFeatureUsed) {
+ if (this.messageCenterFeatureUsed != messageCenterFeatureUsed) {
+ this.messageCenterFeatureUsed = messageCenterFeatureUsed;
+ notifyDataChanged();
+ }
+ }
+
+ public boolean isMessageCenterWhoCardPreviouslyDisplayed() {
+ return messageCenterWhoCardPreviouslyDisplayed;
+ }
+
+ public void setMessageCenterWhoCardPreviouslyDisplayed(boolean messageCenterWhoCardPreviouslyDisplayed) {
+ if (this.messageCenterWhoCardPreviouslyDisplayed != messageCenterWhoCardPreviouslyDisplayed) {
+ this.messageCenterWhoCardPreviouslyDisplayed = messageCenterWhoCardPreviouslyDisplayed;
+ notifyDataChanged();
+ }
+ }
+
+ public String getMessageCenterPendingMessage() {
+ return messageCenterPendingMessage;
+ }
+
+ public void setMessageCenterPendingMessage(String messageCenterPendingMessage) {
+ if (!StringUtils.equal(this.messageCenterPendingMessage, messageCenterPendingMessage)) {
+ this.messageCenterPendingMessage = messageCenterPendingMessage;
+ notifyDataChanged();
+ }
+ }
+
+ public String getMessageCenterPendingAttachments() {
+ return messageCenterPendingAttachments;
+ }
+
+ public void setMessageCenterPendingAttachments(String messageCenterPendingAttachments) {
+ if (!StringUtils.equal(this.messageCenterPendingAttachments, messageCenterPendingAttachments)) {
+ this.messageCenterPendingAttachments = messageCenterPendingAttachments;
+ notifyDataChanged();
+ }
+ }
+
+ public String getTargets() {
+ return targets;
+ }
+
+ public void setTargets(String targets) {
+ if (!StringUtils.equal(this.targets, targets)) {
+ this.targets = targets;
+ notifyDataChanged();
+ }
+ }
+
+ public String getInteractions() {
+ return interactions;
+ }
+
+ public void setInteractions(String interactions) {
+ if (!StringUtils.equal(this.interactions, interactions)) {
+ this.interactions = interactions;
+ notifyDataChanged();
+ }
+ }
+
+ public double getInteractionExpiration() {
+ return interactionExpiration;
+ }
+
+ public void setInteractionExpiration(double interactionExpiration) {
+ if (this.interactionExpiration != interactionExpiration) {
+ this.interactionExpiration = interactionExpiration;
+ notifyDataChanged();
+ }
+ }
+
+ //endregion
+}
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
new file mode 100644
index 000000000..760d6b0a2
--- /dev/null
+++ b/apptentive/src/main/java/com/apptentive/android/sdk/conversation/ConversationManager.java
@@ -0,0 +1,964 @@
+/*
+ * 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.conversation;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.apptentive.android.sdk.Apptentive;
+import com.apptentive.android.sdk.Apptentive.LoginCallback;
+import com.apptentive.android.sdk.ApptentiveInternal;
+import com.apptentive.android.sdk.ApptentiveLog;
+import com.apptentive.android.sdk.comm.ApptentiveHttpClient;
+import com.apptentive.android.sdk.conversation.ConversationMetadata.Filter;
+import com.apptentive.android.sdk.migration.Migrator;
+import com.apptentive.android.sdk.model.ConversationItem;
+import com.apptentive.android.sdk.model.ConversationTokenRequest;
+import com.apptentive.android.sdk.module.engagement.EngagementModule;
+import com.apptentive.android.sdk.network.HttpJsonRequest;
+import com.apptentive.android.sdk.network.HttpRequest;
+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.serialization.ObjectSerialization;
+import com.apptentive.android.sdk.storage.AppRelease;
+import com.apptentive.android.sdk.storage.AppReleaseManager;
+import com.apptentive.android.sdk.storage.Device;
+import com.apptentive.android.sdk.storage.DeviceManager;
+import com.apptentive.android.sdk.storage.Sdk;
+import com.apptentive.android.sdk.storage.SdkManager;
+import com.apptentive.android.sdk.storage.SerializerException;
+import com.apptentive.android.sdk.util.Constants;
+import com.apptentive.android.sdk.util.Jwt;
+import com.apptentive.android.sdk.util.ObjectUtils;
+import com.apptentive.android.sdk.util.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;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.List;
+
+import static com.apptentive.android.sdk.ApptentiveLog.Level.VERY_VERBOSE;
+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.StringUtils.isNullOrEmpty;
+
+/**
+ * Class responsible for managing conversations.
+ *