package org.commcare.activities; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.os.Bundle; import android.support.annotation.AnimRes; import android.support.annotation.LayoutRes; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.ImageView; import android.widget.Toast; import org.commcare.CommCareApplication; import org.commcare.dalvik.R; import org.commcare.interfaces.UiLoadedListener; import org.commcare.google.services.analytics.GoogleAnalyticsFields; import org.commcare.google.services.analytics.GoogleAnalyticsUtils; import org.commcare.tasks.DataPullTask; import org.commcare.tasks.ProcessAndSendTask; import org.commcare.tasks.PullTaskResultReceiver; import org.commcare.tasks.ResultAndError; import org.commcare.utils.SyncDetailCalculations; import org.commcare.views.dialogs.CustomProgressDialog; import org.javarosa.core.services.locale.Localization; public abstract class SyncCapableCommCareActivity<T> extends SessionAwareCommCareActivity<T> implements PullTaskResultReceiver { protected static final int MENU_SYNC = Menu.FIRST; private static final int MENU_GROUP_SYNC_ACTION = Menu.FIRST; private static final boolean SUCCESS = true; private static final boolean FAIL = false; private static final String KEY_LAST_ICON_TRIGGER = "last-icon-trigger"; protected boolean isSyncUserLaunched = false; protected FormAndDataSyncer formAndDataSyncer; private SyncIconState syncStateForIcon; private SyncIconTrigger lastIconTrigger; private MenuItem currentSyncMenuItem; private UiLoadedListener uiLoadedListener; @Override protected void onCreateSessionSafe(Bundle savedInstanceState) { formAndDataSyncer = new FormAndDataSyncer(); computeSyncState(savedInstanceState == null ? SyncIconTrigger.NO_ANIMATION : (SyncIconTrigger)savedInstanceState.getSerializable(KEY_LAST_ICON_TRIGGER)); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putSerializable(KEY_LAST_ICON_TRIGGER, lastIconTrigger); } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); if (uiLoadedListener != null) { uiLoadedListener.onUiLoaded(); } } /** * Attempts first to send unsent forms to the server. If any forms are sent, a sync will be * triggered after they are submitted. If no forms are sent, triggers a sync explicitly. */ protected void sendFormsOrSync(boolean userTriggeredSync) { boolean formsSentToServer = checkAndStartUnsentFormsTask(true, userTriggeredSync); if (!formsSentToServer) { formAndDataSyncer.syncDataForLoggedInUser(this, false, userTriggeredSync); } } protected boolean checkAndStartUnsentFormsTask(boolean syncAfterwards, boolean userTriggered) { isSyncUserLaunched = userTriggered; return formAndDataSyncer.checkAndStartUnsentFormsTask(this, syncAfterwards, userTriggered); } @Override public void handlePullTaskResult(ResultAndError<DataPullTask.PullTaskResult> resultAndError, boolean userTriggeredSync, boolean formsToSend) { if (CommCareApplication.instance().isConsumerApp()) { return; } DataPullTask.PullTaskResult result = resultAndError.data; String reportSyncLabel = result.getCorrespondingGoogleAnalyticsLabel(); int reportSyncValue = result.getCorrespondingGoogleAnalyticsValue(); switch (result) { case AUTH_FAILED: updateUiAfterDataPullOrSend(Localization.get("sync.fail.auth.loggedin"), FAIL); break; case BAD_DATA: case BAD_DATA_REQUIRES_INTERVENTION: updateUiAfterDataPullOrSend(Localization.get("sync.fail.bad.data"), FAIL); break; case DOWNLOAD_SUCCESS: if (formsToSend) { reportSyncValue = GoogleAnalyticsFields.VALUE_WITH_SEND_FORMS; } else { reportSyncValue = GoogleAnalyticsFields.VALUE_JUST_PULL_DATA; } updateUiAfterDataPullOrSend(Localization.get("sync.success.synced"), SUCCESS); break; case SERVER_ERROR: updateUiAfterDataPullOrSend(Localization.get("sync.fail.server.error"), FAIL); break; case UNREACHABLE_HOST: updateUiAfterDataPullOrSend(Localization.get("sync.fail.bad.network"), FAIL); break; case CONNECTION_TIMEOUT: updateUiAfterDataPullOrSend(Localization.get("sync.fail.timeout"), FAIL); break; case UNKNOWN_FAILURE: updateUiAfterDataPullOrSend(Localization.get("sync.fail.unknown"), FAIL); break; case ACTIONABLE_FAILURE: updateUiAfterDataPullOrSend(resultAndError.errorMessage, FAIL); break; } if (userTriggeredSync) { GoogleAnalyticsUtils.reportSyncAttempt( GoogleAnalyticsFields.ACTION_USER_SYNC_ATTEMPT, reportSyncLabel, reportSyncValue); } else { GoogleAnalyticsUtils.reportSyncAttempt( GoogleAnalyticsFields.ACTION_AUTO_SYNC_ATTEMPT, reportSyncLabel, reportSyncValue); } } @Override public void handlePullTaskUpdate(Integer... update) { handleSyncUpdate(this, update); } public static void handleSyncUpdate(CommCareActivity activity, Integer... update) { int progressCode = update[0]; if (progressCode == DataPullTask.PROGRESS_STARTED) { activity.updateProgress(Localization.get("sync.progress.purge"), DataPullTask.DATA_PULL_TASK_ID); } else if (progressCode == DataPullTask.PROGRESS_CLEANED) { activity.updateProgress(Localization.get("sync.progress.authing"), DataPullTask.DATA_PULL_TASK_ID); activity.updateProgressBarVisibility(false); } else if (progressCode == DataPullTask.PROGRESS_AUTHED) { activity.updateProgress(Localization.get("sync.progress.downloading"), DataPullTask.DATA_PULL_TASK_ID); activity.updateProgressBarVisibility(false); } else if (progressCode == DataPullTask.PROGRESS_DOWNLOADING) { activity.updateProgress( Localization.get("sync.process.downloading.progress", new String[]{String.valueOf(update[1])}), Localization.get("sync.downloading.title"), DataPullTask.DATA_PULL_TASK_ID); } else if (progressCode == DataPullTask.PROGRESS_DOWNLOADING_COMPLETE) { activity.hideTaskCancelButton(); } else if (progressCode == DataPullTask.PROGRESS_PROCESSING) { activity.updateProgress( Localization.get("sync.progress", new String[]{String.valueOf(update[1]), String.valueOf(update[2])}), Localization.get("sync.processing.title"), DataPullTask.DATA_PULL_TASK_ID); activity.updateProgressBar(update[1], update[2], DataPullTask.DATA_PULL_TASK_ID); } else if (progressCode == DataPullTask.PROGRESS_RECOVERY_NEEDED) { activity.updateProgress(Localization.get("sync.recover.needed"), DataPullTask.DATA_PULL_TASK_ID); } else if (progressCode == DataPullTask.PROGRESS_RECOVERY_STARTED) { activity.updateProgress(Localization.get("sync.recover.started"), DataPullTask.DATA_PULL_TASK_ID); } else if (progressCode == DataPullTask.PROGRESS_SERVER_PROCESSING) { activity.updateProgress( Localization.get("sync.progress", new String[]{String.valueOf(update[1]), String.valueOf(update[2])}), Localization.get("sync.waiting.title"), DataPullTask.DATA_PULL_TASK_ID); activity.updateProgressBar(update[1], update[2], DataPullTask.DATA_PULL_TASK_ID); } } @Override public void handlePullTaskError() { updateUiAfterDataPullOrSend(Localization.get("sync.fail.unknown"), FAIL); } public void handleSyncNotAttempted(String message) { displayToast(message); } public void handleFormSendResult(String message, boolean success) { updateUiAfterDataPullOrSend(message, success); if (success) { // Since we know that we just had connectivity, now is a great time to try this CommCareApplication.instance().getSession().initHeartbeatLifecycle(); } } abstract void updateUiAfterDataPullOrSend(String message, boolean success); protected void displayToast(String message) { Toast.makeText(this, message, Toast.LENGTH_LONG).show(); } @Override public void startBlockingForTask(int id) { super.startBlockingForTask(id); if (isProcessAndSendTaskId(id)) { triggerSyncIconRefresh(SyncIconTrigger.ANIMATE_SEND_FORMS); } else if (id == DataPullTask.DATA_PULL_TASK_ID) { triggerSyncIconRefresh(SyncIconTrigger.ANIMATE_DATA_PULL); } } @Override public void stopBlockingForTask(int id) { super.stopBlockingForTask(id); if (isProcessAndSendTaskId(id) || id == DataPullTask.DATA_PULL_TASK_ID) { triggerSyncIconRefresh(SyncIconTrigger.NO_ANIMATION); } } private static boolean isProcessAndSendTaskId(int id) { return id == ProcessAndSendTask.SEND_PHASE_ID_NO_DIALOG || id == ProcessAndSendTask.PROCESSING_PHASE_ID_NO_DIALOG || id == ProcessAndSendTask.PROCESSING_PHASE_ID || id == ProcessAndSendTask.SEND_PHASE_ID; } private void triggerSyncIconRefresh(SyncIconTrigger trigger) { if (shouldShowSyncItemInActionBar()) { computeSyncState(trigger); rebuildOptionsMenu(); } } private void computeSyncState(SyncIconTrigger trigger) { lastIconTrigger = trigger; switch(trigger) { case NO_ANIMATION: if (SyncDetailCalculations.getNumUnsentForms() > 0) { syncStateForIcon = SyncIconState.FORMS_PENDING; } else { syncStateForIcon = SyncIconState.UP_TO_DATE; } break; case ANIMATE_DATA_PULL: syncStateForIcon = SyncIconState.PULLING_DATA; break; case ANIMATE_SEND_FORMS: syncStateForIcon = SyncIconState.SENDING_FORMS; break; } } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == MENU_SYNC) { sendFormsOrSync(true); return true; } return super.onOptionsItemSelected(item); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); addSyncItemToActionBar(menu); return true; } private void addSyncItemToActionBar(Menu menu) { if (shouldShowSyncItemInActionBar() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { currentSyncMenuItem = menu.add(MENU_GROUP_SYNC_ACTION, MENU_SYNC, MENU_SYNC, "Sync"); currentSyncMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); switch (syncStateForIcon) { case PULLING_DATA: addDataPullAnimation(currentSyncMenuItem); break; case SENDING_FORMS: addFormSendAnimation(currentSyncMenuItem); break; case FORMS_PENDING: currentSyncMenuItem.setIcon(R.drawable.ic_forms_pending_action_bar); break; case UP_TO_DATE: currentSyncMenuItem.setIcon(R.drawable.ic_sync_action_bar); break; } } } @Override public void rebuildOptionsMenu() { clearCurrentAnimation(currentSyncMenuItem); super.rebuildOptionsMenu(); } private void addDataPullAnimation(MenuItem menuItem) { addAnimationToMenuItem(menuItem, R.layout.data_pull_action_view, R.anim.slide_down_repeat); } private void addFormSendAnimation(MenuItem menuItem) { addAnimationToMenuItem(menuItem, R.layout.send_forms_action_view, R.anim.slide_up_repeat); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) private void addAnimationToMenuItem(MenuItem menuItem, @LayoutRes int layoutResource, @AnimRes int animationId) { LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE); ImageView iv = (ImageView)inflater.inflate(layoutResource, null); Animation animation = AnimationUtils.loadAnimation(this, animationId); iv.startAnimation(animation); menuItem.setActionView(iv); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) private void clearCurrentAnimation(MenuItem item) { if (item != null && item.getActionView() != null) { item.getActionView().clearAnimation(); item.setActionView(null); } } /** * If true, the action bar of this activity will show an icon or animation at all times * indicating the current sync state of the app (1 of either sending forms, pulling data, * has pending forms to send, or up-to-date) */ public abstract boolean shouldShowSyncItemInActionBar(); /** * If true, a progress bar will show beneath the action bar during form submission. In order * to successfully enable this for an activity, the layout file for that activity must also * contain the progress bar element that FormSubmissionProgressBarListener expects. */ public abstract boolean usesSubmissionProgressBar(); public void setUiLoadedListener(UiLoadedListener listener) { this.uiLoadedListener = listener; } public void removeUiLoadedListener() { this.uiLoadedListener = null; } @Override public CustomProgressDialog generateProgressDialog(int taskId) { String title, message; CustomProgressDialog dialog; switch (taskId) { case ProcessAndSendTask.SEND_PHASE_ID: title = Localization.get("sync.progress.submitting.title"); message = Localization.get("sync.progress.submitting"); dialog = CustomProgressDialog.newInstance(title, message, taskId); break; case ProcessAndSendTask.PROCESSING_PHASE_ID: title = Localization.get("form.entry.processing.title"); message = Localization.get("form.entry.processing"); dialog = CustomProgressDialog.newInstance(title, message, taskId); dialog.addProgressBar(); break; case DataPullTask.DATA_PULL_TASK_ID: title = Localization.get("sync.communicating.title"); message = Localization.get("sync.progress.purge"); dialog = CustomProgressDialog.newInstance(title, message, taskId); if (isSyncUserLaunched) { // allow users to cancel syncs that they launched dialog.addCancelButton(); } isSyncUserLaunched = false; break; default: return null; } return dialog; } private enum SyncIconState { UP_TO_DATE, PULLING_DATA, SENDING_FORMS, FORMS_PENDING } private enum SyncIconTrigger { ANIMATE_DATA_PULL, ANIMATE_SEND_FORMS, NO_ANIMATION } }