package org.commcare.tasks; import android.content.Context; import android.content.Intent; import androidx.core.util.Pair; import net.sqlcipher.database.SQLiteDatabase; import org.apache.commons.lang3.StringUtils; import org.commcare.CommCareApplication; import org.commcare.android.database.app.models.FormDefRecord; import org.commcare.android.database.app.models.UserKeyRecord; import org.commcare.android.database.user.models.ACase; import org.commcare.cases.ledger.Ledger; import org.commcare.core.encryption.CryptUtil; import org.commcare.core.network.AuthenticationInterceptor; import org.commcare.core.network.CaptivePortalRedirectException; import org.commcare.core.network.bitcache.BitCache; import org.commcare.data.xml.DataModelPullParser; import org.commcare.engine.cases.CaseUtils; import org.commcare.google.services.analytics.AnalyticsParamValue; import org.commcare.interfaces.CommcareRequestEndpoints; import org.commcare.models.database.SqlStorage; import org.commcare.models.database.user.models.AndroidCaseIndexTable; import org.commcare.models.database.user.models.EntityStorageCache; import org.commcare.models.encryption.ByteEncrypter; import org.commcare.modern.models.RecordTooLargeException; import org.commcare.network.DataPullRequester; import org.commcare.network.HttpUtils; import org.commcare.network.RemoteDataPullResponse; import org.commcare.preferences.HiddenPreferences; import org.commcare.preferences.ServerUrls; import org.commcare.resources.model.CommCareOTARestoreListener; import org.commcare.services.CommCareSessionService; import org.commcare.tasks.templates.CommCareTask; import org.commcare.util.LogTypes; import org.commcare.utils.FormSaveUtil; import org.commcare.utils.SessionUnavailableException; import org.commcare.utils.SyncDetailCalculations; import org.commcare.utils.UnknownSyncError; import org.commcare.xml.AndroidTransactionParserFactory; import org.javarosa.core.model.User; import org.javarosa.core.services.Logger; import org.javarosa.core.services.locale.Localization; import org.javarosa.core.util.PropertyUtils; import org.javarosa.xml.util.ActionableInvalidStructureException; import org.javarosa.xml.util.InvalidStructureException; import org.javarosa.xml.util.UnfullfilledRequirementsException; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.io.InputStream; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.util.Date; import java.util.Hashtable; import java.util.NoSuchElementException; import javax.crypto.SecretKey; /** * @author ctsims */ public abstract class DataPullTask<R> extends CommCareTask<Void, Integer, ResultAndError<DataPullTask.PullTaskResult>, R> implements CommCareOTARestoreListener { private final String server; private final String username; private final String password; protected final Context context; private int mCurrentProgress; private int mTotalItems; private long mSyncStartTime; public static final int DATA_PULL_TASK_ID = 10; public static final int PROGRESS_STARTED = 0; public static final int PROGRESS_CLEANED = 1; public static final int PROGRESS_AUTHED = 2; private static final int PROGRESS_DONE = 4; public static final int PROGRESS_RECOVERY_NEEDED = 8; public static final int PROGRESS_RECOVERY_STARTED = 16; private static final int PROGRESS_RECOVERY_FAIL_SAFE = 32; private static final int PROGRESS_RECOVERY_FAIL_BAD = 64; public static final int PROGRESS_PROCESSING = 128; public static final int PROGRESS_DOWNLOADING = 256; public static final int PROGRESS_DOWNLOADING_COMPLETE = 512; public static final int PROGRESS_SERVER_PROCESSING = 1024; private final DataPullRequester dataPullRequester; private final AsyncRestoreHelper asyncRestoreHelper; private final boolean blockRemoteKeyManagement; private boolean loginNeeded; private UserKeyRecord ukrForLogin; private boolean wasKeyLoggedIn; public DataPullTask(String username, String password, String userId, String server, Context context, DataPullRequester dataPullRequester, boolean blockRemoteKeyManagement) { this.server = server; this.username = username; this.password = password; this.context = context; this.taskId = DATA_PULL_TASK_ID; this.dataPullRequester = dataPullRequester; this.requestor = dataPullRequester.getHttpGenerator(username, password, userId); this.asyncRestoreHelper = new AsyncRestoreHelper(this); this.blockRemoteKeyManagement = blockRemoteKeyManagement; TAG = DataPullTask.class.getSimpleName(); } public DataPullTask(String username, String password, String userId, String server, Context context) { this(username, password, userId, server, context, CommCareApplication.instance().getDataPullRequester(), false); } // TODO PLM: once this task is refactored into manageable components, it should use the // ManagedAsyncTask pattern of checking for isCancelled() and aborting at safe places. @Override protected void onCancelled() { super.onCancelled(); wipeLoginIfItOccurred(); } private final CommcareRequestEndpoints requestor; @Override protected ResultAndError<PullTaskResult> doTaskBackground(Void... params) { if (!CommCareSessionService.sessionAliveLock.tryLock()) { // Don't try to sync if logging out is occurring return new ResultAndError<>(PullTaskResult.SESSION_EXPIRE, "Cannot sync while a logout is in process"); } try { return doTaskBackgroundHelper(); } finally { CommCareSessionService.sessionAliveLock.unlock(); } } private ResultAndError<PullTaskResult> doTaskBackgroundHelper() { if (StringUtils.isEmpty(server)) { return new ResultAndError<>(PullTaskResult.EMPTY_URL, Localization.get("sync.fail.empty.url")); } publishProgress(PROGRESS_STARTED); HiddenPreferences.setPostUpdateSyncNeeded(false); Logger.log(LogTypes.TYPE_USER, "Starting Sync"); determineIfLoginNeeded(); AndroidTransactionParserFactory factory = getTransactionParserFactory(); byte[] wrappedEncryptionKey = getEncryptionKey(); if (wrappedEncryptionKey == null) { this.publishProgress(PROGRESS_DONE); return new ResultAndError<>(PullTaskResult.ENCRYPTION_FAILURE, "Unable to get or generate encryption key"); } factory.initUserParser(wrappedEncryptionKey); if (!loginNeeded) { //Only purge cases if we already had a logged in user. Otherwise we probably can't read the DB. CaseUtils.purgeCases(); } return getRequestResultOrRetry(factory); } private void determineIfLoginNeeded() { try { loginNeeded = !CommCareApplication.instance().getSession().isActive(); } catch (SessionUnavailableException sue) { // expected if we aren't initialized. loginNeeded = true; } } private AndroidTransactionParserFactory getTransactionParserFactory() { return new AndroidTransactionParserFactory(context, requestor) { boolean publishedAuth = false; @Override public void reportProgress(int progress) { if (!publishedAuth) { DataPullTask.this.publishProgress(PROGRESS_AUTHED, progress); publishedAuth = true; } } }; } private byte[] getEncryptionKey() { byte[] key; if (loginNeeded) { initUKRForLogin(); if (ukrForLogin == null) { return null; } key = ukrForLogin.getEncryptedKey(); } else { key = CommCareApplication.instance().getSession().getLoggedInUser().getWrappedKey(); } this.publishProgress(PROGRESS_CLEANED); // Either way, we don't want to do this step again return key; } private void initUKRForLogin() { if (blockRemoteKeyManagement || shouldGenerateFirstKey()) { SecretKey newKey = CryptUtil.generateSemiRandomKey(); if (newKey == null) { return; } String sandboxId = PropertyUtils.genUUID().replace("-", ""); ukrForLogin = new UserKeyRecord(username, UserKeyRecord.generatePwdHash(password), ByteEncrypter.wrapByteArrayWithString(newKey.getEncoded(), password), new Date(), new Date(Long.MAX_VALUE), sandboxId); } else { ukrForLogin = UserKeyRecord.getCurrentValidRecordByPassword( CommCareApplication.instance().getCurrentApp(), username, password, true); if (ukrForLogin == null) { Logger.log(LogTypes.TYPE_ERROR_ASSERTION, "Shouldn't be able to not have a valid key record when OTA restoring with a key server"); } } } private static boolean shouldGenerateFirstKey() { String keyServer = ServerUrls.getKeyServer(); return keyServer == null || keyServer.equals(""); } private ResultAndError<PullTaskResult> getRequestResultOrRetry(AndroidTransactionParserFactory factory) { while (asyncRestoreHelper.retryWaitPeriodInProgress()) { try { Thread.sleep(500); } catch (InterruptedException e) { } if (isCancelled()) { return new ResultAndError<>(PullTaskResult.CANCELLED); } } PullTaskResult responseError = PullTaskResult.UNKNOWN_FAILURE; asyncRestoreHelper.retryAtTime = -1; try { ResultAndError<PullTaskResult> result = makeRequestAndHandleResponse(factory); if (PullTaskResult.RETRY_NEEDED.equals(result.data)) { asyncRestoreHelper.startReportingServerProgress(); return getRequestResultOrRetry(factory); } else { return result; } } catch (SocketTimeoutException e) { e.printStackTrace(); Logger.log(LogTypes.TYPE_WARNING_NETWORK, "Timed out listening to receive data during sync"); responseError = PullTaskResult.CONNECTION_TIMEOUT; } catch (UnknownHostException e) { e.printStackTrace(); Logger.log(LogTypes.TYPE_WARNING_NETWORK, "Couldn't sync due to bad network"); responseError = PullTaskResult.UNREACHABLE_HOST; } catch (AuthenticationInterceptor.PlainTextPasswordException e) { e.printStackTrace(); Logger.log(LogTypes.TYPE_ERROR_CONFIG_STRUCTURE, "Encountered PlainTextPasswordException during sync: Sending password over HTTP"); responseError = PullTaskResult.AUTH_OVER_HTTP; } catch (CaptivePortalRedirectException e) { e.printStackTrace(); Logger.log(LogTypes.TYPE_WARNING_NETWORK, "Couldn't sync due to presense of captive portal"); responseError = PullTaskResult.CAPTIVE_PORTAL; } catch (IOException e) { e.printStackTrace(); Logger.log(LogTypes.TYPE_WARNING_NETWORK, "Couldn't sync due to IO Error|" + e.getMessage()); } catch (UnknownSyncError e) { e.printStackTrace(); Logger.log(LogTypes.TYPE_WARNING_NETWORK, "Couldn't sync due to Unknown Error|" + e.getMessage()); } wipeLoginIfItOccurred(); this.publishProgress(PROGRESS_DONE); return new ResultAndError<>(responseError); } /** * @return the proper result, or null if we have not yet been able to determine the result to * return */ private ResultAndError<PullTaskResult> makeRequestAndHandleResponse(AndroidTransactionParserFactory factory) throws IOException, UnknownSyncError { RemoteDataPullResponse pullResponse = dataPullRequester.makeDataPullRequest(this, requestor, server, !loginNeeded); int responseCode = pullResponse.responseCode; Logger.log(LogTypes.TYPE_USER, "Data pull request opened. Response code: " + responseCode); if (responseCode == 401) { return handleAuthFailed(); } else if (responseCode >= 200 && responseCode < 300) { if (responseCode == 202) { return asyncRestoreHelper.handleRetryResponseCode(pullResponse); } else { return handleSuccessResponseCode(pullResponse, factory); } } else if (responseCode == 412) { return handleBadLocalState(factory); } else if (responseCode == 406) { return processErrorResponseWithMessage(pullResponse); } else if (responseCode == 500) { return handleServerError(); } else if (responseCode == 503 || responseCode == 429) { return handleRateLimitedError(); } else { throw new UnknownSyncError(); } } private ResultAndError<PullTaskResult> processErrorResponseWithMessage(RemoteDataPullResponse pullResponse) { return new ResultAndError<>(PullTaskResult.ACTIONABLE_FAILURE, HttpUtils.parseUserVisibleError(pullResponse.getResponse())); } private ResultAndError<PullTaskResult> handleAuthFailed() { wipeLoginIfItOccurred(); Logger.log(LogTypes.TYPE_USER, "Bad Auth Request for user!|" + username); return new ResultAndError<>(PullTaskResult.AUTH_FAILED); } /** * @return the proper result, or null if we have not yet been able to determine the result to * return */ private ResultAndError<PullTaskResult> handleSuccessResponseCode( RemoteDataPullResponse pullResponse, AndroidTransactionParserFactory factory) throws IOException, UnknownSyncError { asyncRestoreHelper.completeServerProgressBarIfShowing(); handleLoginNeededOnSuccess(); this.publishProgress(PROGRESS_AUTHED, 0); if (isCancelled()) { // About to enter data commit phase; last chance to finish early if cancelled. return new ResultAndError<>(PullTaskResult.CANCELLED); } this.publishProgress(PROGRESS_DOWNLOADING_COMPLETE, 0); Logger.log(LogTypes.TYPE_USER, "Remote Auth Successful|" + username); try { BitCache cache = pullResponse.writeResponseToCache(context); String syncToken = readInput(cache.retrieveCache(), factory); updateUserSyncToken(syncToken); onSuccessfulSync(); return new ResultAndError<>(PullTaskResult.DOWNLOAD_SUCCESS); } catch (XmlPullParserException e) { wipeLoginIfItOccurred(); e.printStackTrace(); Logger.log(LogTypes.TYPE_USER, "User Sync failed due to bad payload|" + e.getMessage()); return new ResultAndError<>(PullTaskResult.BAD_DATA, e.getMessage()); } catch (ActionableInvalidStructureException e) { wipeLoginIfItOccurred(); e.printStackTrace(); Logger.log(LogTypes.TYPE_USER, "User Sync failed due to bad payload|" + e.getMessage()); return new ResultAndError<>(PullTaskResult.BAD_DATA_REQUIRES_INTERVENTION, e.getLocalizedMessage()); } catch (InvalidStructureException e) { wipeLoginIfItOccurred(); e.printStackTrace(); Logger.log(LogTypes.TYPE_USER, "User Sync failed due to bad payload|" + e.getMessage()); return new ResultAndError<>(PullTaskResult.BAD_DATA, e.getMessage()); } catch (UnfullfilledRequirementsException e) { e.printStackTrace(); Logger.log(LogTypes.TYPE_ERROR_ASSERTION, "User sync failed oddly, unfulfilled reqs |" + e.getMessage()); throw new UnknownSyncError(); } catch (IllegalStateException e) { e.printStackTrace(); Logger.log(LogTypes.TYPE_ERROR_ASSERTION, "User sync failed oddly, IllegalStateException |" + e.getMessage()); throw new UnknownSyncError(); } catch (RecordTooLargeException e) { wipeLoginIfItOccurred(); e.printStackTrace(); Logger.log(LogTypes.TYPE_ERROR_ASSERTION, "Storage Full during user sync |" + e.getMessage()); return new ResultAndError<>(PullTaskResult.STORAGE_FULL); } } private void handleLoginNeededOnSuccess() { if (loginNeeded) { // This is currently necessary to make sure that data is encoded, but there is // probably a better way to do it CommCareApplication.instance().startUserSession( ByteEncrypter.unwrapByteArrayWithString(ukrForLogin.getEncryptedKey(), password), ukrForLogin, false); wasKeyLoggedIn = true; } } /** * @return the proper result, or null if we have not yet been able to determine the result to * return */ private ResultAndError<PullTaskResult> handleBadLocalState(AndroidTransactionParserFactory factory) throws UnknownSyncError { this.publishProgress(PROGRESS_RECOVERY_NEEDED); Logger.log(LogTypes.TYPE_USER, "Sync Recovery Triggered"); Pair<Integer, String> returnCodeAndMessageFromRecovery = recover(requestor, factory); int returnCode = returnCodeAndMessageFromRecovery.first; String failureReason = returnCodeAndMessageFromRecovery.second; if (returnCode == PROGRESS_DONE) { // Recovery was successful onSuccessfulSync(); return new ResultAndError<>(PullTaskResult.DOWNLOAD_SUCCESS); } else if (returnCode == PROGRESS_RECOVERY_FAIL_SAFE || returnCode == PROGRESS_RECOVERY_FAIL_BAD) { wipeLoginIfItOccurred(); this.publishProgress(PROGRESS_DONE); return new ResultAndError<>(PullTaskResult.RECOVERY_FAILURE, failureReason); } else { throw new UnknownSyncError(); } } private void onSuccessfulSync() { recordSuccessfulSyncTime(username); Intent i = new Intent("org.commcare.dalvik.api.action.data.update"); this.context.sendBroadcast(i); if (loginNeeded) { CommCareApplication.instance().getAppStorage(UserKeyRecord.class).write(ukrForLogin); } Logger.log(LogTypes.TYPE_USER, "User Sync Successful|" + username); updateCurrentUser(password); this.publishProgress(PROGRESS_DONE); } private ResultAndError<PullTaskResult> handleServerError() { wipeLoginIfItOccurred(); Logger.log(LogTypes.TYPE_USER, "500 Server Error during data pull|" + username); return new ResultAndError<>(PullTaskResult.SERVER_ERROR); } private ResultAndError<PullTaskResult> handleRateLimitedError() { wipeLoginIfItOccurred(); Logger.log(LogTypes.TYPE_USER, "503 Server Error during data pull|" + username); return new ResultAndError<>(PullTaskResult.RATE_LIMITED_SERVER_ERROR); } private void wipeLoginIfItOccurred() { if (wasKeyLoggedIn) { CommCareApplication.instance().releaseUserResourcesAndServices(); } } @Override public void tryAbort() { if (requestor != null) { requestor.abortCurrentRequest(); } } private static void recordSuccessfulSyncTime(String username) { CommCareApplication.instance().getCurrentApp().getAppPreferences().edit() .putLong(SyncDetailCalculations.getLastSyncKey(username), new Date().getTime()).apply(); } //TODO: This and the normal sync share a ton of code. It's hard to really... figure out the right way to private Pair<Integer, String> recover(CommcareRequestEndpoints requestor, AndroidTransactionParserFactory factory) { while (asyncRestoreHelper.retryWaitPeriodInProgress()) { try { Thread.sleep(500); } catch (InterruptedException e) { } if (isCancelled()) { return new Pair<>(PROGRESS_RECOVERY_FAIL_SAFE, "Task was cancelled during recovery sync"); } } // This chunk is the safe field of operations which can all fail in IO in such a way that // we can just report back that things didn't work and don't need to attempt any recovery // or additional work BitCache cache; try { // Make a new request without all of the flags RemoteDataPullResponse pullResponse = dataPullRequester.makeDataPullRequest(this, requestor, server, false); if (!(pullResponse.responseCode >= 200 && pullResponse.responseCode < 300)) { return new Pair<>(PROGRESS_RECOVERY_FAIL_SAFE, "Received a non-success response during recovery sync"); } else if (pullResponse.responseCode == 202) { ResultAndError<PullTaskResult> result = asyncRestoreHelper.handleRetryResponseCode(pullResponse); if (PullTaskResult.RETRY_NEEDED.equals(result.data)) { asyncRestoreHelper.startReportingServerProgress(); return recover(requestor, factory); } else { return new Pair<>(PROGRESS_RECOVERY_FAIL_SAFE, "Retry response during recovery sync was improperly formed"); } } // Grab a cache. The plan is to download the incoming data, wipe (move) the existing // db, and then restore fresh from the downloaded file cache = pullResponse.writeResponseToCache(context); } catch (IOException e) { e.printStackTrace(); //Ok, well, we're bailing here, but we didn't make any changes Logger.log(LogTypes.TYPE_USER, "Sync Recovery Failed due to IOException|" + e.getMessage()); return new Pair<>(PROGRESS_RECOVERY_FAIL_SAFE, ""); } this.publishProgress(PROGRESS_RECOVERY_STARTED); Logger.log(LogTypes.TYPE_USER, "Sync Recovery payload downloaded"); //Ok. Here's where things get real. We now have a stable copy of the fresh data from the //server, so it's "safe" for us to wipe the casedb copy of it. //CTS: We're not doing this in a super good way right now, need to be way more fault tolerant. //this is the temporary implementation of everything past this point //Wipe storage SQLiteDatabase userDb = CommCareApplication.instance().getUserDbHandle(); userDb.beginTransaction(); wipeStorageForFourTwelveSync(userDb); try { String syncToken = readInputWithoutCommit(cache.retrieveCache(), factory); updateUserSyncToken(syncToken); Logger.log(LogTypes.TYPE_USER, "Sync Recovery Successful"); userDb.setTransactionSuccessful(); return new Pair<>(PROGRESS_DONE, ""); } catch (InvalidStructureException | XmlPullParserException | UnfullfilledRequirementsException | SessionUnavailableException | IOException e) { Logger.exception("Sync recovery failed|" + e.getLocalizedMessage(), e); return new Pair<>(PROGRESS_RECOVERY_FAIL_BAD, e.getLocalizedMessage()); } finally { userDb.endTransaction(); //destroy temp file cache.release(); } } private void wipeStorageForFourTwelveSync(SQLiteDatabase userDb) { SqlStorage.wipeTableWithoutCommit(userDb, ACase.STORAGE_KEY); SqlStorage.wipeTableWithoutCommit(userDb, Ledger.STORAGE_KEY); SqlStorage.wipeTableWithoutCommit(userDb, AndroidCaseIndexTable.TABLE_NAME); EntityStorageCache.wipeCacheForCurrentAppWithoutCommit(userDb); } private void updateCurrentUser(String password) { SqlStorage<User> storage = CommCareApplication.instance().getUserStorage("USER", User.class); User u = storage.getRecordForValue(User.META_USERNAME, username); CommCareApplication.instance().getSession().setCurrentUser(u, password); } private void updateUserSyncToken(String syncToken) { SqlStorage<User> storage = CommCareApplication.instance().getUserStorage("USER", User.class); try { User u = storage.getRecordForValue(User.META_USERNAME, username); u.setLastSyncToken(syncToken); storage.write(u); } catch (NoSuchElementException nsee) { //TODO: Something here? Maybe figure out if we downloaded a user from the server and attach the data to it? } } private void initParsers(AndroidTransactionParserFactory factory) { factory.initCaseParser(); factory.initStockParser(); Hashtable<String, String> formNamespaces = FormSaveUtil.getNamespaceToFilePathMap(CommCareApplication.instance().getAppStorage(FormDefRecord.class)); factory.initFormInstanceParser(formNamespaces); } private void parseStream(InputStream stream, AndroidTransactionParserFactory factory) throws InvalidStructureException, IOException, XmlPullParserException, UnfullfilledRequirementsException { DataModelPullParser parser = new DataModelPullParser(stream, factory, true, false, this); parser.parse(); } private String readInputWithoutCommit(InputStream stream, AndroidTransactionParserFactory factory) throws InvalidStructureException, IOException, XmlPullParserException, UnfullfilledRequirementsException { initParsers(factory); parseStream(stream, factory); return factory.getSyncToken(); } private String readInput(InputStream stream, AndroidTransactionParserFactory factory) throws InvalidStructureException, IOException, XmlPullParserException, UnfullfilledRequirementsException { initParsers(factory); //this is _really_ coupled, but we'll tolerate it for now because of the absurd performance gains SQLiteDatabase db = CommCareApplication.instance().getUserDbHandle(); db.beginTransaction(); try { parseStream(stream, factory); db.setTransactionSuccessful(); } finally { db.endTransaction(); } //Return the sync token ID return factory.getSyncToken(); } //BEGIN - OTA Listener methods below - Note that most of the methods //below weren't really implemented @Override public void onUpdate(int numberCompleted) { mCurrentProgress = numberCompleted; int millisecondsElapsed = (int)(System.currentTimeMillis() - mSyncStartTime); this.publishProgress(PROGRESS_PROCESSING, mCurrentProgress, mTotalItems, millisecondsElapsed); } @Override public void setTotalForms(int totalItemCount) { mTotalItems = totalItemCount; mCurrentProgress = 0; mSyncStartTime = System.currentTimeMillis(); this.publishProgress(PROGRESS_PROCESSING, mCurrentProgress, mTotalItems, 0); } protected void reportServerProgress(int completedSoFar, int total) { publishProgress(PROGRESS_SERVER_PROCESSING, completedSoFar, total); } public void reportDownloadProgress(int totalRead) { publishProgress(PROGRESS_DOWNLOADING, totalRead); } public AsyncRestoreHelper getAsyncRestoreHelper() { return this.asyncRestoreHelper; } public enum PullTaskResult { DOWNLOAD_SUCCESS(AnalyticsParamValue.SYNC_SUCCESS), RETRY_NEEDED(AnalyticsParamValue.SYNC_FAIL_RETRY_NEEDED), EMPTY_URL(AnalyticsParamValue.SYNC_FAIL_EMPTY_URL), AUTH_FAILED(AnalyticsParamValue.SYNC_FAIL_AUTH), BAD_DATA(AnalyticsParamValue.SYNC_FAIL_BAD_DATA), BAD_DATA_REQUIRES_INTERVENTION(AnalyticsParamValue.SYNC_FAIL_BAD_DATA), UNKNOWN_FAILURE(AnalyticsParamValue.SYNC_FAIL_UNKNOWN), CANCELLED(AnalyticsParamValue.SYNC_FAIL_CANCELLED), ENCRYPTION_FAILURE(AnalyticsParamValue.SYNC_FAIL_ENCRYPTION), SESSION_EXPIRE(AnalyticsParamValue.SYNC_FAIL_SESSION_EXPIRE), RECOVERY_FAILURE(AnalyticsParamValue.SYNC_FAIL_RECOVERY), ACTIONABLE_FAILURE(AnalyticsParamValue.SYNC_FAIL_ACTIONABLE), UNREACHABLE_HOST(AnalyticsParamValue.SYNC_FAIL_UNREACHABLE_HOST), CONNECTION_TIMEOUT(AnalyticsParamValue.SYNC_FAIL_CONNECTION_TIMEOUT), SERVER_ERROR(AnalyticsParamValue.SYNC_FAIL_SERVER_ERROR), RATE_LIMITED_SERVER_ERROR(AnalyticsParamValue.SYNC_FAIL_RATE_LIMITED_SERVER_ERROR), STORAGE_FULL(AnalyticsParamValue.SYNC_FAIL_STORAGE_FULL), CAPTIVE_PORTAL(AnalyticsParamValue.SYNC_FAIL_CAPTIVE_PORTAL), AUTH_OVER_HTTP(AnalyticsParamValue.SYNC_FAIL_AUTH_OVER_HTTP); public final String analyticsFailureReasonParam; PullTaskResult(String analyticsParam) { this.analyticsFailureReasonParam = analyticsParam; } } }