package org.commcare.activities;
import android.app.Activity;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.util.Pair;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import org.commcare.CommCareApplication;
import org.commcare.android.logging.ReportingUtils;
import org.commcare.android.nsd.MicroNode;
import org.commcare.android.nsd.NSDDiscoveryTools;
import org.commcare.android.nsd.NsdServiceListener;
import org.commcare.dalvik.BuildConfig;
import org.commcare.engine.resource.AppInstallStatus;
import org.commcare.engine.resource.ResourceInstallUtils;
import org.commcare.interfaces.CommCareActivityUIController;
import org.commcare.interfaces.WithUIController;
import org.commcare.logging.AndroidLogger;
import org.commcare.preferences.CommCarePreferences;
import org.commcare.preferences.DeveloperPreferences;
import org.commcare.tasks.InstallStagedUpdateTask;
import org.commcare.tasks.ResultAndError;
import org.commcare.tasks.TaskListener;
import org.commcare.tasks.TaskListenerRegistrationException;
import org.commcare.tasks.UpdateTask;
import org.commcare.utils.ConnectivityStatus;
import org.commcare.utils.ConsumerAppsUtil;
import org.commcare.utils.SessionUnavailableException;
import org.commcare.views.dialogs.CustomProgressDialog;
import org.commcare.views.dialogs.DialogChoiceItem;
import org.commcare.views.dialogs.PaneledChoiceDialog;
import org.commcare.views.notifications.NotificationMessage;
import org.commcare.views.notifications.NotificationMessageFactory;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.locale.Localization;
/**
* Allow user to manage app updating:
* - Check and download the latest update
* - Stop a downloading update
* - Apply a downloaded update
*
* @author Phillip Mates (pmates@dimagi.com)
*/
public class UpdateActivity extends CommCareActivity<UpdateActivity>
implements TaskListener<Integer, ResultAndError<AppInstallStatus>>, WithUIController, NsdServiceListener {
public static final String KEY_FROM_LATEST_BUILD_ACTIVITY = "from-test-latest-build-util";
// Options menu codes
public static final int MENU_UPDATE_TARGET_OPTIONS = Menu.FIRST;
public static final int MENU_UPDATE_FROM_CCZ = Menu.FIRST + 1;
public static final int MENU_UPDATE_FROM_HUB = Menu.FIRST + 2;
// Activity request codes
private static final int OFFLINE_UPDATE = 0;
private static final String TAG = UpdateActivity.class.getSimpleName();
private static final String TASK_CANCELLING_KEY = "update_task_cancelling";
private static final String IS_APPLYING_UPDATE_KEY = "applying_update_task_running";
private static final String IS_LOCAL_UPDATE = "is-local-update";
public static final String OFFLINE_UPDATE_REF = "offline-update-ref";
private static final int DIALOG_UPGRADE_INSTALL = 6;
private static final int DIALOG_CONSUMER_APP_UPGRADE = 7;
private boolean taskIsCancelling;
private boolean isApplyingUpdate;
private UpdateTask updateTask;
private UpdateUIController uiController;
private boolean proceedAutomatically;
private boolean isLocalUpdate;
private String offlineUpdateRef;
private MicroNode.AppManifest hubAppRecord;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
uiController.setupUI();
if (getIntent().getBooleanExtra(KEY_FROM_LATEST_BUILD_ACTIVITY, false)) {
proceedAutomatically = true;
} else if (CommCareApplication.instance().isConsumerApp()) {
proceedAutomatically = true;
isLocalUpdate = true;
}
loadSavedInstanceState(savedInstanceState);
boolean isRotation = savedInstanceState != null;
setupUpdateTask(isRotation);
}
private void loadSavedInstanceState(Bundle savedInstanceState) {
if (savedInstanceState != null) {
taskIsCancelling = savedInstanceState.getBoolean(TASK_CANCELLING_KEY, false);
isApplyingUpdate = savedInstanceState.getBoolean(IS_APPLYING_UPDATE_KEY, false);
isLocalUpdate = savedInstanceState.getBoolean(IS_LOCAL_UPDATE, false);
offlineUpdateRef = savedInstanceState.getString(OFFLINE_UPDATE_REF);
uiController.loadSavedUIState(savedInstanceState);
}
}
private void setupUpdateTask(boolean isRotation) {
updateTask = UpdateTask.getRunningInstance();
if (updateTask != null) {
try {
updateTask.registerTaskListener(this);
} catch (TaskListenerRegistrationException e) {
Log.e(TAG, "Attempting to register a TaskListener to an already " +
"registered task.");
uiController.errorUiState();
}
} else if (!isRotation && !taskIsCancelling
&& (ConnectivityStatus.isNetworkAvailable(this) || offlineUpdateRef != null)) {
startUpdateCheck();
}
}
@Override
protected void onResume() {
super.onResume();
NSDDiscoveryTools.registerForNsdServices(this, this);
if (!ConnectivityStatus.isNetworkAvailable(this) && offlineUpdateRef == null) {
uiController.noConnectivityUiState();
return;
}
setUiFromTask();
}
@Override
protected void onPause() {
super.onPause();
NSDDiscoveryTools.unregisterForNsdServices(this);
}
private void setUiFromTask() {
if (updateTask != null) {
if (taskIsCancelling) {
uiController.cancellingUiState();
} else {
setUiStateFromTaskStatus(updateTask.getStatus());
}
int currentProgress = updateTask.getProgress();
int maxProgress = updateTask.getMaxProgress();
uiController.updateProgressBar(currentProgress, maxProgress);
} else {
setPendingUpdate();
}
uiController.refreshView();
}
private void setUiStateFromTaskStatus(AsyncTask.Status taskStatus) {
switch (taskStatus) {
case RUNNING:
uiController.downloadingUiState();
break;
case PENDING:
break;
case FINISHED:
uiController.errorUiState();
break;
default:
uiController.errorUiState();
}
}
private void setPendingUpdate() {
if (!isApplyingUpdate && ResourceInstallUtils.isUpdateReadyToInstall()) {
uiController.unappliedUpdateAvailableUiState();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterTask();
}
private void unregisterTask() {
if (updateTask != null) {
try {
updateTask.unregisterTaskListener(this);
} catch (TaskListenerRegistrationException e) {
Log.e(TAG, "Attempting to unregister a not previously " +
"registered TaskListener.");
}
updateTask = null;
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(TASK_CANCELLING_KEY, taskIsCancelling);
outState.putBoolean(IS_APPLYING_UPDATE_KEY, isApplyingUpdate);
outState.putString(OFFLINE_UPDATE_REF, offlineUpdateRef);
outState.putBoolean(IS_LOCAL_UPDATE, isLocalUpdate);
uiController.saveCurrentUIState(outState);
}
@Override
public void handleTaskUpdate(Integer... vals) {
int progress = vals[0];
int max = vals[1];
uiController.updateProgressBar(progress, max);
String msg = Localization.get("updates.found",
new String[]{"" + progress, "" + max});
uiController.updateProgressText(msg);
}
@Override
public void handleTaskCompletion(ResultAndError<AppInstallStatus> result) {
if (CommCareApplication.instance().isConsumerApp()) {
dismissProgressDialog();
}
if (result.data == AppInstallStatus.UpdateStaged) {
uiController.unappliedUpdateAvailableUiState();
if (proceedAutomatically) {
launchUpdateInstallTask();
}
} else if (result.data == AppInstallStatus.UpToDate) {
uiController.upToDateUiState();
if (proceedAutomatically) {
finishWithResult(RefreshToLatestBuildActivity.ALREADY_UP_TO_DATE);
}
} else {
reportFailureToNotifications(result.errorMessage);
uiController.checkFailedUiState();
if (proceedAutomatically) {
finishWithResult(RefreshToLatestBuildActivity.UPDATE_ERROR);
}
}
unregisterTask();
uiController.refreshView();
}
private void reportFailureToNotifications(String errorMessage) {
NotificationMessage notificationMessage = null;
if (UpdateTask.isCombinedErrorMessage(errorMessage)) {
Pair<String, String> resourceAndMessage =
UpdateTask.splitCombinedErrorMessage(errorMessage);
notificationMessage =
NotificationMessageFactory.message(AppInstallStatus.InvalidResource,
new String[]{null, resourceAndMessage.first, resourceAndMessage.second});
} else if (!"".equals(errorMessage)) {
notificationMessage =
NotificationMessageFactory.message(AppInstallStatus.UpdateFailedGeneral,
new String[]{null, errorMessage, null});
}
if (notificationMessage != null) {
CommCareApplication.notificationManager()
.reportNotificationMessage(notificationMessage, true);
}
}
private void finishWithResult(String result) {
Intent i = new Intent();
setResult(RESULT_OK, i);
i.putExtra(RefreshToLatestBuildActivity.KEY_UPDATE_ATTEMPT_RESULT, result);
finish();
}
@Override
public void handleTaskCancellation() {
unregisterTask();
uiController.idleUiState();
}
protected void startUpdateCheck() {
try {
updateTask = UpdateTask.getNewInstance();
initUpdateTaskProgressDisplay();
if (isLocalUpdate) {
updateTask.setLocalAuthority();
}
updateTask.registerTaskListener(this);
} catch (IllegalStateException e) {
connectToRunningTask();
return;
} catch (TaskListenerRegistrationException e) {
enterErrorState("Attempting to register a TaskListener to an " +
"already registered task.");
return;
}
String profileRef;
if (hubAppRecord != null && offlineUpdateRef != null) {
updateTask.setLocalAuthority();
}
if (offlineUpdateRef != null) {
profileRef = offlineUpdateRef;
offlineUpdateRef = null;
} else {
profileRef = ResourceInstallUtils.getDefaultProfileRef();
}
uiController.downloadingUiState();
updateTask.executeParallel(profileRef);
}
/**
* Since updates in a consumer app do not use the normal UpdateActivity UI, use an
* alternative method of displaying the update check's progress in that case
*/
private void initUpdateTaskProgressDisplay() {
if (CommCareApplication.instance().isConsumerApp()) {
showProgressDialog(DIALOG_CONSUMER_APP_UPGRADE);
} else {
updateTask.startPinnedNotification(this);
}
}
private void connectToRunningTask() {
setupUpdateTask(false);
setUiFromTask();
}
private void enterErrorState(String errorMsg) {
Log.e(TAG, errorMsg);
uiController.errorUiState();
}
public void stopUpdateCheck() {
if (updateTask != null) {
updateTask.cancelWasUserTriggered();
updateTask.cancel(true);
taskIsCancelling = true;
uiController.cancellingUiState();
} else {
uiController.idleUiState();
}
if (proceedAutomatically) {
finishWithResult(RefreshToLatestBuildActivity.UPDATE_CANCELED);
}
}
/**
* Block the user with a dialog while the update is finalized.
*/
protected void launchUpdateInstallTask() {
InstallStagedUpdateTask<UpdateActivity> task =
new InstallStagedUpdateTask<UpdateActivity>(DIALOG_UPGRADE_INSTALL) {
@Override
protected void deliverResult(UpdateActivity receiver,
AppInstallStatus result) {
if (result == AppInstallStatus.Installed) {
reportAppUpdate();
receiver.logoutOnSuccessfulUpdate();
} else {
if (proceedAutomatically) {
finishWithResult(RefreshToLatestBuildActivity.UPDATE_ERROR);
return;
}
receiver.uiController.errorUiState();
}
receiver.isApplyingUpdate = false;
}
@Override
protected void deliverUpdate(UpdateActivity receiver,
int[]... update) {
}
@Override
protected void deliverError(UpdateActivity receiver,
Exception e) {
receiver.uiController.errorUiState();
receiver.isApplyingUpdate = false;
}
};
task.connect(this);
task.executeParallel();
isApplyingUpdate = true;
uiController.applyingUpdateUiState();
}
@Override
public CustomProgressDialog generateProgressDialog(int taskId) {
if (CommCareApplication.instance().isConsumerApp()) {
return ConsumerAppsUtil.getGenericConsumerAppsProgressDialog(taskId, false);
} else if (taskId != DIALOG_UPGRADE_INSTALL) {
Log.w(TAG, "taskId passed to generateProgressDialog does not match "
+ "any valid possibilities in CommCareSetupActivity");
return null;
} else {
return generateNormalUpdateInstallDialog(taskId);
}
}
private static CustomProgressDialog generateNormalUpdateInstallDialog(int taskId) {
String title = Localization.get("updates.installing.title");
String message = Localization.get("updates.installing.message");
CustomProgressDialog dialog =
CustomProgressDialog.newInstance(title, message, taskId);
dialog.setCancelable(false);
return dialog;
}
private static void reportAppUpdate() {
String updateLogMessage = "Update to app version " + ReportingUtils.getAppBuildNumber();
try {
String username = CommCareApplication.instance().getRecordForCurrentUser().getUsername();
updateLogMessage += " by user " + username;
} catch (SessionUnavailableException e) {
// Must be updating from the app manager, in which case we don't have a current user
}
Logger.log(AndroidLogger.TYPE_RESOURCES, updateLogMessage);
}
private void logoutOnSuccessfulUpdate() {
final String upgradeFinishedText =
Localization.get("updates.install.finished");
CommCareApplication.instance().expireUserSession();
if (proceedAutomatically) {
finishWithResult(RefreshToLatestBuildActivity.UPDATE_SUCCESS);
} else {
Toast.makeText(this, upgradeFinishedText, Toast.LENGTH_LONG).show();
setResult(RESULT_OK);
this.finish();
}
}
@Override
public String getActivityTitle() {
return "Update" + super.getActivityTitle();
}
@Override
public void initUIController() {
boolean fromAppManager = getIntent().getBooleanExtra(AppManagerActivity.KEY_LAUNCH_FROM_MANAGER, false);
if (CommCareApplication.instance().isConsumerApp()) {
uiController = new BlankUpdateUIController(this, fromAppManager);
} else {
uiController = new UpdateUIController(this, fromAppManager);
}
}
@Override
public CommCareActivityUIController getUIController() {
return this.uiController;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
menu.add(0, MENU_UPDATE_TARGET_OPTIONS, 0, Localization.get("menu.update.options"));
menu.add(0, MENU_UPDATE_FROM_CCZ, 1, Localization.get("menu.update.from.ccz"));
menu.add(0, MENU_UPDATE_FROM_HUB, 2, Localization.get("menu.update.from.hub"));
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.findItem(MENU_UPDATE_TARGET_OPTIONS).setVisible(
DeveloperPreferences.shouldShowUpdateOptionsSetting());
menu.findItem(MENU_UPDATE_FROM_CCZ).setVisible(BuildConfig.DEBUG ||
!getIntent().getBooleanExtra(AppManagerActivity.KEY_LAUNCH_FROM_MANAGER, false));
menu.findItem(MENU_UPDATE_FROM_HUB).setVisible(hubAppRecord != null);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_UPDATE_FROM_CCZ:
Intent i = new Intent(getApplicationContext(), InstallArchiveActivity.class);
i.putExtra(InstallArchiveActivity.FROM_UPDATE, true);
startActivityForResult(i, OFFLINE_UPDATE);
return true;
case MENU_UPDATE_FROM_HUB:
triggerLocalHubUpdate();
return true;
case MENU_UPDATE_TARGET_OPTIONS:
showUpdateTargetChoiceDialog();
}
return super.onOptionsItemSelected(item);
}
private void triggerLocalHubUpdate() {
offlineUpdateRef = hubAppRecord.getLocalUrl();
this.startUpdateCheck();
}
private void showUpdateTargetChoiceDialog() {
final PaneledChoiceDialog dialog =
new PaneledChoiceDialog(this, Localization.get("menu.update.options"));
DialogChoiceItem latestStarredChoice = new DialogChoiceItem(
Localization.get("update.option.starred"), -1, new View.OnClickListener() {
@Override
public void onClick(View v) {
CommCarePreferences.setUpdateTarget(CommCarePreferences.UPDATE_TARGET_STARRED);
dialog.dismiss();
}
});
DialogChoiceItem latestBuildChoice = new DialogChoiceItem(
Localization.get("update.option.build"), -1, new View.OnClickListener() {
@Override
public void onClick(View v) {
CommCarePreferences.setUpdateTarget(CommCarePreferences.UPDATE_TARGET_BUILD);
dialog.dismiss();
}
});
DialogChoiceItem latestSavedChoice = new DialogChoiceItem(
Localization.get("update.option.saved"), -1, new View.OnClickListener() {
@Override
public void onClick(View v) {
CommCarePreferences.setUpdateTarget(CommCarePreferences.UPDATE_TARGET_SAVED);
dialog.dismiss();
}
});
dialog.setChoiceItems(
new DialogChoiceItem[]{latestStarredChoice, latestBuildChoice, latestSavedChoice});
this.showAlertDialog(dialog);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
switch (requestCode) {
case OFFLINE_UPDATE:
if (resultCode == Activity.RESULT_OK) {
offlineUpdateRef = intent.getStringExtra(InstallArchiveActivity.ARCHIVE_JR_REFERENCE);
if (offlineUpdateRef != null) {
isLocalUpdate = true;
setupUpdateTask(false);
}
}
break;
}
}
private void notifyLocalUpdatePathAvailable(MicroNode.AppManifest hubAppRecord) {
this.hubAppRecord = hubAppRecord;
this.rebuildOptionsMenu();
}
@Override
public synchronized void onMicronodeDiscovery() {
boolean appsAvailable = false;
//If we aren't staged, don't go down this road
if (CommCareApplication.instance().getCurrentApp() == null) {
return;
}
String appId = CommCareApplication.instance().getCurrentApp().getUniqueId();
for (MicroNode node : NSDDiscoveryTools.getAvailableMicronodes()) {
final MicroNode.AppManifest appManifest = node.getManifestForAppId(appId);
if (appManifest != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
notifyLocalUpdatePathAvailable(appManifest);
}
});
}
}
}
}