From b70560a63c539395851bcf97641ae5ef0a4b7460 Mon Sep 17 00:00:00 2001 From: Barry Li Date: Fri, 10 Jun 2016 11:09:31 -0700 Subject: [PATCH 01/26] Android-666 Add a dedicated task manager that dispatch work to a single worker thread in serial manner. As a starter, all db operations are dispatched through ints interfaces. --- .../sdk/storage/ApptentiveTaskManager.java | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveTaskManager.java diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveTaskManager.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveTaskManager.java new file mode 100644 index 000000000..bf014f0c6 --- /dev/null +++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveTaskManager.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2016, Apptentive, Inc. All Rights Reserved. + * Please refer to the LICENSE file for the terms and conditions + * under which redistribution and use of this file is permitted. + */ + +package com.apptentive.android.sdk.storage; + +import android.content.Context; + +import com.apptentive.android.sdk.model.Payload; +import com.apptentive.android.sdk.model.StoredFile; +import com.apptentive.android.sdk.module.messagecenter.model.ApptentiveMessage; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + + +public class ApptentiveTaskManager implements PayloadStore, EventStore, MessageStore { + + private ApptentiveDatabaseHelper dbHelper; + private ThreadPoolExecutor singleThreadExecutor; + + /* + * Creates an asynchronous task manager with one worker thread. This constructor must be invoked on the UI thread. + */ + public ApptentiveTaskManager(Context context) { + dbHelper = new ApptentiveDatabaseHelper(context); + /* When a new database task is submitted, the executor has the following behaviors: + * 1. If the thread pool has no thread yet, it creates a single worker thread. + * 2. If the single worker thread is running with tasks, it queues tasks. + * 3. If the queue is full, the task will be rejected and run on caller thread. + * + */ + singleThreadExecutor = new ThreadPoolExecutor(1, 1, + 30L, TimeUnit.SECONDS, + new LinkedBlockingQueue(), + new ThreadPoolExecutor.CallerRunsPolicy()); + + // If no new task arrives in 30 seconds, the worker thread terminates; otherwise it will be reused + singleThreadExecutor.allowCoreThreadTimeOut(true); + } + + + /* Wrapper class that can be used to return worker thread result to caller through message + * Usage: Message message = callerThreadHandler.obtainMessage(MESSAGE_FINISH, + * new AsyncTaskExResult>(ApptentiveTaskManager.this, result)); + * message.sendToTarget(); + */ + @SuppressWarnings({"RawUseOfParameterizedType"}) + private static class ApptentiveTaskResult { + final ApptentiveTaskManager mTask; + final Data[] mData; + + ApptentiveTaskResult(ApptentiveTaskManager task, Data... data) { + mTask = task; + mData = data; + } + } + + /** + * If an item with the same nonce as an item passed in already exists, it is overwritten by the item. Otherwise + * a new message is added. + */ + public void addPayload(final Payload... payloads) { + singleThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dbHelper.addPayload(payloads); + } + }); + } + + public void deletePayload(final Payload payload){ + if (payload != null) { + singleThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dbHelper.deletePayload(payload); + } + }); + } + } + + public void deleteAllPayloads() { + singleThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dbHelper.deleteAllPayloads(); + } + }); + } + + public synchronized Future getOldestUnsentPayload() throws Exception { + return singleThreadExecutor.submit(new Callable() { + @Override + public Payload call() throws Exception { + return dbHelper.getOldestUnsentPayload(); + } + }); + } + + @Override + public void addOrUpdateMessages(final ApptentiveMessage... apptentiveMessages) { + singleThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dbHelper.addOrUpdateMessages(apptentiveMessages); + } + }); + } + + @Override + public void updateMessage(final ApptentiveMessage apptentiveMessage) { + singleThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dbHelper.updateMessage(apptentiveMessage); + } + }); + } + + @Override + public Future> getAllMessages() throws Exception { + return singleThreadExecutor.submit(new Callable>() { + @Override + public List call() throws Exception { + List result = dbHelper.getAllMessages(); + return result; + } + }); + } + + @Override + public Future getLastReceivedMessageId() throws Exception { + return singleThreadExecutor.submit(new Callable() { + @Override + public String call() throws Exception { + return dbHelper.getLastReceivedMessageId(); + } + }); + } + + @Override + public Future getUnreadMessageCount() throws Exception { + return singleThreadExecutor.submit(new Callable() { + @Override + public Integer call() throws Exception { + return dbHelper.getUnreadMessageCount(); + } + }); + } + + @Override + public void deleteAllMessages() { + singleThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dbHelper.deleteAllMessages(); + } + }); + } + + @Override + public void deleteMessage(final String nonce) { + singleThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dbHelper.deleteMessage(nonce); + } + }); + } + + public void deleteAssociatedFiles(final String messageNonce) { + singleThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dbHelper.deleteAssociatedFiles(messageNonce); + } + }); + } + + public Future> getAssociatedFiles(final String nonce) throws Exception { + return singleThreadExecutor.submit(new Callable>() { + @Override + public List call() throws Exception { + return dbHelper.getAssociatedFiles(nonce); + } + }); + } + + public Future addCompoundMessageFiles(final List associatedFiles) throws Exception{ + return singleThreadExecutor.submit(new Callable() { + @Override + public Boolean call() throws Exception { + return dbHelper.addCompoundMessageFiles(associatedFiles); + } + }); + } + + public void reset(Context context) { + dbHelper.reset(context); + } + +} \ No newline at end of file From a50a48080b61d4352f80a9851d6e7c5d9a4e8afd Mon Sep 17 00:00:00 2001 From: Barry Li Date: Fri, 10 Jun 2016 11:10:56 -0700 Subject: [PATCH 02/26] Android-666 Rename ApptentiveDatabase to ApptentiveDataBaseHelper and remove all db.close() so the db is always opened --- ...ase.java => ApptentiveDatabaseHelper.java} | 61 ++++++------------- 1 file changed, 19 insertions(+), 42 deletions(-) rename apptentive/src/main/java/com/apptentive/android/sdk/storage/{ApptentiveDatabase.java => ApptentiveDatabaseHelper.java} (93%) diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveDatabase.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveDatabaseHelper.java similarity index 93% rename from apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveDatabase.java rename to apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveDatabaseHelper.java index 7ada353ab..f2053f015 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveDatabase.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveDatabaseHelper.java @@ -33,19 +33,19 @@ * * @author Sky Kelsey */ -public class ApptentiveDatabase extends SQLiteOpenHelper implements PayloadStore, EventStore, MessageStore { +public class ApptentiveDatabaseHelper extends SQLiteOpenHelper { // COMMON private static final int DATABASE_VERSION = 2; - private static final String DATABASE_NAME = "apptentive"; + public static final String DATABASE_NAME = "apptentive"; private static final int TRUE = 1; private static final int FALSE = 0; // PAYLOAD - private static final String TABLE_PAYLOAD = "payload"; - private static final String PAYLOAD_KEY_DB_ID = "_id"; // 0 - private static final String PAYLOAD_KEY_BASE_TYPE = "base_type"; // 1 - private static final String PAYLOAD_KEY_JSON = "json"; // 2 + public static final String TABLE_PAYLOAD = "payload"; + public static final String PAYLOAD_KEY_DB_ID = "_id"; // 0 + public static final String PAYLOAD_KEY_BASE_TYPE = "base_type"; // 1 + public static final String PAYLOAD_KEY_JSON = "json"; // 2 private static final String TABLE_CREATE_PAYLOAD = "CREATE TABLE " + TABLE_PAYLOAD + @@ -55,7 +55,7 @@ public class ApptentiveDatabase extends SQLiteOpenHelper implements PayloadStore PAYLOAD_KEY_JSON + " TEXT" + ");"; - private static final String QUERY_PAYLOAD_GET_NEXT_TO_SEND = "SELECT * FROM " + TABLE_PAYLOAD + " ORDER BY " + PAYLOAD_KEY_DB_ID + " ASC LIMIT 1"; + public static final String QUERY_PAYLOAD_GET_NEXT_TO_SEND = "SELECT * FROM " + TABLE_PAYLOAD + " ORDER BY " + PAYLOAD_KEY_DB_ID + " ASC LIMIT 1"; private static final String QUERY_PAYLOAD_GET_ALL_MESSAGE_IN_ORDER = "SELECT * FROM " + TABLE_PAYLOAD + " WHERE " + PAYLOAD_KEY_BASE_TYPE + " = ?" + " ORDER BY " + PAYLOAD_KEY_DB_ID + " ASC"; @@ -156,7 +156,7 @@ public void ensureClosed(Cursor cursor) { } } - public ApptentiveDatabase(Context context) { + public ApptentiveDatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); fileDir = context.getFilesDir(); } @@ -196,7 +196,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { * If an item with the same nonce as an item passed in already exists, it is overwritten by the item. Otherwise * a new message is added. */ - public synchronized void addPayload(Payload... payloads) { + public void addPayload(Payload... payloads) { SQLiteDatabase db = null; try { db = getWritableDatabase(); @@ -216,12 +216,10 @@ public synchronized void addPayload(Payload... payloads) { } } catch (SQLException sqe) { ApptentiveLog.e("addPayload EXCEPTION: " + sqe.getMessage()); - } finally { - ensureClosed(db); } } - public synchronized void deletePayload(Payload payload) { + public void deletePayload(Payload payload) { if (payload != null) { SQLiteDatabase db = null; try { @@ -229,25 +227,21 @@ public synchronized void deletePayload(Payload payload) { db.delete(TABLE_PAYLOAD, PAYLOAD_KEY_DB_ID + " = ?", new String[]{Long.toString(payload.getDatabaseId())}); } catch (SQLException sqe) { ApptentiveLog.e("deletePayload EXCEPTION: " + sqe.getMessage()); - } finally { - ensureClosed(db); } } } - public synchronized void deleteAllPayloads() { + public void deleteAllPayloads() { SQLiteDatabase db = null; try { db = getWritableDatabase(); db.delete(TABLE_PAYLOAD, "", null); } catch (SQLException sqe) { ApptentiveLog.e("deleteAllPayloads EXCEPTION: " + sqe.getMessage()); - } finally { - ensureClosed(db); } } - public synchronized Payload getOldestUnsentPayload() { + public Payload getOldestUnsentPayload() { SQLiteDatabase db = null; Cursor cursor = null; @@ -270,17 +264,15 @@ public synchronized Payload getOldestUnsentPayload() { return null; } finally { ensureClosed(cursor); - ensureClosed(db); } } - // MessageStore /** * Saves the message into the message table, and also into the payload table so it can be sent to the server. */ - public synchronized void addOrUpdateMessages(ApptentiveMessage... apptentiveMessages) { + public void addOrUpdateMessages(ApptentiveMessage... apptentiveMessages) { SQLiteDatabase db = null; try { db = getWritableDatabase(); @@ -319,12 +311,10 @@ public synchronized void addOrUpdateMessages(ApptentiveMessage... apptentiveMess } } catch (SQLException sqe) { ApptentiveLog.e("addOrUpdateMessages EXCEPTION: " + sqe.getMessage()); - } finally { - ensureClosed(db); } } - public synchronized void updateMessage(ApptentiveMessage apptentiveMessage) { + public void updateMessage(ApptentiveMessage apptentiveMessage) { SQLiteDatabase db = null; try { db = getWritableDatabase(); @@ -346,11 +336,10 @@ public synchronized void updateMessage(ApptentiveMessage apptentiveMessage) { if (db != null) { db.endTransaction(); } - ensureClosed(db); } } - public synchronized List getAllMessages() { + public List getAllMessages() { List apptentiveMessages = new ArrayList(); SQLiteDatabase db = null; Cursor cursor = null; @@ -375,7 +364,6 @@ public synchronized List getAllMessages() { ApptentiveLog.e("getAllMessages EXCEPTION: " + sqe.getMessage()); } finally { ensureClosed(cursor); - ensureClosed(db); } return apptentiveMessages; } @@ -394,7 +382,6 @@ public synchronized String getLastReceivedMessageId() { ApptentiveLog.e("getLastReceivedMessageId EXCEPTION: " + sqe.getMessage()); } finally { ensureClosed(cursor); - ensureClosed(db); } return ret; } @@ -411,7 +398,6 @@ public synchronized int getUnreadMessageCount() { return 0; } finally { ensureClosed(cursor); - ensureClosed(db); } } @@ -422,8 +408,6 @@ public synchronized void deleteAllMessages() { db.delete(TABLE_MESSAGE, "", null); } catch (SQLException sqe) { ApptentiveLog.e("deleteAllMessages EXCEPTION: " + sqe.getMessage()); - } finally { - ensureClosed(db); } } @@ -435,8 +419,6 @@ public synchronized void deleteMessage(String nonce) { ApptentiveLog.d("Deleted %d messages.", deleted); } catch (SQLException sqe) { ApptentiveLog.e("deleteMessage EXCEPTION: " + sqe.getMessage()); - } finally { - ensureClosed(db); } } @@ -444,8 +426,7 @@ public synchronized void deleteMessage(String nonce) { // // File Store // - - public synchronized void migrateToCompoundMessage(SQLiteDatabase db) { + private void migrateToCompoundMessage(SQLiteDatabase db) { Cursor cursor = null; // Migrate legacy stored files to compound message associated files try { @@ -577,7 +558,7 @@ public synchronized void migrateToCompoundMessage(SQLiteDatabase db) { } } - public synchronized void deleteAssociatedFiles(String messageNonce) { + public void deleteAssociatedFiles(String messageNonce) { SQLiteDatabase db = null; try { db = getWritableDatabase(); @@ -585,12 +566,10 @@ public synchronized void deleteAssociatedFiles(String messageNonce) { ApptentiveLog.d("Deleted %d stored files.", deleted); } catch (SQLException sqe) { ApptentiveLog.e("deleteAssociatedFiles EXCEPTION: " + sqe.getMessage()); - } finally { - ensureClosed(db); } } - public synchronized List getAssociatedFiles(String nonce) { + public List getAssociatedFiles(String nonce) { SQLiteDatabase db = null; Cursor cursor = null; List associatedFiles = new ArrayList(); @@ -614,7 +593,6 @@ public synchronized List getAssociatedFiles(String nonce) { ApptentiveLog.e("getAssociatedFiles EXCEPTION: " + sqe.getMessage()); } finally { ensureClosed(cursor); - ensureClosed(db); } return associatedFiles.size() > 0 ? associatedFiles : null; } @@ -625,7 +603,7 @@ public synchronized List getAssociatedFiles(String nonce) { * @param associatedFiles list of associated files * @return true if succeed */ - public synchronized boolean addCompoundMessageFiles(List associatedFiles) { + public boolean addCompoundMessageFiles(List associatedFiles) { String messageNonce = associatedFiles.get(0).getId(); SQLiteDatabase db = null; long ret = -1; @@ -651,7 +629,6 @@ public synchronized boolean addCompoundMessageFiles(List associatedF } catch (SQLException sqe) { ApptentiveLog.e("addCompoundMessageFiles EXCEPTION: " + sqe.getMessage()); } finally { - ensureClosed(db); return ret != -1; } } From b87adb19397c4ca87328ff407b894e2c8a5022ef Mon Sep 17 00:00:00 2001 From: Barry Li Date: Fri, 10 Jun 2016 11:13:38 -0700 Subject: [PATCH 03/26] Android-666 change the synchronous interface return types to asynchronous Future. The caller may wait on the returned Future for result by calling Future::get(), or proceed without waiting --- .../android/sdk/storage/MessageStore.java | 15 ++++++++------- .../android/sdk/storage/PayloadStore.java | 9 ++++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/MessageStore.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/MessageStore.java index 29d024b89..3452acad9 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/MessageStore.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/MessageStore.java @@ -10,23 +10,24 @@ import com.apptentive.android.sdk.module.messagecenter.model.ApptentiveMessage; import java.util.List; +import java.util.concurrent.Future; /** * @author Sky Kelsey */ public interface MessageStore extends PayloadStore { - public void addOrUpdateMessages(ApptentiveMessage... apptentiveMessage); + void addOrUpdateMessages(ApptentiveMessage... apptentiveMessage); - public void updateMessage(ApptentiveMessage apptentiveMessage); + void updateMessage(ApptentiveMessage apptentiveMessage); - public List getAllMessages(); + Future> getAllMessages() throws Exception; - public String getLastReceivedMessageId(); + Future getLastReceivedMessageId() throws Exception; - public int getUnreadMessageCount(); + Future getUnreadMessageCount() throws Exception; - public void deleteAllMessages(); + void deleteAllMessages(); - public void deleteMessage(String nonce); + void deleteMessage(String nonce); } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/PayloadStore.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/PayloadStore.java index 65c0a2f87..527762d33 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/PayloadStore.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/PayloadStore.java @@ -1,9 +1,9 @@ package com.apptentive.android.sdk.storage; -import android.content.Context; - import com.apptentive.android.sdk.model.Payload; +import java.util.concurrent.Future; + /** * @author Sky Kelsey */ @@ -15,6 +15,9 @@ public interface PayloadStore { public void deleteAllPayloads(); - public Payload getOldestUnsentPayload(); + /* Asynchronous call to retrieve the oldest unsent payload from the data storage. + * Calling get() method on the returned Future object will block the caller until the Future has completed, + */ + public Future getOldestUnsentPayload() throws Exception; } From 1211813c69e7b764ef70b977b75211a556d9f540 Mon Sep 17 00:00:00 2001 From: Barry Li Date: Fri, 10 Jun 2016 11:16:01 -0700 Subject: [PATCH 04/26] Android-666 change all reference of synchronous calls to Apptentive storage to through ApptentiveTaskManager --- .../android/sdk/ApptentiveInternal.java | 37 +++++++---- .../android/sdk/model/CodePointStore.java | 5 +- .../android/sdk/model/EventManager.java | 2 +- .../interaction/fragment/SurveyFragment.java | 2 +- .../module/messagecenter/MessageManager.java | 45 +++++++++----- .../messagecenter/model/CompoundMessage.java | 61 +++++++++++++------ .../sdk/storage/PayloadSendWorker.java | 20 +++--- 7 files changed, 115 insertions(+), 57 deletions(-) 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 05c53eb67..08c6cfbf1 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInternal.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInternal.java @@ -46,7 +46,7 @@ import com.apptentive.android.sdk.module.rating.impl.GooglePlayRatingProvider; import com.apptentive.android.sdk.module.survey.OnSurveyFinishedListener; import com.apptentive.android.sdk.storage.AppReleaseManager; -import com.apptentive.android.sdk.storage.ApptentiveDatabase; +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; @@ -77,12 +77,15 @@ public class ApptentiveInternal { InteractionManager interactionManager; MessageManager messageManager; PayloadSendWorker payloadWorker; - ApptentiveDatabase database; + ApptentiveTaskManager taskManager; CodePointStore codePointStore; ApptentiveActivityLifecycleCallbacks lifecycleCallbacks; // 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; + Integer currentVersionCode; + String currentVersionName; + boolean appIsInForeground; boolean isAppDebuggable; SharedPreferences prefs; @@ -170,13 +173,13 @@ public static ApptentiveInternal createInstance(Context context, final String ap MessageManager msgManager = new MessageManager(); PayloadSendWorker payloadWorker = new PayloadSendWorker(); InteractionManager interactionMgr = new InteractionManager(); - ApptentiveDatabase db = new ApptentiveDatabase(sApptentiveInternal.appContext); + ApptentiveTaskManager worker = new ApptentiveTaskManager(sApptentiveInternal.appContext); CodePointStore store = new CodePointStore(); sApptentiveInternal.messageManager = msgManager; sApptentiveInternal.payloadWorker = payloadWorker; sApptentiveInternal.interactionManager = interactionMgr; - sApptentiveInternal.database = db; + sApptentiveInternal.taskManager = worker; sApptentiveInternal.codePointStore = store; sApptentiveInternal.cachedExecutor = Executors.newCachedThreadPool(); sApptentiveInternal.apiKey = Util.trim(apptentiveApiKey); @@ -265,6 +268,14 @@ public Context getApplicationContext() { return appContext; } + public int getApplicationVersionCode() { + return currentVersionCode; + } + + public String getApplicationVersionName() { + return currentVersionName; + } + public ApptentiveActivityLifecycleCallbacks getRegisteredLifecycleCallbacks() { return lifecycleCallbacks; } @@ -295,8 +306,8 @@ public PayloadSendWorker getPayloadWorker() { return payloadWorker; } - public ApptentiveDatabase getApptentiveDatabase() { - return database; + public ApptentiveTaskManager getApptentiveTaskManager() { + return taskManager; } public CodePointStore getCodePointStore() { @@ -546,8 +557,8 @@ public void init() { int themeOverrideResId = appContext.getResources().getIdentifier("ApptentiveThemeOverride", "style", appPackageName); - Integer currentVersionCode = packageInfo.versionCode; - String currentVersionName = packageInfo.versionName; + currentVersionCode = packageInfo.versionCode; + currentVersionName = packageInfo.versionName; VersionHistoryStore.VersionHistoryEntry lastVersionEntrySeen = VersionHistoryStore.getLastVersionSeen(); AppRelease appRelease = new AppRelease(); appRelease.setVersion(currentVersionName); @@ -632,7 +643,7 @@ private void onVersionChanged(Integer previousVersionCode, Integer currentVersio AppRelease appRelease = AppReleaseManager.storeAppReleaseAndReturnDiff(currentAppRelease); if (appRelease != null) { ApptentiveLog.d("App release was updated."); - database.addPayload(appRelease); + taskManager.addPayload(appRelease); } invalidateCaches(); } @@ -819,7 +830,7 @@ void syncDevice() { if (deviceInfo != null) { ApptentiveLog.d("Device info was updated."); ApptentiveLog.v(deviceInfo.toString()); - database.addPayload(deviceInfo); + taskManager.addPayload(deviceInfo); } else { ApptentiveLog.d("Device info was not updated."); } @@ -833,7 +844,7 @@ private void syncSdk() { if (sdk != null) { ApptentiveLog.d("Sdk was updated."); ApptentiveLog.v(sdk.toString()); - database.addPayload(sdk); + taskManager.addPayload(sdk); } else { ApptentiveLog.d("Sdk was not updated."); } @@ -847,7 +858,7 @@ private void syncPerson() { if (person != null) { ApptentiveLog.d("Person was updated."); ApptentiveLog.v(person.toString()); - database.addPayload(person); + taskManager.addPayload(person); } else { ApptentiveLog.d("Person was not updated."); } @@ -1035,7 +1046,7 @@ private void setPersonId(String newPersonId) { public void resetSdkState() { prefs.edit().clear().apply(); - database.reset(appContext); + taskManager.reset(appContext); } public void notifyInteractionUpdated(boolean successful) { diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/CodePointStore.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/CodePointStore.java index 7428a7dbb..86718a3e1 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/model/CodePointStore.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/CodePointStore.java @@ -101,9 +101,8 @@ public synchronized void storeInteractionForCurrentAppVersion(String fullCodePoi } private void storeRecordForCurrentAppVersion(boolean isInteraction, String fullCodePoint) { - Context context = ApptentiveInternal.getInstance().getApplicationContext(); - String version = Util.getAppVersionName(context); - int build = Util.getAppVersionCode(context); + String version = ApptentiveInternal.getInstance().getApplicationVersionName(); + int build = ApptentiveInternal.getInstance().getApplicationVersionCode(); storeRecord(isInteraction, fullCodePoint, version, build); } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/EventManager.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/EventManager.java index b0d5bb81c..de0bc0d5b 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/model/EventManager.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/EventManager.java @@ -15,7 +15,7 @@ public class EventManager { private static EventStore getEventStore() { - return ApptentiveInternal.getInstance().getApptentiveDatabase(); + return ApptentiveInternal.getInstance().getApptentiveTaskManager(); } public static void sendEvent(Event event) { diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java index 0a3a40149..26de0a1d0 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java @@ -119,7 +119,7 @@ public void onClick(View view) { EngagementModule.engageInternal(getActivity(), interaction, EVENT_SUBMIT); - ApptentiveInternal.getInstance().getApptentiveDatabase().addPayload(new SurveyResponse(interaction, answers)); + ApptentiveInternal.getInstance().getApptentiveTaskManager().addPayload(new SurveyResponse(interaction, answers)); ApptentiveLog.d("Survey Submitted."); callListener(true); } else { diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/MessageManager.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/MessageManager.java index 9f3c559ea..3ec8176b9 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/MessageManager.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/MessageManager.java @@ -38,6 +38,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.concurrent.Future; /** * @author Sky Kelsey @@ -71,7 +72,6 @@ public class MessageManager { private MessagePollingWorker pollingWorker; - public MessageManager() { } @@ -159,8 +159,13 @@ public synchronized boolean fetchAndStoreMessages(boolean isMessageCenterForegro } // Fetch the messages. - String lastId = getMessageStore().getLastReceivedMessageId(); - List messagesToSave = fetchMessages(lastId); + List messagesToSave = null; + try { + Future future = getMessageStore().getLastReceivedMessageId(); + messagesToSave = fetchMessages(future.get()); + } catch (Exception e) { + ApptentiveLog.e("Error retrieving last received message id from worker thread"); + } CompoundMessage messageOnToast = null; if (messagesToSave != null && messagesToSave.size() > 0) { @@ -206,12 +211,16 @@ public synchronized boolean fetchAndStoreMessages(boolean isMessageCenterForegro public List getMessageCenterListItems() { List messagesToShow = new ArrayList(); - List messagesAll = getMessageStore().getAllMessages(); - // Do not display hidden messages on Message Center - for (ApptentiveMessage message : messagesAll) { - if (!message.isHidden()) { - messagesToShow.add(message); + try { + List messagesAll = getMessageStore().getAllMessages().get(); + // Do not display hidden messages on Message Center + for (ApptentiveMessage message : messagesAll) { + if (!message.isHidden()) { + messagesToShow.add(message); + } } + } catch (Exception e) { + ApptentiveLog.e("Error getting all messages in worker thread"); } return messagesToShow; @@ -219,7 +228,7 @@ public List getMessageCenterListItems() public void sendMessage(ApptentiveMessage apptentiveMessage) { getMessageStore().addOrUpdateMessages(apptentiveMessage); - ApptentiveInternal.getInstance().getApptentiveDatabase().addPayload(apptentiveMessage); + ApptentiveInternal.getInstance().getApptentiveTaskManager().addPayload(apptentiveMessage); } /** @@ -328,11 +337,17 @@ public void onSentMessage(ApptentiveMessage apptentiveMessage, ApptentiveHttpRes } private MessageStore getMessageStore() { - return ApptentiveInternal.getInstance().getApptentiveDatabase(); + return ApptentiveInternal.getInstance().getApptentiveTaskManager(); } public int getUnreadMessageCount() { - return getMessageStore().getUnreadMessageCount(); + int msgCount = 0; + try { + msgCount = getMessageStore().getUnreadMessageCount().get(); + } catch (Exception e) { + ApptentiveLog.e("Error getting unread messages count in worker thread"); + } + return msgCount; } @@ -452,10 +467,10 @@ private void showUnreadMessageToastNotification(final CompoundMessage apptentive final ApptentiveToastNotificationManager manager = ApptentiveToastNotificationManager.getInstance(foreground, true); final ApptentiveToastNotification.Builder builder = new ApptentiveToastNotification.Builder(foreground); builder.setContentTitle(foreground.getResources().getString(R.string.apptentive_message_center_title)) - .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_LIGHTS) - .setSmallIcon(R.drawable.avatar).setContentText(apptentiveMsg.getBody()) - .setContentIntent(pendingIntent) - .setFullScreenIntent(pendingIntent, false); + .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_LIGHTS) + .setSmallIcon(R.drawable.avatar).setContentText(apptentiveMsg.getBody()) + .setContentIntent(pendingIntent) + .setFullScreenIntent(pendingIntent, false); foreground.runOnUiThread(new Runnable() { public void run() { ApptentiveToastNotification notification = builder.buildApptentiveToastNotification(); diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/model/CompoundMessage.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/model/CompoundMessage.java index df100c345..255a26129 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/model/CompoundMessage.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/model/CompoundMessage.java @@ -9,7 +9,6 @@ import com.apptentive.android.sdk.ApptentiveInternal; import com.apptentive.android.sdk.ApptentiveLog; import com.apptentive.android.sdk.model.StoredFile; -import com.apptentive.android.sdk.storage.ApptentiveDatabase; import com.apptentive.android.sdk.util.image.ImageItem; import org.json.JSONArray; @@ -19,6 +18,8 @@ import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; /** * @author Barry Li @@ -153,8 +154,15 @@ public boolean setAssociatedImages(List attachedImages) { storedFile.setCreationTime(image.time); attachmentStoredFiles.add(storedFile); } - ApptentiveDatabase db = ApptentiveInternal.getInstance().getApptentiveDatabase(); - return db.addCompoundMessageFiles(attachmentStoredFiles); + boolean bRet = false; + try { + Future future = ApptentiveInternal.getInstance().getApptentiveTaskManager().addCompoundMessageFiles(attachmentStoredFiles); + bRet = future.get(); + } catch (Exception e) { + ApptentiveLog.e("Unable to set associated images in worker thread"); + } finally { + return bRet; + } } public boolean setAssociatedFiles(List attachedFiles) { @@ -167,8 +175,15 @@ public boolean setAssociatedFiles(List attachedFiles) { } setTextOnly(hasNoAttachments); - ApptentiveDatabase db = ApptentiveInternal.getInstance().getApptentiveDatabase(); - return db.addCompoundMessageFiles(attachedFiles); + boolean bRet = false; + try { + Future future = ApptentiveInternal.getInstance().getApptentiveTaskManager().addCompoundMessageFiles(attachedFiles); + bRet = future.get(); + } catch (Exception e) { + ApptentiveLog.e("Unable to set associated files in worker thread"); + } finally { + return bRet; + } } @@ -176,23 +191,35 @@ public List getAssociatedFiles() { if (hasNoAttachments) { return null; } - ApptentiveDatabase db = ApptentiveInternal.getInstance().getApptentiveDatabase(); - return db.getAssociatedFiles(getNonce()); + List associatedFiles = null; + try { + Future> future = ApptentiveInternal.getInstance().getApptentiveTaskManager().getAssociatedFiles(getNonce()); + associatedFiles = future.get(); + } catch (InterruptedException | ExecutionException e) { + ApptentiveLog.e("Unable to get associated files in worker thread"); + } finally { + return associatedFiles; + } } public void deleteAssociatedFiles() { - ApptentiveDatabase db = ApptentiveInternal.getInstance().getApptentiveDatabase(); - List associatedFiles = db.getAssociatedFiles(getNonce()); - // Delete local cached files - if (associatedFiles == null || associatedFiles.size() == 0) { - return; - } + try { + Future> future = ApptentiveInternal.getInstance().getApptentiveTaskManager().getAssociatedFiles(getNonce()); + List associatedFiles = future.get(); + // Delete local cached files + if (associatedFiles == null || associatedFiles.size() == 0) { + return; + } - for (StoredFile file : associatedFiles) { - File localFile = new File(file.getLocalFilePath()); - localFile.delete(); + for (StoredFile file : associatedFiles) { + File localFile = new File(file.getLocalFilePath()); + localFile.delete(); + } + // Delete records from db + ApptentiveInternal.getInstance().getApptentiveTaskManager().deleteAssociatedFiles(getNonce()); + } catch (Exception e) { + ApptentiveLog.e("Unable to delete associated files in worker thread"); } - db.deleteAssociatedFiles(getNonce()); } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/PayloadSendWorker.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/PayloadSendWorker.java index 109d48089..bd45b9039 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/PayloadSendWorker.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/PayloadSendWorker.java @@ -21,6 +21,7 @@ import com.apptentive.android.sdk.module.metric.MetricModule; import com.apptentive.android.sdk.util.Util; +import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -86,7 +87,7 @@ public void handleMessage(android.os.Message msg) { } private PayloadStore getPayloadStore() { - return ApptentiveInternal.getInstance().getApptentiveDatabase(); + return ApptentiveInternal.getInstance().getApptentiveTaskManager(); } private class PayloadSendRunnable implements Runnable { @@ -101,7 +102,6 @@ public void run() { while (appInForeground.get()) { MessageManager mgr = ApptentiveInternal.getInstance().getMessageManager(); - PayloadStore db = getPayloadStore(); if (TextUtils.isEmpty(ApptentiveInternal.getInstance().getApptentiveConversationToken())){ ApptentiveLog.i("No conversation token yet."); if (mgr != null) { @@ -119,8 +119,14 @@ public void run() { break; } ApptentiveLog.v("Checking for payloads to send."); - Payload payload; - payload = db.getOldestUnsentPayload(); + + Payload payload = null; + try { + Future future = ApptentiveInternal.getInstance().getApptentiveTaskManager().getOldestUnsentPayload(); + payload = future.get(); + } catch (Exception e) { + ApptentiveLog.e("Error getting oldest unsent payload in worker thread"); + } if (payload == null) { // There is no payload in the db. Terminate the thread threadCanRun.set(false); @@ -163,7 +169,7 @@ public void run() { break; default: ApptentiveLog.e("Didn't send unknown Payload BaseType: " + payload.getBaseType()); - db.deletePayload(payload); + ApptentiveInternal.getInstance().getApptentiveTaskManager().deletePayload(payload); break; } @@ -171,11 +177,11 @@ public void run() { if (response != null) { if (response.isSuccessful()) { ApptentiveLog.d("Payload submission successful. Removing from send queue."); - db.deletePayload(payload); + ApptentiveInternal.getInstance().getApptentiveTaskManager().deletePayload(payload); } else if (response.isRejectedPermanently() || response.isBadPayload()) { ApptentiveLog.d("Payload rejected. Removing from send queue."); ApptentiveLog.v("Rejected json:", payload.toString()); - db.deletePayload(payload); + ApptentiveInternal.getInstance().getApptentiveTaskManager().deletePayload(payload); } else if (response.isRejectedTemporarily()) { if (mgr != null) { mgr.pauseSending(MessageManager.SEND_PAUSE_REASON_SERVER); From 8c877ebb252def90d2224f878eb0beb67604b7d4 Mon Sep 17 00:00:00 2001 From: Barry Li Date: Fri, 10 Jun 2016 11:09:31 -0700 Subject: [PATCH 05/26] ANDROID-666 Add a dedicated task manager that dispatch work to a single worker thread in serial manner. As a starter, all db operations are dispatched through ints interfaces. --- .../sdk/storage/ApptentiveTaskManager.java | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveTaskManager.java diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveTaskManager.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveTaskManager.java new file mode 100644 index 000000000..bf014f0c6 --- /dev/null +++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveTaskManager.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2016, Apptentive, Inc. All Rights Reserved. + * Please refer to the LICENSE file for the terms and conditions + * under which redistribution and use of this file is permitted. + */ + +package com.apptentive.android.sdk.storage; + +import android.content.Context; + +import com.apptentive.android.sdk.model.Payload; +import com.apptentive.android.sdk.model.StoredFile; +import com.apptentive.android.sdk.module.messagecenter.model.ApptentiveMessage; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + + +public class ApptentiveTaskManager implements PayloadStore, EventStore, MessageStore { + + private ApptentiveDatabaseHelper dbHelper; + private ThreadPoolExecutor singleThreadExecutor; + + /* + * Creates an asynchronous task manager with one worker thread. This constructor must be invoked on the UI thread. + */ + public ApptentiveTaskManager(Context context) { + dbHelper = new ApptentiveDatabaseHelper(context); + /* When a new database task is submitted, the executor has the following behaviors: + * 1. If the thread pool has no thread yet, it creates a single worker thread. + * 2. If the single worker thread is running with tasks, it queues tasks. + * 3. If the queue is full, the task will be rejected and run on caller thread. + * + */ + singleThreadExecutor = new ThreadPoolExecutor(1, 1, + 30L, TimeUnit.SECONDS, + new LinkedBlockingQueue(), + new ThreadPoolExecutor.CallerRunsPolicy()); + + // If no new task arrives in 30 seconds, the worker thread terminates; otherwise it will be reused + singleThreadExecutor.allowCoreThreadTimeOut(true); + } + + + /* Wrapper class that can be used to return worker thread result to caller through message + * Usage: Message message = callerThreadHandler.obtainMessage(MESSAGE_FINISH, + * new AsyncTaskExResult>(ApptentiveTaskManager.this, result)); + * message.sendToTarget(); + */ + @SuppressWarnings({"RawUseOfParameterizedType"}) + private static class ApptentiveTaskResult { + final ApptentiveTaskManager mTask; + final Data[] mData; + + ApptentiveTaskResult(ApptentiveTaskManager task, Data... data) { + mTask = task; + mData = data; + } + } + + /** + * If an item with the same nonce as an item passed in already exists, it is overwritten by the item. Otherwise + * a new message is added. + */ + public void addPayload(final Payload... payloads) { + singleThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dbHelper.addPayload(payloads); + } + }); + } + + public void deletePayload(final Payload payload){ + if (payload != null) { + singleThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dbHelper.deletePayload(payload); + } + }); + } + } + + public void deleteAllPayloads() { + singleThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dbHelper.deleteAllPayloads(); + } + }); + } + + public synchronized Future getOldestUnsentPayload() throws Exception { + return singleThreadExecutor.submit(new Callable() { + @Override + public Payload call() throws Exception { + return dbHelper.getOldestUnsentPayload(); + } + }); + } + + @Override + public void addOrUpdateMessages(final ApptentiveMessage... apptentiveMessages) { + singleThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dbHelper.addOrUpdateMessages(apptentiveMessages); + } + }); + } + + @Override + public void updateMessage(final ApptentiveMessage apptentiveMessage) { + singleThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dbHelper.updateMessage(apptentiveMessage); + } + }); + } + + @Override + public Future> getAllMessages() throws Exception { + return singleThreadExecutor.submit(new Callable>() { + @Override + public List call() throws Exception { + List result = dbHelper.getAllMessages(); + return result; + } + }); + } + + @Override + public Future getLastReceivedMessageId() throws Exception { + return singleThreadExecutor.submit(new Callable() { + @Override + public String call() throws Exception { + return dbHelper.getLastReceivedMessageId(); + } + }); + } + + @Override + public Future getUnreadMessageCount() throws Exception { + return singleThreadExecutor.submit(new Callable() { + @Override + public Integer call() throws Exception { + return dbHelper.getUnreadMessageCount(); + } + }); + } + + @Override + public void deleteAllMessages() { + singleThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dbHelper.deleteAllMessages(); + } + }); + } + + @Override + public void deleteMessage(final String nonce) { + singleThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dbHelper.deleteMessage(nonce); + } + }); + } + + public void deleteAssociatedFiles(final String messageNonce) { + singleThreadExecutor.execute(new Runnable() { + @Override + public void run() { + dbHelper.deleteAssociatedFiles(messageNonce); + } + }); + } + + public Future> getAssociatedFiles(final String nonce) throws Exception { + return singleThreadExecutor.submit(new Callable>() { + @Override + public List call() throws Exception { + return dbHelper.getAssociatedFiles(nonce); + } + }); + } + + public Future addCompoundMessageFiles(final List associatedFiles) throws Exception{ + return singleThreadExecutor.submit(new Callable() { + @Override + public Boolean call() throws Exception { + return dbHelper.addCompoundMessageFiles(associatedFiles); + } + }); + } + + public void reset(Context context) { + dbHelper.reset(context); + } + +} \ No newline at end of file From ba589cc55d36388b98ad6928f2968b0ca9b94ee8 Mon Sep 17 00:00:00 2001 From: Barry Li Date: Fri, 10 Jun 2016 11:10:56 -0700 Subject: [PATCH 06/26] ANDROID-666 Rename ApptentiveDatabase to ApptentiveDataBaseHelper and remove all db.close() so the db is always opened --- ...ase.java => ApptentiveDatabaseHelper.java} | 61 ++++++------------- 1 file changed, 19 insertions(+), 42 deletions(-) rename apptentive/src/main/java/com/apptentive/android/sdk/storage/{ApptentiveDatabase.java => ApptentiveDatabaseHelper.java} (93%) diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveDatabase.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveDatabaseHelper.java similarity index 93% rename from apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveDatabase.java rename to apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveDatabaseHelper.java index 7ada353ab..f2053f015 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveDatabase.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/ApptentiveDatabaseHelper.java @@ -33,19 +33,19 @@ * * @author Sky Kelsey */ -public class ApptentiveDatabase extends SQLiteOpenHelper implements PayloadStore, EventStore, MessageStore { +public class ApptentiveDatabaseHelper extends SQLiteOpenHelper { // COMMON private static final int DATABASE_VERSION = 2; - private static final String DATABASE_NAME = "apptentive"; + public static final String DATABASE_NAME = "apptentive"; private static final int TRUE = 1; private static final int FALSE = 0; // PAYLOAD - private static final String TABLE_PAYLOAD = "payload"; - private static final String PAYLOAD_KEY_DB_ID = "_id"; // 0 - private static final String PAYLOAD_KEY_BASE_TYPE = "base_type"; // 1 - private static final String PAYLOAD_KEY_JSON = "json"; // 2 + public static final String TABLE_PAYLOAD = "payload"; + public static final String PAYLOAD_KEY_DB_ID = "_id"; // 0 + public static final String PAYLOAD_KEY_BASE_TYPE = "base_type"; // 1 + public static final String PAYLOAD_KEY_JSON = "json"; // 2 private static final String TABLE_CREATE_PAYLOAD = "CREATE TABLE " + TABLE_PAYLOAD + @@ -55,7 +55,7 @@ public class ApptentiveDatabase extends SQLiteOpenHelper implements PayloadStore PAYLOAD_KEY_JSON + " TEXT" + ");"; - private static final String QUERY_PAYLOAD_GET_NEXT_TO_SEND = "SELECT * FROM " + TABLE_PAYLOAD + " ORDER BY " + PAYLOAD_KEY_DB_ID + " ASC LIMIT 1"; + public static final String QUERY_PAYLOAD_GET_NEXT_TO_SEND = "SELECT * FROM " + TABLE_PAYLOAD + " ORDER BY " + PAYLOAD_KEY_DB_ID + " ASC LIMIT 1"; private static final String QUERY_PAYLOAD_GET_ALL_MESSAGE_IN_ORDER = "SELECT * FROM " + TABLE_PAYLOAD + " WHERE " + PAYLOAD_KEY_BASE_TYPE + " = ?" + " ORDER BY " + PAYLOAD_KEY_DB_ID + " ASC"; @@ -156,7 +156,7 @@ public void ensureClosed(Cursor cursor) { } } - public ApptentiveDatabase(Context context) { + public ApptentiveDatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); fileDir = context.getFilesDir(); } @@ -196,7 +196,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { * If an item with the same nonce as an item passed in already exists, it is overwritten by the item. Otherwise * a new message is added. */ - public synchronized void addPayload(Payload... payloads) { + public void addPayload(Payload... payloads) { SQLiteDatabase db = null; try { db = getWritableDatabase(); @@ -216,12 +216,10 @@ public synchronized void addPayload(Payload... payloads) { } } catch (SQLException sqe) { ApptentiveLog.e("addPayload EXCEPTION: " + sqe.getMessage()); - } finally { - ensureClosed(db); } } - public synchronized void deletePayload(Payload payload) { + public void deletePayload(Payload payload) { if (payload != null) { SQLiteDatabase db = null; try { @@ -229,25 +227,21 @@ public synchronized void deletePayload(Payload payload) { db.delete(TABLE_PAYLOAD, PAYLOAD_KEY_DB_ID + " = ?", new String[]{Long.toString(payload.getDatabaseId())}); } catch (SQLException sqe) { ApptentiveLog.e("deletePayload EXCEPTION: " + sqe.getMessage()); - } finally { - ensureClosed(db); } } } - public synchronized void deleteAllPayloads() { + public void deleteAllPayloads() { SQLiteDatabase db = null; try { db = getWritableDatabase(); db.delete(TABLE_PAYLOAD, "", null); } catch (SQLException sqe) { ApptentiveLog.e("deleteAllPayloads EXCEPTION: " + sqe.getMessage()); - } finally { - ensureClosed(db); } } - public synchronized Payload getOldestUnsentPayload() { + public Payload getOldestUnsentPayload() { SQLiteDatabase db = null; Cursor cursor = null; @@ -270,17 +264,15 @@ public synchronized Payload getOldestUnsentPayload() { return null; } finally { ensureClosed(cursor); - ensureClosed(db); } } - // MessageStore /** * Saves the message into the message table, and also into the payload table so it can be sent to the server. */ - public synchronized void addOrUpdateMessages(ApptentiveMessage... apptentiveMessages) { + public void addOrUpdateMessages(ApptentiveMessage... apptentiveMessages) { SQLiteDatabase db = null; try { db = getWritableDatabase(); @@ -319,12 +311,10 @@ public synchronized void addOrUpdateMessages(ApptentiveMessage... apptentiveMess } } catch (SQLException sqe) { ApptentiveLog.e("addOrUpdateMessages EXCEPTION: " + sqe.getMessage()); - } finally { - ensureClosed(db); } } - public synchronized void updateMessage(ApptentiveMessage apptentiveMessage) { + public void updateMessage(ApptentiveMessage apptentiveMessage) { SQLiteDatabase db = null; try { db = getWritableDatabase(); @@ -346,11 +336,10 @@ public synchronized void updateMessage(ApptentiveMessage apptentiveMessage) { if (db != null) { db.endTransaction(); } - ensureClosed(db); } } - public synchronized List getAllMessages() { + public List getAllMessages() { List apptentiveMessages = new ArrayList(); SQLiteDatabase db = null; Cursor cursor = null; @@ -375,7 +364,6 @@ public synchronized List getAllMessages() { ApptentiveLog.e("getAllMessages EXCEPTION: " + sqe.getMessage()); } finally { ensureClosed(cursor); - ensureClosed(db); } return apptentiveMessages; } @@ -394,7 +382,6 @@ public synchronized String getLastReceivedMessageId() { ApptentiveLog.e("getLastReceivedMessageId EXCEPTION: " + sqe.getMessage()); } finally { ensureClosed(cursor); - ensureClosed(db); } return ret; } @@ -411,7 +398,6 @@ public synchronized int getUnreadMessageCount() { return 0; } finally { ensureClosed(cursor); - ensureClosed(db); } } @@ -422,8 +408,6 @@ public synchronized void deleteAllMessages() { db.delete(TABLE_MESSAGE, "", null); } catch (SQLException sqe) { ApptentiveLog.e("deleteAllMessages EXCEPTION: " + sqe.getMessage()); - } finally { - ensureClosed(db); } } @@ -435,8 +419,6 @@ public synchronized void deleteMessage(String nonce) { ApptentiveLog.d("Deleted %d messages.", deleted); } catch (SQLException sqe) { ApptentiveLog.e("deleteMessage EXCEPTION: " + sqe.getMessage()); - } finally { - ensureClosed(db); } } @@ -444,8 +426,7 @@ public synchronized void deleteMessage(String nonce) { // // File Store // - - public synchronized void migrateToCompoundMessage(SQLiteDatabase db) { + private void migrateToCompoundMessage(SQLiteDatabase db) { Cursor cursor = null; // Migrate legacy stored files to compound message associated files try { @@ -577,7 +558,7 @@ public synchronized void migrateToCompoundMessage(SQLiteDatabase db) { } } - public synchronized void deleteAssociatedFiles(String messageNonce) { + public void deleteAssociatedFiles(String messageNonce) { SQLiteDatabase db = null; try { db = getWritableDatabase(); @@ -585,12 +566,10 @@ public synchronized void deleteAssociatedFiles(String messageNonce) { ApptentiveLog.d("Deleted %d stored files.", deleted); } catch (SQLException sqe) { ApptentiveLog.e("deleteAssociatedFiles EXCEPTION: " + sqe.getMessage()); - } finally { - ensureClosed(db); } } - public synchronized List getAssociatedFiles(String nonce) { + public List getAssociatedFiles(String nonce) { SQLiteDatabase db = null; Cursor cursor = null; List associatedFiles = new ArrayList(); @@ -614,7 +593,6 @@ public synchronized List getAssociatedFiles(String nonce) { ApptentiveLog.e("getAssociatedFiles EXCEPTION: " + sqe.getMessage()); } finally { ensureClosed(cursor); - ensureClosed(db); } return associatedFiles.size() > 0 ? associatedFiles : null; } @@ -625,7 +603,7 @@ public synchronized List getAssociatedFiles(String nonce) { * @param associatedFiles list of associated files * @return true if succeed */ - public synchronized boolean addCompoundMessageFiles(List associatedFiles) { + public boolean addCompoundMessageFiles(List associatedFiles) { String messageNonce = associatedFiles.get(0).getId(); SQLiteDatabase db = null; long ret = -1; @@ -651,7 +629,6 @@ public synchronized boolean addCompoundMessageFiles(List associatedF } catch (SQLException sqe) { ApptentiveLog.e("addCompoundMessageFiles EXCEPTION: " + sqe.getMessage()); } finally { - ensureClosed(db); return ret != -1; } } From 3ca91a307d86ee142b453693d13398c0ea38fbe5 Mon Sep 17 00:00:00 2001 From: Barry Li Date: Fri, 10 Jun 2016 11:13:38 -0700 Subject: [PATCH 07/26] ANDROID-666 change the synchronous interface return types to asynchronous Future. The caller may wait on the returned Future for result by calling Future::get(), or proceed without waiting --- .../android/sdk/storage/MessageStore.java | 15 ++++++++------- .../android/sdk/storage/PayloadStore.java | 9 ++++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/MessageStore.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/MessageStore.java index 29d024b89..3452acad9 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/MessageStore.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/MessageStore.java @@ -10,23 +10,24 @@ import com.apptentive.android.sdk.module.messagecenter.model.ApptentiveMessage; import java.util.List; +import java.util.concurrent.Future; /** * @author Sky Kelsey */ public interface MessageStore extends PayloadStore { - public void addOrUpdateMessages(ApptentiveMessage... apptentiveMessage); + void addOrUpdateMessages(ApptentiveMessage... apptentiveMessage); - public void updateMessage(ApptentiveMessage apptentiveMessage); + void updateMessage(ApptentiveMessage apptentiveMessage); - public List getAllMessages(); + Future> getAllMessages() throws Exception; - public String getLastReceivedMessageId(); + Future getLastReceivedMessageId() throws Exception; - public int getUnreadMessageCount(); + Future getUnreadMessageCount() throws Exception; - public void deleteAllMessages(); + void deleteAllMessages(); - public void deleteMessage(String nonce); + void deleteMessage(String nonce); } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/PayloadStore.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/PayloadStore.java index 65c0a2f87..527762d33 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/PayloadStore.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/PayloadStore.java @@ -1,9 +1,9 @@ package com.apptentive.android.sdk.storage; -import android.content.Context; - import com.apptentive.android.sdk.model.Payload; +import java.util.concurrent.Future; + /** * @author Sky Kelsey */ @@ -15,6 +15,9 @@ public interface PayloadStore { public void deleteAllPayloads(); - public Payload getOldestUnsentPayload(); + /* Asynchronous call to retrieve the oldest unsent payload from the data storage. + * Calling get() method on the returned Future object will block the caller until the Future has completed, + */ + public Future getOldestUnsentPayload() throws Exception; } From ee905e3593acd344504fde425e3cef37c31ae0a2 Mon Sep 17 00:00:00 2001 From: Barry Li Date: Fri, 10 Jun 2016 11:16:01 -0700 Subject: [PATCH 08/26] ANDROID-666 change all reference of synchronous calls to Apptentive storage to through ApptentiveTaskManager --- .../android/sdk/ApptentiveInternal.java | 37 +++++++---- .../android/sdk/model/CodePointStore.java | 5 +- .../android/sdk/model/EventManager.java | 2 +- .../interaction/fragment/SurveyFragment.java | 2 +- .../module/messagecenter/MessageManager.java | 45 +++++++++----- .../messagecenter/model/CompoundMessage.java | 61 +++++++++++++------ .../sdk/storage/PayloadSendWorker.java | 20 +++--- 7 files changed, 115 insertions(+), 57 deletions(-) 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 05c53eb67..08c6cfbf1 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInternal.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/ApptentiveInternal.java @@ -46,7 +46,7 @@ import com.apptentive.android.sdk.module.rating.impl.GooglePlayRatingProvider; import com.apptentive.android.sdk.module.survey.OnSurveyFinishedListener; import com.apptentive.android.sdk.storage.AppReleaseManager; -import com.apptentive.android.sdk.storage.ApptentiveDatabase; +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; @@ -77,12 +77,15 @@ public class ApptentiveInternal { InteractionManager interactionManager; MessageManager messageManager; PayloadSendWorker payloadWorker; - ApptentiveDatabase database; + ApptentiveTaskManager taskManager; CodePointStore codePointStore; ApptentiveActivityLifecycleCallbacks lifecycleCallbacks; // 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; + Integer currentVersionCode; + String currentVersionName; + boolean appIsInForeground; boolean isAppDebuggable; SharedPreferences prefs; @@ -170,13 +173,13 @@ public static ApptentiveInternal createInstance(Context context, final String ap MessageManager msgManager = new MessageManager(); PayloadSendWorker payloadWorker = new PayloadSendWorker(); InteractionManager interactionMgr = new InteractionManager(); - ApptentiveDatabase db = new ApptentiveDatabase(sApptentiveInternal.appContext); + ApptentiveTaskManager worker = new ApptentiveTaskManager(sApptentiveInternal.appContext); CodePointStore store = new CodePointStore(); sApptentiveInternal.messageManager = msgManager; sApptentiveInternal.payloadWorker = payloadWorker; sApptentiveInternal.interactionManager = interactionMgr; - sApptentiveInternal.database = db; + sApptentiveInternal.taskManager = worker; sApptentiveInternal.codePointStore = store; sApptentiveInternal.cachedExecutor = Executors.newCachedThreadPool(); sApptentiveInternal.apiKey = Util.trim(apptentiveApiKey); @@ -265,6 +268,14 @@ public Context getApplicationContext() { return appContext; } + public int getApplicationVersionCode() { + return currentVersionCode; + } + + public String getApplicationVersionName() { + return currentVersionName; + } + public ApptentiveActivityLifecycleCallbacks getRegisteredLifecycleCallbacks() { return lifecycleCallbacks; } @@ -295,8 +306,8 @@ public PayloadSendWorker getPayloadWorker() { return payloadWorker; } - public ApptentiveDatabase getApptentiveDatabase() { - return database; + public ApptentiveTaskManager getApptentiveTaskManager() { + return taskManager; } public CodePointStore getCodePointStore() { @@ -546,8 +557,8 @@ public void init() { int themeOverrideResId = appContext.getResources().getIdentifier("ApptentiveThemeOverride", "style", appPackageName); - Integer currentVersionCode = packageInfo.versionCode; - String currentVersionName = packageInfo.versionName; + currentVersionCode = packageInfo.versionCode; + currentVersionName = packageInfo.versionName; VersionHistoryStore.VersionHistoryEntry lastVersionEntrySeen = VersionHistoryStore.getLastVersionSeen(); AppRelease appRelease = new AppRelease(); appRelease.setVersion(currentVersionName); @@ -632,7 +643,7 @@ private void onVersionChanged(Integer previousVersionCode, Integer currentVersio AppRelease appRelease = AppReleaseManager.storeAppReleaseAndReturnDiff(currentAppRelease); if (appRelease != null) { ApptentiveLog.d("App release was updated."); - database.addPayload(appRelease); + taskManager.addPayload(appRelease); } invalidateCaches(); } @@ -819,7 +830,7 @@ void syncDevice() { if (deviceInfo != null) { ApptentiveLog.d("Device info was updated."); ApptentiveLog.v(deviceInfo.toString()); - database.addPayload(deviceInfo); + taskManager.addPayload(deviceInfo); } else { ApptentiveLog.d("Device info was not updated."); } @@ -833,7 +844,7 @@ private void syncSdk() { if (sdk != null) { ApptentiveLog.d("Sdk was updated."); ApptentiveLog.v(sdk.toString()); - database.addPayload(sdk); + taskManager.addPayload(sdk); } else { ApptentiveLog.d("Sdk was not updated."); } @@ -847,7 +858,7 @@ private void syncPerson() { if (person != null) { ApptentiveLog.d("Person was updated."); ApptentiveLog.v(person.toString()); - database.addPayload(person); + taskManager.addPayload(person); } else { ApptentiveLog.d("Person was not updated."); } @@ -1035,7 +1046,7 @@ private void setPersonId(String newPersonId) { public void resetSdkState() { prefs.edit().clear().apply(); - database.reset(appContext); + taskManager.reset(appContext); } public void notifyInteractionUpdated(boolean successful) { diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/CodePointStore.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/CodePointStore.java index 7428a7dbb..86718a3e1 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/model/CodePointStore.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/CodePointStore.java @@ -101,9 +101,8 @@ public synchronized void storeInteractionForCurrentAppVersion(String fullCodePoi } private void storeRecordForCurrentAppVersion(boolean isInteraction, String fullCodePoint) { - Context context = ApptentiveInternal.getInstance().getApplicationContext(); - String version = Util.getAppVersionName(context); - int build = Util.getAppVersionCode(context); + String version = ApptentiveInternal.getInstance().getApplicationVersionName(); + int build = ApptentiveInternal.getInstance().getApplicationVersionCode(); storeRecord(isInteraction, fullCodePoint, version, build); } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/model/EventManager.java b/apptentive/src/main/java/com/apptentive/android/sdk/model/EventManager.java index b0d5bb81c..de0bc0d5b 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/model/EventManager.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/model/EventManager.java @@ -15,7 +15,7 @@ public class EventManager { private static EventStore getEventStore() { - return ApptentiveInternal.getInstance().getApptentiveDatabase(); + return ApptentiveInternal.getInstance().getApptentiveTaskManager(); } public static void sendEvent(Event event) { diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java index 0a3a40149..26de0a1d0 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java @@ -119,7 +119,7 @@ public void onClick(View view) { EngagementModule.engageInternal(getActivity(), interaction, EVENT_SUBMIT); - ApptentiveInternal.getInstance().getApptentiveDatabase().addPayload(new SurveyResponse(interaction, answers)); + ApptentiveInternal.getInstance().getApptentiveTaskManager().addPayload(new SurveyResponse(interaction, answers)); ApptentiveLog.d("Survey Submitted."); callListener(true); } else { diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/MessageManager.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/MessageManager.java index 9f3c559ea..3ec8176b9 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/MessageManager.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/MessageManager.java @@ -38,6 +38,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.concurrent.Future; /** * @author Sky Kelsey @@ -71,7 +72,6 @@ public class MessageManager { private MessagePollingWorker pollingWorker; - public MessageManager() { } @@ -159,8 +159,13 @@ public synchronized boolean fetchAndStoreMessages(boolean isMessageCenterForegro } // Fetch the messages. - String lastId = getMessageStore().getLastReceivedMessageId(); - List messagesToSave = fetchMessages(lastId); + List messagesToSave = null; + try { + Future future = getMessageStore().getLastReceivedMessageId(); + messagesToSave = fetchMessages(future.get()); + } catch (Exception e) { + ApptentiveLog.e("Error retrieving last received message id from worker thread"); + } CompoundMessage messageOnToast = null; if (messagesToSave != null && messagesToSave.size() > 0) { @@ -206,12 +211,16 @@ public synchronized boolean fetchAndStoreMessages(boolean isMessageCenterForegro public List getMessageCenterListItems() { List messagesToShow = new ArrayList(); - List messagesAll = getMessageStore().getAllMessages(); - // Do not display hidden messages on Message Center - for (ApptentiveMessage message : messagesAll) { - if (!message.isHidden()) { - messagesToShow.add(message); + try { + List messagesAll = getMessageStore().getAllMessages().get(); + // Do not display hidden messages on Message Center + for (ApptentiveMessage message : messagesAll) { + if (!message.isHidden()) { + messagesToShow.add(message); + } } + } catch (Exception e) { + ApptentiveLog.e("Error getting all messages in worker thread"); } return messagesToShow; @@ -219,7 +228,7 @@ public List getMessageCenterListItems() public void sendMessage(ApptentiveMessage apptentiveMessage) { getMessageStore().addOrUpdateMessages(apptentiveMessage); - ApptentiveInternal.getInstance().getApptentiveDatabase().addPayload(apptentiveMessage); + ApptentiveInternal.getInstance().getApptentiveTaskManager().addPayload(apptentiveMessage); } /** @@ -328,11 +337,17 @@ public void onSentMessage(ApptentiveMessage apptentiveMessage, ApptentiveHttpRes } private MessageStore getMessageStore() { - return ApptentiveInternal.getInstance().getApptentiveDatabase(); + return ApptentiveInternal.getInstance().getApptentiveTaskManager(); } public int getUnreadMessageCount() { - return getMessageStore().getUnreadMessageCount(); + int msgCount = 0; + try { + msgCount = getMessageStore().getUnreadMessageCount().get(); + } catch (Exception e) { + ApptentiveLog.e("Error getting unread messages count in worker thread"); + } + return msgCount; } @@ -452,10 +467,10 @@ private void showUnreadMessageToastNotification(final CompoundMessage apptentive final ApptentiveToastNotificationManager manager = ApptentiveToastNotificationManager.getInstance(foreground, true); final ApptentiveToastNotification.Builder builder = new ApptentiveToastNotification.Builder(foreground); builder.setContentTitle(foreground.getResources().getString(R.string.apptentive_message_center_title)) - .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_LIGHTS) - .setSmallIcon(R.drawable.avatar).setContentText(apptentiveMsg.getBody()) - .setContentIntent(pendingIntent) - .setFullScreenIntent(pendingIntent, false); + .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_LIGHTS) + .setSmallIcon(R.drawable.avatar).setContentText(apptentiveMsg.getBody()) + .setContentIntent(pendingIntent) + .setFullScreenIntent(pendingIntent, false); foreground.runOnUiThread(new Runnable() { public void run() { ApptentiveToastNotification notification = builder.buildApptentiveToastNotification(); diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/model/CompoundMessage.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/model/CompoundMessage.java index df100c345..255a26129 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/model/CompoundMessage.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/messagecenter/model/CompoundMessage.java @@ -9,7 +9,6 @@ import com.apptentive.android.sdk.ApptentiveInternal; import com.apptentive.android.sdk.ApptentiveLog; import com.apptentive.android.sdk.model.StoredFile; -import com.apptentive.android.sdk.storage.ApptentiveDatabase; import com.apptentive.android.sdk.util.image.ImageItem; import org.json.JSONArray; @@ -19,6 +18,8 @@ import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; /** * @author Barry Li @@ -153,8 +154,15 @@ public boolean setAssociatedImages(List attachedImages) { storedFile.setCreationTime(image.time); attachmentStoredFiles.add(storedFile); } - ApptentiveDatabase db = ApptentiveInternal.getInstance().getApptentiveDatabase(); - return db.addCompoundMessageFiles(attachmentStoredFiles); + boolean bRet = false; + try { + Future future = ApptentiveInternal.getInstance().getApptentiveTaskManager().addCompoundMessageFiles(attachmentStoredFiles); + bRet = future.get(); + } catch (Exception e) { + ApptentiveLog.e("Unable to set associated images in worker thread"); + } finally { + return bRet; + } } public boolean setAssociatedFiles(List attachedFiles) { @@ -167,8 +175,15 @@ public boolean setAssociatedFiles(List attachedFiles) { } setTextOnly(hasNoAttachments); - ApptentiveDatabase db = ApptentiveInternal.getInstance().getApptentiveDatabase(); - return db.addCompoundMessageFiles(attachedFiles); + boolean bRet = false; + try { + Future future = ApptentiveInternal.getInstance().getApptentiveTaskManager().addCompoundMessageFiles(attachedFiles); + bRet = future.get(); + } catch (Exception e) { + ApptentiveLog.e("Unable to set associated files in worker thread"); + } finally { + return bRet; + } } @@ -176,23 +191,35 @@ public List getAssociatedFiles() { if (hasNoAttachments) { return null; } - ApptentiveDatabase db = ApptentiveInternal.getInstance().getApptentiveDatabase(); - return db.getAssociatedFiles(getNonce()); + List associatedFiles = null; + try { + Future> future = ApptentiveInternal.getInstance().getApptentiveTaskManager().getAssociatedFiles(getNonce()); + associatedFiles = future.get(); + } catch (InterruptedException | ExecutionException e) { + ApptentiveLog.e("Unable to get associated files in worker thread"); + } finally { + return associatedFiles; + } } public void deleteAssociatedFiles() { - ApptentiveDatabase db = ApptentiveInternal.getInstance().getApptentiveDatabase(); - List associatedFiles = db.getAssociatedFiles(getNonce()); - // Delete local cached files - if (associatedFiles == null || associatedFiles.size() == 0) { - return; - } + try { + Future> future = ApptentiveInternal.getInstance().getApptentiveTaskManager().getAssociatedFiles(getNonce()); + List associatedFiles = future.get(); + // Delete local cached files + if (associatedFiles == null || associatedFiles.size() == 0) { + return; + } - for (StoredFile file : associatedFiles) { - File localFile = new File(file.getLocalFilePath()); - localFile.delete(); + for (StoredFile file : associatedFiles) { + File localFile = new File(file.getLocalFilePath()); + localFile.delete(); + } + // Delete records from db + ApptentiveInternal.getInstance().getApptentiveTaskManager().deleteAssociatedFiles(getNonce()); + } catch (Exception e) { + ApptentiveLog.e("Unable to delete associated files in worker thread"); } - db.deleteAssociatedFiles(getNonce()); } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/storage/PayloadSendWorker.java b/apptentive/src/main/java/com/apptentive/android/sdk/storage/PayloadSendWorker.java index 109d48089..bd45b9039 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/storage/PayloadSendWorker.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/storage/PayloadSendWorker.java @@ -21,6 +21,7 @@ import com.apptentive.android.sdk.module.metric.MetricModule; import com.apptentive.android.sdk.util.Util; +import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -86,7 +87,7 @@ public void handleMessage(android.os.Message msg) { } private PayloadStore getPayloadStore() { - return ApptentiveInternal.getInstance().getApptentiveDatabase(); + return ApptentiveInternal.getInstance().getApptentiveTaskManager(); } private class PayloadSendRunnable implements Runnable { @@ -101,7 +102,6 @@ public void run() { while (appInForeground.get()) { MessageManager mgr = ApptentiveInternal.getInstance().getMessageManager(); - PayloadStore db = getPayloadStore(); if (TextUtils.isEmpty(ApptentiveInternal.getInstance().getApptentiveConversationToken())){ ApptentiveLog.i("No conversation token yet."); if (mgr != null) { @@ -119,8 +119,14 @@ public void run() { break; } ApptentiveLog.v("Checking for payloads to send."); - Payload payload; - payload = db.getOldestUnsentPayload(); + + Payload payload = null; + try { + Future future = ApptentiveInternal.getInstance().getApptentiveTaskManager().getOldestUnsentPayload(); + payload = future.get(); + } catch (Exception e) { + ApptentiveLog.e("Error getting oldest unsent payload in worker thread"); + } if (payload == null) { // There is no payload in the db. Terminate the thread threadCanRun.set(false); @@ -163,7 +169,7 @@ public void run() { break; default: ApptentiveLog.e("Didn't send unknown Payload BaseType: " + payload.getBaseType()); - db.deletePayload(payload); + ApptentiveInternal.getInstance().getApptentiveTaskManager().deletePayload(payload); break; } @@ -171,11 +177,11 @@ public void run() { if (response != null) { if (response.isSuccessful()) { ApptentiveLog.d("Payload submission successful. Removing from send queue."); - db.deletePayload(payload); + ApptentiveInternal.getInstance().getApptentiveTaskManager().deletePayload(payload); } else if (response.isRejectedPermanently() || response.isBadPayload()) { ApptentiveLog.d("Payload rejected. Removing from send queue."); ApptentiveLog.v("Rejected json:", payload.toString()); - db.deletePayload(payload); + ApptentiveInternal.getInstance().getApptentiveTaskManager().deletePayload(payload); } else if (response.isRejectedTemporarily()) { if (mgr != null) { mgr.pauseSending(MessageManager.SEND_PAUSE_REASON_SERVER); From e4538af39df44448623586b1566ac64372f37edb Mon Sep 17 00:00:00 2001 From: skykelsey Date: Wed, 15 Jun 2016 16:04:17 -0700 Subject: [PATCH 09/26] Initial checkin for new range question type (NPS). ANDROID-694 --- .../interaction/fragment/SurveyFragment.java | 4 + .../interaction/model/SurveyInteraction.java | 4 + .../interaction/model/survey/Question.java | 2 + .../model/survey/RangeQuestion.java | 44 ++++++++++ .../view/survey/RangeSurveyQuestionView.java | 86 +++++++++++++++++++ .../apptentive_survey_question_range.xml | 14 +++ 6 files changed, 154 insertions(+) create mode 100644 apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/survey/RangeQuestion.java create mode 100644 apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java create mode 100644 apptentive/src/main/res/layout/apptentive_survey_question_range.xml diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java index 0a3a40149..0fb283add 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java @@ -35,10 +35,12 @@ import com.apptentive.android.sdk.module.engagement.interaction.model.survey.MultichoiceQuestion; import com.apptentive.android.sdk.module.engagement.interaction.model.survey.MultiselectQuestion; import com.apptentive.android.sdk.module.engagement.interaction.model.survey.Question; +import com.apptentive.android.sdk.module.engagement.interaction.model.survey.RangeQuestion; import com.apptentive.android.sdk.module.engagement.interaction.model.survey.SinglelineQuestion; import com.apptentive.android.sdk.module.engagement.interaction.view.survey.BaseSurveyQuestionView; import com.apptentive.android.sdk.module.engagement.interaction.view.survey.MultichoiceSurveyQuestionView; import com.apptentive.android.sdk.module.engagement.interaction.view.survey.MultiselectSurveyQuestionView; +import com.apptentive.android.sdk.module.engagement.interaction.view.survey.RangeSurveyQuestionView; import com.apptentive.android.sdk.module.engagement.interaction.view.survey.SurveyQuestionView; import com.apptentive.android.sdk.module.engagement.interaction.view.survey.TextSurveyQuestionView; import com.apptentive.android.sdk.module.survey.OnSurveyFinishedListener; @@ -151,6 +153,8 @@ public void onClick(View view) { } else if (question.getType() == Question.QUESTION_TYPE_MULTISELECT) { surveyQuestionView = MultiselectSurveyQuestionView.newInstance((MultiselectQuestion) question); + } else if (question.getType() == Question.QUESTION_TYPE_RANGE) { + surveyQuestionView = RangeSurveyQuestionView.newInstance((RangeQuestion) question); } else { surveyQuestionView = null; } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/SurveyInteraction.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/SurveyInteraction.java index 588b18811..54b8553ff 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/SurveyInteraction.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/SurveyInteraction.java @@ -9,6 +9,7 @@ import com.apptentive.android.sdk.module.engagement.interaction.model.survey.MultichoiceQuestion; import com.apptentive.android.sdk.module.engagement.interaction.model.survey.MultiselectQuestion; import com.apptentive.android.sdk.module.engagement.interaction.model.survey.Question; +import com.apptentive.android.sdk.module.engagement.interaction.model.survey.RangeQuestion; import com.apptentive.android.sdk.module.engagement.interaction.model.survey.SinglelineQuestion; import org.json.JSONArray; @@ -93,6 +94,9 @@ public List getQuestions() { case multiselect: question = new MultiselectQuestion(questionJson.toString()); break; + case range: + question = new RangeQuestion(questionJson.toString()); + break; default: break; } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/survey/Question.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/survey/Question.java index dc590eafd..3a96e87c0 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/survey/Question.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/survey/Question.java @@ -10,6 +10,7 @@ public interface Question { int QUESTION_TYPE_SINGLELINE = 1; int QUESTION_TYPE_MULTICHOICE = 2; int QUESTION_TYPE_MULTISELECT = 3; + int QUESTION_TYPE_RANGE = 4; int getType(); @@ -27,5 +28,6 @@ enum Type { multichoice, singleline, multiselect, + range } } diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/survey/RangeQuestion.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/survey/RangeQuestion.java new file mode 100644 index 000000000..5b686f2c5 --- /dev/null +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/model/survey/RangeQuestion.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016, Apptentive, Inc. All Rights Reserved. + * Please refer to the LICENSE file for the terms and conditions + * under which redistribution and use of this file is permitted. + */ + +package com.apptentive.android.sdk.module.engagement.interaction.model.survey; + +import org.json.JSONException; + +public class RangeQuestion extends BaseQuestion { + + private static final String KEY_MIN = "min"; + private static final String KEY_MAX = "max"; + private static final String KEY_MIN_LABEL = "min_label"; + private static final String KEY_MAX_LABEL = "max_label"; + + private static final int DEFAULT_MIN = 0; + private static final int DEFAULT_MAX = 10; + + public RangeQuestion(String json) throws JSONException { + super(json); + } + + public int getType() { + return QUESTION_TYPE_RANGE; + } + + public int getMin() { + return optInt(KEY_MIN, DEFAULT_MIN); + } + + public int getMax() { + return optInt(KEY_MAX, DEFAULT_MAX); + } + + public String getMinLabel() { + return optString(KEY_MIN_LABEL, null); + } + + public String getMaxLabel() { + return optString(KEY_MAX_LABEL, null); + } +} diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java new file mode 100644 index 000000000..6305f9624 --- /dev/null +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016, Apptentive, Inc. All Rights Reserved. + * Please refer to the LICENSE file for the terms and conditions + * under which redistribution and use of this file is permitted. + */ + +package com.apptentive.android.sdk.module.engagement.interaction.view.survey; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.apptentive.android.sdk.R; +import com.apptentive.android.sdk.module.engagement.interaction.model.survey.RangeQuestion; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + + +public class RangeSurveyQuestionView extends BaseSurveyQuestionView { + + public static RangeSurveyQuestionView newInstance(RangeQuestion question) { + RangeSurveyQuestionView f = new RangeSurveyQuestionView(); + Bundle b = new Bundle(); + b.putString("question", question.toString()); + f.setArguments(b); + return f; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Bundle bundle = getArguments(); + if (bundle != null) { + try { + question = new RangeQuestion(bundle.getString("question")); + } catch (JSONException e) { + } + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View v = super.onCreateView(inflater, container, savedInstanceState); + inflater.inflate(R.layout.apptentive_survey_question_range, getAnswerContainer(v)); + return v; + } + + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + } + + @Override + public void onResume() { + super.onResume(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + } + + @Override + public boolean isValid() { + // TODO + boolean valid = !question.isRequired() || false; + return valid; + } + + @Override + public Object getAnswer() { + int value = 10; // TODO: Get real value + try { + JSONArray jsonArray = new JSONArray(); + JSONObject jsonObject = new JSONObject(); + jsonArray.put(jsonObject); + jsonObject.put("value", value); + return jsonArray; + } catch (JSONException e) { + // Return null; + } + return null; + } +} diff --git a/apptentive/src/main/res/layout/apptentive_survey_question_range.xml b/apptentive/src/main/res/layout/apptentive_survey_question_range.xml new file mode 100644 index 000000000..29cfe9afd --- /dev/null +++ b/apptentive/src/main/res/layout/apptentive_survey_question_range.xml @@ -0,0 +1,14 @@ + + + + + + + From cb3fa7074af8f0a402f04d047eb458cde9d172b4 Mon Sep 17 00:00:00 2001 From: skykelsey Date: Wed, 15 Jun 2016 16:49:26 -0700 Subject: [PATCH 10/26] Add radio buttons and all labels to range question type. ANDROID-694 --- .../view/survey/RangeSurveyQuestionView.java | 44 ++++++++++++++++++- .../apptentive_survey_question_range.xml | 14 ------ ...pptentive_survey_question_range_answer.xml | 37 ++++++++++++++++ ...pptentive_survey_question_range_choice.xml | 15 +++++++ 4 files changed, 95 insertions(+), 15 deletions(-) delete mode 100644 apptentive/src/main/res/layout/apptentive_survey_question_range.xml create mode 100644 apptentive/src/main/res/layout/apptentive_survey_question_range_answer.xml create mode 100644 apptentive/src/main/res/layout/apptentive_survey_question_range_choice.xml diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java index 6305f9624..8ff110f19 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java @@ -7,10 +7,15 @@ package com.apptentive.android.sdk.module.engagement.interaction.view.survey; import android.os.Bundle; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.TextView; +import com.apptentive.android.sdk.ApptentiveLog; import com.apptentive.android.sdk.R; import com.apptentive.android.sdk.module.engagement.interaction.model.survey.RangeQuestion; @@ -18,6 +23,8 @@ import org.json.JSONException; import org.json.JSONObject; +import java.text.NumberFormat; + public class RangeSurveyQuestionView extends BaseSurveyQuestionView { @@ -44,7 +51,42 @@ public void onCreate(Bundle savedInstanceState) { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = super.onCreateView(inflater, container, savedInstanceState); - inflater.inflate(R.layout.apptentive_survey_question_range, getAnswerContainer(v)); + ViewGroup answerContainer = getAnswerContainer(v); + ViewGroup answer = (ViewGroup) inflater.inflate(R.layout.apptentive_survey_question_range_answer, answerContainer, false); + answerContainer.addView(answer); + + String minLabel = question.getMinLabel(); + if (!TextUtils.isEmpty(minLabel)) { + TextView minLabelTextView = (TextView) answer.findViewById(R.id.min_label); + minLabelTextView.setText(minLabel); + } + String maxLabel = question.getMaxLabel(); + if (!TextUtils.isEmpty(maxLabel)) { + TextView maxLabelTextView = (TextView) answer.findViewById(R.id.max_label); + maxLabelTextView.setText(maxLabel); + } + + LinearLayout rangeContainer = (LinearLayout) answer.findViewById(R.id.range_container); + int min = question.getMin(); + int max = question.getMax(); + + NumberFormat defaultNumberFormat = NumberFormat.getInstance(); + + for (int i = min; i <= max; i++) { + try { + CompoundButton compoundButton = (CompoundButton) inflater.inflate(R.layout.apptentive_survey_question_range_choice, rangeContainer, false); + compoundButton.setText(defaultNumberFormat.format(i)); + rangeContainer.addView(compoundButton); + } catch (Throwable e) { + String message = "Error"; + while (e != null) { + ApptentiveLog.e(message, e); + message = " caused by:"; + e = e.getCause(); + } + throw new RuntimeException(e); + } + } return v; } diff --git a/apptentive/src/main/res/layout/apptentive_survey_question_range.xml b/apptentive/src/main/res/layout/apptentive_survey_question_range.xml deleted file mode 100644 index 29cfe9afd..000000000 --- a/apptentive/src/main/res/layout/apptentive_survey_question_range.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - diff --git a/apptentive/src/main/res/layout/apptentive_survey_question_range_answer.xml b/apptentive/src/main/res/layout/apptentive_survey_question_range_answer.xml new file mode 100644 index 000000000..c3d1568ab --- /dev/null +++ b/apptentive/src/main/res/layout/apptentive_survey_question_range_answer.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + diff --git a/apptentive/src/main/res/layout/apptentive_survey_question_range_choice.xml b/apptentive/src/main/res/layout/apptentive_survey_question_range_choice.xml new file mode 100644 index 000000000..63f3d9cc2 --- /dev/null +++ b/apptentive/src/main/res/layout/apptentive_survey_question_range_choice.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file From 2c72296e505cedad8501cd8402de5a99b946f83c Mon Sep 17 00:00:00 2001 From: skykelsey Date: Wed, 15 Jun 2016 16:56:23 -0700 Subject: [PATCH 11/26] Use RadioButton and RadioGroup for range question. ANDROID-694 --- .../view/survey/RangeSurveyQuestionView.java | 12 ++++++------ .../apptentive_survey_question_range_answer.xml | 12 ++++++------ .../apptentive_survey_question_range_choice.xml | 16 ++++++++-------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java index 8ff110f19..d15370163 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java @@ -11,8 +11,8 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.CompoundButton; -import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; import android.widget.TextView; import com.apptentive.android.sdk.ApptentiveLog; @@ -66,7 +66,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa maxLabelTextView.setText(maxLabel); } - LinearLayout rangeContainer = (LinearLayout) answer.findViewById(R.id.range_container); + RadioGroup radioGroup = (RadioGroup) answer.findViewById(R.id.range_container); int min = question.getMin(); int max = question.getMax(); @@ -74,9 +74,9 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa for (int i = min; i <= max; i++) { try { - CompoundButton compoundButton = (CompoundButton) inflater.inflate(R.layout.apptentive_survey_question_range_choice, rangeContainer, false); - compoundButton.setText(defaultNumberFormat.format(i)); - rangeContainer.addView(compoundButton); + RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.apptentive_survey_question_range_choice, radioGroup, false); + radioButton.setText(defaultNumberFormat.format(i)); + radioGroup.addView(radioButton); } catch (Throwable e) { String message = "Error"; while (e != null) { diff --git a/apptentive/src/main/res/layout/apptentive_survey_question_range_answer.xml b/apptentive/src/main/res/layout/apptentive_survey_question_range_answer.xml index c3d1568ab..9aa6939e0 100644 --- a/apptentive/src/main/res/layout/apptentive_survey_question_range_answer.xml +++ b/apptentive/src/main/res/layout/apptentive_survey_question_range_answer.xml @@ -11,13 +11,13 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingBottom="4dp"> - + - + - \ No newline at end of file + \ No newline at end of file From c073b43f85ae3a4860e1c90595bdd3926734b5ca Mon Sep 17 00:00:00 2001 From: skykelsey Date: Wed, 15 Jun 2016 17:41:14 -0700 Subject: [PATCH 12/26] Update survey range question styles. ANDROID-694 --- .../res/layout/apptentive_survey_question_range_answer.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apptentive/src/main/res/layout/apptentive_survey_question_range_answer.xml b/apptentive/src/main/res/layout/apptentive_survey_question_range_answer.xml index 9aa6939e0..bdc9733e1 100644 --- a/apptentive/src/main/res/layout/apptentive_survey_question_range_answer.xml +++ b/apptentive/src/main/res/layout/apptentive_survey_question_range_answer.xml @@ -10,11 +10,12 @@ + android:paddingBottom="20dp"> @@ -25,7 +26,7 @@ android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_below="@id/range_container" - android:textAppearance="@style/Apptentive.TextAppearance.Body1"/> + android:textAppearance="@style/Apptentive.TextAppearance.Caption"/> + android:textAppearance="@style/Apptentive.TextAppearance.Caption"/> From 10a733c6efb5e7e14af1d14a6b33309fad919e6d Mon Sep 17 00:00:00 2001 From: skykelsey Date: Wed, 15 Jun 2016 20:35:38 -0700 Subject: [PATCH 13/26] Keep track of selected range value, implement getAnswer(). ANDROID-694 --- .../view/survey/RangeSurveyQuestionView.java | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java index d15370163..62eca9f7b 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java @@ -11,6 +11,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.CompoundButton; import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.TextView; @@ -26,7 +27,17 @@ import java.text.NumberFormat; -public class RangeSurveyQuestionView extends BaseSurveyQuestionView { +public class RangeSurveyQuestionView extends BaseSurveyQuestionView implements RadioButton.OnCheckedChangeListener { + + private RadioGroup radioGroup; + private int min; + private int max; + private String minLabel; + private String maxLabel; + + // TODO: Save these + private boolean valueWasSelected; + private int selectedValue; public static RangeSurveyQuestionView newInstance(RangeQuestion question) { RangeSurveyQuestionView f = new RangeSurveyQuestionView(); @@ -46,6 +57,10 @@ public void onCreate(Bundle savedInstanceState) { } catch (JSONException e) { } } + min = question.getMin(); + max = question.getMax(); + minLabel = question.getMinLabel(); + maxLabel = question.getMaxLabel(); } @Override @@ -55,20 +70,16 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa ViewGroup answer = (ViewGroup) inflater.inflate(R.layout.apptentive_survey_question_range_answer, answerContainer, false); answerContainer.addView(answer); - String minLabel = question.getMinLabel(); if (!TextUtils.isEmpty(minLabel)) { TextView minLabelTextView = (TextView) answer.findViewById(R.id.min_label); minLabelTextView.setText(minLabel); } - String maxLabel = question.getMaxLabel(); if (!TextUtils.isEmpty(maxLabel)) { TextView maxLabelTextView = (TextView) answer.findViewById(R.id.max_label); maxLabelTextView.setText(maxLabel); } - RadioGroup radioGroup = (RadioGroup) answer.findViewById(R.id.range_container); - int min = question.getMin(); - int max = question.getMax(); + radioGroup = (RadioGroup) answer.findViewById(R.id.range_container); NumberFormat defaultNumberFormat = NumberFormat.getInstance(); @@ -76,6 +87,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa try { RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.apptentive_survey_question_range_choice, radioGroup, false); radioButton.setText(defaultNumberFormat.format(i)); + radioButton.setTag(i); radioGroup.addView(radioButton); } catch (Throwable e) { String message = "Error"; @@ -97,6 +109,11 @@ public void onViewCreated(View view, Bundle savedInstanceState) { @Override public void onResume() { super.onResume(); + + for (int i = 0; i < radioGroup.getChildCount(); i++) { + RadioButton radioButton = (RadioButton) radioGroup.getChildAt(i); + radioButton.setOnCheckedChangeListener(this); + } } @Override @@ -113,16 +130,23 @@ public boolean isValid() { @Override public Object getAnswer() { - int value = 10; // TODO: Get real value - try { - JSONArray jsonArray = new JSONArray(); - JSONObject jsonObject = new JSONObject(); - jsonArray.put(jsonObject); - jsonObject.put("value", value); - return jsonArray; - } catch (JSONException e) { - // Return null; + if (valueWasSelected) { + try { + JSONArray jsonArray = new JSONArray(); + JSONObject jsonObject = new JSONObject(); + jsonArray.put(jsonObject); + jsonObject.put("value", selectedValue); + return jsonArray; + } catch (JSONException e) { + // Return null; + } } return null; } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + selectedValue = (int) buttonView.getTag(); + valueWasSelected = true; + } } From 2d54b8f45d49443c879b8c4a9afba996c96df472 Mon Sep 17 00:00:00 2001 From: skykelsey Date: Thu, 16 Jun 2016 10:18:53 -0700 Subject: [PATCH 14/26] Clean up state restoration in range question. ANDROID-694 --- .../view/survey/RangeSurveyQuestionView.java | 68 ++++++++++--------- ...pptentive_survey_question_range_choice.xml | 3 +- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java index 62eca9f7b..af95382d3 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java @@ -7,6 +7,7 @@ package com.apptentive.android.sdk.module.engagement.interaction.view.survey; import android.os.Bundle; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; @@ -29,13 +30,17 @@ public class RangeSurveyQuestionView extends BaseSurveyQuestionView implements RadioButton.OnCheckedChangeListener { - private RadioGroup radioGroup; + private static final NumberFormat defaultNumberFormat = NumberFormat.getInstance(); + private static final String KEY_VALUE_WAS_SELECTED = "value_was_selected"; + private static final String KEY_SELECTED_VALUE = "selected_value"; + private int min; private int max; private String minLabel; private String maxLabel; - // TODO: Save these + private RadioGroup radioGroup; + private boolean valueWasSelected; private int selectedValue; @@ -55,6 +60,7 @@ public void onCreate(Bundle savedInstanceState) { try { question = new RangeQuestion(bundle.getString("question")); } catch (JSONException e) { + // Nothing } } min = question.getMin(); @@ -81,51 +87,45 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa radioGroup = (RadioGroup) answer.findViewById(R.id.range_container); - NumberFormat defaultNumberFormat = NumberFormat.getInstance(); - for (int i = min; i <= max; i++) { - try { - RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.apptentive_survey_question_range_choice, radioGroup, false); - radioButton.setText(defaultNumberFormat.format(i)); - radioButton.setTag(i); - radioGroup.addView(radioButton); - } catch (Throwable e) { - String message = "Error"; - while (e != null) { - ApptentiveLog.e(message, e); - message = " caused by:"; - e = e.getCause(); - } - throw new RuntimeException(e); - } + RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.apptentive_survey_question_range_choice, radioGroup, false); + radioButton.setText(defaultNumberFormat.format(i)); + radioButton.setTag(i); + radioButton.setOnCheckedChangeListener(this); + radioGroup.addView(radioButton); } return v; } - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putBoolean(KEY_VALUE_WAS_SELECTED, valueWasSelected); + outState.putInt(KEY_SELECTED_VALUE, selectedValue); + super.onSaveInstanceState(outState); } @Override - public void onResume() { - super.onResume(); + public void onViewStateRestored(@Nullable Bundle savedInstanceState) { + super.onViewStateRestored(savedInstanceState); + + // Restore instance state after the fragment tries to to avoid any UI weirdness. + if (savedInstanceState != null) { + valueWasSelected = savedInstanceState.getBoolean(KEY_VALUE_WAS_SELECTED, false); + selectedValue = savedInstanceState.getInt(KEY_SELECTED_VALUE, 0); + } for (int i = 0; i < radioGroup.getChildCount(); i++) { RadioButton radioButton = (RadioButton) radioGroup.getChildAt(i); - radioButton.setOnCheckedChangeListener(this); + if (valueWasSelected && (int) radioButton.getTag() == selectedValue) { + radioButton.setChecked(true); + return; + } } } - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } - @Override public boolean isValid() { - // TODO - boolean valid = !question.isRequired() || false; - return valid; + return !question.isRequired() || valueWasSelected; } @Override @@ -146,7 +146,9 @@ public Object getAnswer() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - selectedValue = (int) buttonView.getTag(); - valueWasSelected = true; + if (isChecked) { + selectedValue = (int) buttonView.getTag(); + valueWasSelected = true; + } } } diff --git a/apptentive/src/main/res/layout/apptentive_survey_question_range_choice.xml b/apptentive/src/main/res/layout/apptentive_survey_question_range_choice.xml index 4779a8ba1..b0629fc54 100644 --- a/apptentive/src/main/res/layout/apptentive_survey_question_range_choice.xml +++ b/apptentive/src/main/res/layout/apptentive_survey_question_range_choice.xml @@ -12,4 +12,5 @@ style="@style/Widget.AppCompat.CompoundButton.RadioButton" android:button="@null" android:drawableBottom="?android:attr/listChoiceIndicatorSingle" - android:gravity="center_horizontal|bottom"/> \ No newline at end of file + android:gravity="center_horizontal|bottom" + android:saveEnabled="false"/> \ No newline at end of file From 155fb3c3f034e73f55bcef8fadb41ebaf28724c5 Mon Sep 17 00:00:00 2001 From: skykelsey Date: Thu, 16 Jun 2016 10:57:07 -0700 Subject: [PATCH 15/26] Fire listener for validation when answer is selected. ANDROID-694 --- .../interaction/view/survey/RangeSurveyQuestionView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java index af95382d3..b8e6d17a4 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/RangeSurveyQuestionView.java @@ -17,7 +17,6 @@ import android.widget.RadioGroup; import android.widget.TextView; -import com.apptentive.android.sdk.ApptentiveLog; import com.apptentive.android.sdk.R; import com.apptentive.android.sdk.module.engagement.interaction.model.survey.RangeQuestion; @@ -149,6 +148,7 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (isChecked) { selectedValue = (int) buttonView.getTag(); valueWasSelected = true; + fireListener(); } } } From 2e4a6ad7eadb01bfa48f7093b5620b883d5355ab Mon Sep 17 00:00:00 2001 From: skykelsey Date: Mon, 20 Jun 2016 17:43:29 -0700 Subject: [PATCH 16/26] For each survey question, add a tag containing the question's index to the root view of the question. This enables simpler espresso tests. --- .../interaction/fragment/SurveyFragment.java | 7 ++++--- .../view/survey/BaseSurveyQuestionView.java | 18 ++++++++++++------ .../layout/apptentive_survey_question_base.xml | 1 + 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java index 0a3a40149..89353df57 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/fragment/SurveyFragment.java @@ -142,8 +142,9 @@ public void onClick(View view) { questionsContainer.removeAllViews(); // Then render all the questions - for (final Question question : questions) { - final BaseSurveyQuestionView surveyQuestionView; + for (int i = 0; i < questions.size(); i++) { + Question question = questions.get(i); + BaseSurveyQuestionView surveyQuestionView; if (question.getType() == Question.QUESTION_TYPE_SINGLELINE) { surveyQuestionView = TextSurveyQuestionView.newInstance((SinglelineQuestion) question); } else if (question.getType() == Question.QUESTION_TYPE_MULTICHOICE) { @@ -156,7 +157,7 @@ public void onClick(View view) { } if (surveyQuestionView != null) { surveyQuestionView.setOnSurveyQuestionAnsweredListener(this); - getRetainedChildFragmentManager().beginTransaction().add(R.id.questions, surveyQuestionView, question.getId()).commit(); + getRetainedChildFragmentManager().beginTransaction().add(R.id.questions, surveyQuestionView, Integer.toString(i)).commit(); } } } else { diff --git a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/BaseSurveyQuestionView.java b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/BaseSurveyQuestionView.java index 720543c97..9b79d6aa6 100644 --- a/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/BaseSurveyQuestionView.java +++ b/apptentive/src/main/java/com/apptentive/android/sdk/module/engagement/interaction/view/survey/BaseSurveyQuestionView.java @@ -12,6 +12,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; @@ -22,17 +23,18 @@ abstract public class BaseSurveyQuestionView extends Fragment implements SurveyQuestionView { - protected Q question; - private OnSurveyQuestionAnsweredListener listener; + private static final String SENT_METRIC = "sent_metric"; - protected TextView requiredView; - protected View dashView; - protected TextView instructionsView; + protected Q question; + private FrameLayout root; + private TextView requiredView; + private View dashView; + private TextView instructionsView; private View validationFailedBorder; - private static final String SENT_METRIC = "sent_metric"; private boolean sentMetric; + private OnSurveyQuestionAnsweredListener listener; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -41,10 +43,14 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + root = (FrameLayout) view.findViewById(R.id.question_base); requiredView = (TextView) view.findViewById(R.id.question_required); dashView = view.findViewById(R.id.dash_view); instructionsView = (TextView) view.findViewById(R.id.question_instructions); + // Makes UI tests easier. We can potentially obviate this if surveys used a RecyclerView. + root.setTag(Integer.parseInt(getTag())); + TextView title = (TextView) view.findViewById(R.id.question_title); title.setText(question.getValue()); diff --git a/apptentive/src/main/res/layout/apptentive_survey_question_base.xml b/apptentive/src/main/res/layout/apptentive_survey_question_base.xml index 14863307c..0e5974815 100644 --- a/apptentive/src/main/res/layout/apptentive_survey_question_base.xml +++ b/apptentive/src/main/res/layout/apptentive_survey_question_base.xml @@ -7,6 +7,7 @@ --> Date: Tue, 21 Jun 2016 10:17:33 -0700 Subject: [PATCH 17/26] Check in IDEA project file updates. --- .idea/codeStyleSettings.xml | 4 ++-- .idea/runConfigurations/Apptentive_Tests.xml | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml index d6262b915..50b9a0176 100644 --- a/.idea/codeStyleSettings.xml +++ b/.idea/codeStyleSettings.xml @@ -44,9 +44,9 @@