Fix for managed-profile provisioning UI state not updating correctly.
Multiple fixes included to achieve this:
* Fixes to ProfileOwnerProvisioningActivity:
* Adjustments to ProfileOwnerProvisioningActivity so that
cancellation and error cases are handled distinctly.
* Rework of ProfileOwnerProvisioningService state-management and
error handling to be more robust:
* Switch from three variables (mDone, mCancelInFuture and
mLastErrorMessage) determining process state to one with more
explicit state definition, and hopefully clearer semantics.
* Ensure that already handled errors under in
RunnerTask.doInBackground() cause the provisioning process to
fail-fast.
* Ensure that other unhandled exceptions are caught and update
the provisioning status and UI appropriately.
Bug: 18584465
Change-Id: Ic56c74eb061c35e1a8ce133c4f9f5f001596d5e7
diff --git a/src/com/android/managedprovisioning/ProfileOwnerProvisioningActivity.java b/src/com/android/managedprovisioning/ProfileOwnerProvisioningActivity.java
index b163cde..dd3dcf5 100644
--- a/src/com/android/managedprovisioning/ProfileOwnerProvisioningActivity.java
+++ b/src/com/android/managedprovisioning/ProfileOwnerProvisioningActivity.java
@@ -182,27 +182,32 @@
ProvisionLogger.logd("Error reported: " + errorLogMessage);
error(R.string.managed_provisioning_error_text, errorLogMessage);
// Note that this will be reported as a canceled action
+ mCancelStatus = CANCELSTATUS_FINALIZING;
} else if (ProfileOwnerProvisioningService.ACTION_PROVISIONING_CANCELLED.equals(action)) {
if (mCancelStatus != CANCELSTATUS_CANCELLING) {
return;
}
mCancelProgressDialog.dismiss();
- ProfileOwnerProvisioningActivity.this.setResult(Activity.RESULT_CANCELED);
- stopService(new Intent(ProfileOwnerProvisioningActivity.this,
- ProfileOwnerProvisioningService.class));
- ProfileOwnerProvisioningActivity.this.finish();
+ onProvisioningAborted();
}
}
+ private void onProvisioningAborted() {
+ ProfileOwnerProvisioningActivity.this.setResult(Activity.RESULT_CANCELED);
+ stopService(new Intent(ProfileOwnerProvisioningActivity.this,
+ ProfileOwnerProvisioningService.class));
+ ProfileOwnerProvisioningActivity.this.finish();
+ }
+
@Override
public void onBackPressed() {
- if (mCancelStatus == CANCELSTATUS_PROVISIONING) {
+ if (mCancelStatus != CANCELSTATUS_PROVISIONING) {
+ mCancelStatus = CANCELSTATUS_CONFIRMING;
showCancelProvisioningDialog();
}
}
private void showCancelProvisioningDialog() {
- mCancelStatus = CANCELSTATUS_CONFIRMING;
AlertDialog alertDialog = new AlertDialog.Builder(this)
.setCancelable(false)
.setTitle(R.string.profile_owner_cancel_title)
@@ -247,7 +252,7 @@
.setPositiveButton(R.string.device_owner_error_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,int id) {
- confirmCancel();
+ onProvisioningAborted();
}
})
.show()
@@ -255,6 +260,10 @@
}
private void confirmCancel() {
+ if (mCancelStatus != CANCELSTATUS_CONFIRMING) {
+ // Can only cancel if provisioning hasn't finished at this point.
+ return;
+ }
mCancelStatus = CANCELSTATUS_CANCELLING;
Intent intent = new Intent(ProfileOwnerProvisioningActivity.this,
ProfileOwnerProvisioningService.class);
diff --git a/src/com/android/managedprovisioning/ProfileOwnerProvisioningService.java b/src/com/android/managedprovisioning/ProfileOwnerProvisioningService.java
index 36e1fd9..e7d22f2 100644
--- a/src/com/android/managedprovisioning/ProfileOwnerProvisioningService.java
+++ b/src/com/android/managedprovisioning/ProfileOwnerProvisioningService.java
@@ -79,6 +79,21 @@
"com.android.managedprovisioning.cancelled";
public static final String EXTRA_LOG_MESSAGE_KEY = "ProvisioingErrorLogMessage";
+ // Status flags for the provisioning process.
+ /** Provisioning not started. */
+ private static final int STATUS_UNKNOWN = 0;
+ /** Provisioning started, no errors or cancellation requested received. */
+ private static final int STATUS_STARTED = 1;
+ /** Provisioning in progress, but user has requested cancellation. */
+ private static final int STATUS_CANCELLING = 2;
+ // Final possible states for the provisioning process.
+ /** Provisioning completed successfully. */
+ private static final int STATUS_DONE = 3;
+ /** Provisioning failed and cleanup complete. */
+ private static final int STATUS_ERROR = 4;
+ /** Provisioning cancelled and cleanup complete. */
+ private static final int STATUS_CANCELLED = 5;
+
private String mMdmPackageName;
private ComponentName mActiveAdminComponentName;
@@ -92,20 +107,40 @@
private AccountManager mAccountManager;
private UserManager mUserManager;
- private int mStartIdProvisioning;
private AsyncTask<Intent, Void, Void> runnerTask;
// MessageId of the last error message.
private String mLastErrorMessage = null;
- private boolean mDone = false;
- private boolean mCancelInFuture = false;
+ // Current status of the provisioning process.
+ private int mProvisioningStatus = STATUS_UNKNOWN;
private class RunnerTask extends AsyncTask<Intent, Void, Void> {
@Override
protected Void doInBackground(Intent ... intents) {
- initialize(intents[0]);
- startManagedProfileProvisioning();
+ // Atomically move to STATUS_STARTED at most once.
+ synchronized (ProfileOwnerProvisioningService.this) {
+ if (mProvisioningStatus == STATUS_UNKNOWN) {
+ mProvisioningStatus = STATUS_STARTED;
+ } else {
+ // Process already started, don't start again.
+ return null;
+ }
+ }
+
+ try {
+ initialize(intents[0]);
+ startManagedProfileProvisioning();
+ } catch (ProvisioningException e) {
+ // Handle internal errors.
+ error(e.getMessage(), e);
+ finish();
+ } catch (Exception e) {
+ // General catch-all to ensure process cleans up in all cases.
+ error("Failed to initialize managed profile, aborting.", e);
+ finish();
+ }
+
return null;
}
}
@@ -144,27 +179,59 @@
}
private void reportStatus() {
- if (mLastErrorMessage != null) {
- sendError();
- }
synchronized (this) {
- if (mDone) {
- finishProvisioning();
+ switch (mProvisioningStatus) {
+ case STATUS_DONE:
+ notifyActivityOfSuccess();
+ break;
+ case STATUS_CANCELLED:
+ notifyActivityCancelled();
+ break;
+ case STATUS_ERROR:
+ notifyActivityError();
+ break;
+ case STATUS_UNKNOWN:
+ case STATUS_STARTED:
+ case STATUS_CANCELLING:
+ // Don't notify UI of status when just-started/in-progress.
+ break;
}
}
}
private void cancelProvisioning() {
synchronized (this) {
- if (!mDone) {
- mCancelInFuture = true;
- return;
+ switch (mProvisioningStatus) {
+ case STATUS_DONE:
+ // Process completed, we should honor user request to cancel
+ // though.
+ mProvisioningStatus = STATUS_CANCELLING;
+ cleanupUserProfile();
+ mProvisioningStatus = STATUS_CANCELLED;
+ reportStatus();
+ break;
+ case STATUS_UNKNOWN:
+ // Process hasn't started, move straight to cancelled state.
+ mProvisioningStatus = STATUS_CANCELLED;
+ reportStatus();
+ break;
+ case STATUS_STARTED:
+ // Process is mid-flow, flag up that the user has requested
+ // cancellation.
+ mProvisioningStatus = STATUS_CANCELLING;
+ break;
+ case STATUS_CANCELLING:
+ // Cancellation already being processed.
+ break;
+ case STATUS_CANCELLED:
+ case STATUS_ERROR:
+ // Process already completed, nothing left to cancel.
+ break;
}
- cleanup();
}
}
- private void initialize(Intent intent) {
+ private void initialize(Intent intent) throws ProvisioningException {
mMdmPackageName = intent.getStringExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME);
mAccountToMigrate = (Account) intent.getParcelableExtra(
EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE);
@@ -182,30 +249,30 @@
/**
* Find the Device admin receiver component from the manifest.
*/
- private ComponentName getAdminReceiverComponent(String packageName) {
- ComponentName adminReceiverComponent = null;
-
+ private ComponentName getAdminReceiverComponent(String packageName)
+ throws ProvisioningException {
try {
PackageInfo pi = getPackageManager().getPackageInfo(packageName,
PackageManager.GET_RECEIVERS);
for (ActivityInfo ai : pi.receivers) {
if (!TextUtils.isEmpty(ai.permission) &&
ai.permission.equals(BIND_DEVICE_ADMIN)) {
- adminReceiverComponent = new ComponentName(packageName, ai.name);
-
+ return new ComponentName(packageName, ai.name);
}
}
+
+ throw raiseError("Didn't find admin receiver component with BIND_DEVICE_ADMIN "
+ + "permissions");
} catch (NameNotFoundException e) {
- error("Error: The provided mobile device management package does not define a device"
- + "admin receiver component in its manifest.");
+ throw raiseError("Error: The provided mobile device management package does not define "
+ + "a device admin receiver component in its manifest.");
}
- return adminReceiverComponent;
}
/**
* This is the core method of this class. It goes through every provisioning step.
*/
- private void startManagedProfileProvisioning() {
+ private void startManagedProfileProvisioning() throws ProvisioningException {
ProvisionLogger.logd("Starting managed profile provisioning");
@@ -231,14 +298,26 @@
@Override
public void onSuccess() {
- disableBluetoothSharingTask.run();
- disableInstallShortcutListenersTask.run();
- setUpProfileAndFinish();
+ // Need to explicitly handle exceptions here, as
+ // onError() is not invoked for failures in
+ // onSuccess().
+ try {
+ disableBluetoothSharingTask.run();
+ disableInstallShortcutListenersTask.run();
+ setUpProfile();
+ } catch (ProvisioningException e) {
+ error(e.getMessage(), e);
+ } catch (Exception e) {
+ error("Provisioning failed", e);
+ }
+ finish();
}
@Override
public void onError() {
- error("Delete non required apps task failed.");
+ // Raise an error with a tracing exception attached.
+ error("Delete non required apps task failed.", new Exception());
+ finish();
}
});
@@ -250,7 +329,7 @@
* Called when the new profile is ready for provisioning (the profile is created and all the
* apps not needed have been deleted).
*/
- private void setUpProfileAndFinish() {
+ private void setUpProfile() throws ProvisioningException {
installMdmOnManagedProfile();
setMdmAsActiveAdmin();
setMdmAsManagedProfileOwner();
@@ -258,19 +337,58 @@
getPackageManager(), getUserId(), mManagedProfileUserInfo.id);
if (!startManagedProfile(mManagedProfileUserInfo.id)) {
- error("Could not start user in background");
- return;
+ throw raiseError("Could not start user in background");
}
copyAccount(mAccountToMigrate);
+ }
+
+ /**
+ * Notify the calling activity of our final status, perform any cleanup if
+ * the process didn't succeed.
+ */
+ private void finish() {
+ ProvisionLogger.logi("Finishing provisioing process, status: "
+ + mProvisioningStatus);
+ // Reached the end of the provisioning process, take appropriate action
+ // based on current mProvisioningStatus.
synchronized (this) {
- mDone = true;
- if (mCancelInFuture) {
- cleanup();
- } else {
- // Notify activity of success.
- finishProvisioning();
+ switch (mProvisioningStatus) {
+ case STATUS_STARTED:
+ // Provisioning process completed normally.
+ notifyMdmAndCleanup();
+ mProvisioningStatus = STATUS_DONE;
+ break;
+ case STATUS_UNKNOWN:
+ // No idea how we could end up in finish() in this state,
+ // but for safety treat it as an error and fall-through to
+ // STATUS_ERROR.
+ mLastErrorMessage = "finish() invoked in STATUS_UNKNOWN";
+ mProvisioningStatus = STATUS_ERROR;
+ break;
+ case STATUS_ERROR:
+ // Process errored out, cleanup partially created managed
+ // profile.
+ cleanupUserProfile();
+ break;
+ case STATUS_CANCELLING:
+ // User requested cancellation during processing, remove
+ // the successfully created profile.
+ cleanupUserProfile();
+ mProvisioningStatus = STATUS_CANCELLED;
+ break;
+ case STATUS_CANCELLED:
+ case STATUS_DONE:
+ // Shouldn't be possible to already be in this state?!?
+ ProvisionLogger.logw("finish() invoked multiple times?");
+ break;
}
}
+
+ ProvisionLogger.logi("Finished provisioing process, final status: "
+ + mProvisioningStatus);
+
+ // Notify UI activity of final status reached.
+ reportStatus();
}
/**
@@ -296,11 +414,6 @@
.sendBroadcast(successIntent);
}
- private void finishProvisioning() {
- notifyMdmAndCleanup();
- notifyActivityOfSuccess();
- }
-
/**
* Notify the mdm that provisioning has completed. When the mdm has received the intent, stop
* the service and notify the {@link ProfileOwnerProvisioningActivity} so that it can finish
@@ -409,7 +522,7 @@
}
}
- private void createProfile(String profileName) {
+ private void createProfile(String profileName) throws ProvisioningException {
ProvisionLogger.logd("Creating managed profile with name " + profileName);
@@ -419,14 +532,14 @@
if (mManagedProfileUserInfo == null) {
if (UserManager.getMaxSupportedUsers() == mUserManager.getUserCount()) {
- error("Profile creation failed, maximum number of users reached.");
+ throw raiseError("Profile creation failed, maximum number of users reached.");
} else {
- error("Couldn't create profile. Reason unknown.");
+ throw raiseError("Couldn't create profile. Reason unknown.");
}
}
}
- private void installMdmOnManagedProfile() {
+ private void installMdmOnManagedProfile() throws ProvisioningException {
ProvisionLogger.logd("Installing mobile device management app " + mMdmPackageName +
" on managed profile");
@@ -438,14 +551,14 @@
return;
case PackageManager.INSTALL_FAILED_USER_RESTRICTED:
// Should not happen because we're not installing a restricted user
- error("Could not install mobile device management app on managed "
+ throw raiseError("Could not install mobile device management app on managed "
+ "profile because the user is restricted");
case PackageManager.INSTALL_FAILED_INVALID_URI:
// Should not happen because we already checked
- error("Could not install mobile device management app on managed "
+ throw raiseError("Could not install mobile device management app on managed "
+ "profile because the package could not be found");
default:
- error("Could not install mobile device management app on managed "
+ throw raiseError("Could not install mobile device management app on managed "
+ "profile. Unknown status: " + status);
}
} catch (RemoteException neverThrown) {
@@ -454,7 +567,7 @@
}
}
- private void setMdmAsManagedProfileOwner() {
+ private void setMdmAsManagedProfileOwner() throws ProvisioningException {
ProvisionLogger.logd("Setting package " + mMdmPackageName + " as managed profile owner.");
DevicePolicyManager dpm =
@@ -462,7 +575,7 @@
if (!dpm.setProfileOwner(mActiveAdminComponentName, mMdmPackageName,
mManagedProfileUserInfo.id)) {
ProvisionLogger.logw("Could not set profile owner.");
- error("Could not set profile owner.");
+ throw raiseError("Could not set profile owner.");
}
}
@@ -475,33 +588,71 @@
mManagedProfileUserInfo.id);
}
- private void error(String dialogMessage) {
- mLastErrorMessage = dialogMessage;
- sendError();
+ private ProvisioningException raiseError(String message) throws ProvisioningException {
+ throw new ProvisioningException(message);
}
- private void sendError() {
+ /**
+ * Record the fact that an error occurred, change mProvisioningStatus to
+ * reflect the fact the provisioning process failed
+ */
+ private void error(String dialogMessage, Exception e) {
+ synchronized (this) {
+ // Only case where an error condition should be notified is if we
+ // are in the normal flow for provisioning. If the process has been
+ // cancelled or already completed, then the fact there is an error
+ // is almost irrelevant.
+ if (mProvisioningStatus == STATUS_STARTED) {
+ mProvisioningStatus = STATUS_ERROR;
+ mLastErrorMessage = dialogMessage;
+
+ ProvisionLogger.logw(
+ "Error occured during provisioning process: "
+ + dialogMessage,
+ e);
+ } else {
+ ProvisionLogger.logw(
+ "Unexpected error occured in status ["
+ + mProvisioningStatus + "]: " + dialogMessage,
+ e);
+ }
+ }
+ }
+
+ private void notifyActivityError() {
Intent intent = new Intent(ACTION_PROVISIONING_ERROR);
intent.putExtra(EXTRA_LOG_MESSAGE_KEY, mLastErrorMessage);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
+ private void notifyActivityCancelled() {
+ Intent cancelIntent = new Intent(ACTION_PROVISIONING_CANCELLED);
+ LocalBroadcastManager.getInstance(this).sendBroadcast(cancelIntent);
+ }
+
/**
- * Performs cleanup of the device on failure.
+ * Performs cleanup of any created user-profile on failure/cancellation.
*/
- private void cleanup() {
- // The only cleanup we need to do is remove the profile we created.
+ private void cleanupUserProfile() {
if (mManagedProfileUserInfo != null) {
ProvisionLogger.logd("Removing managed profile");
mUserManager.removeUser(mManagedProfileUserInfo.id);
}
- Intent cancelIntent = new Intent(ACTION_PROVISIONING_CANCELLED);
- LocalBroadcastManager.getInstance(ProfileOwnerProvisioningService.this)
- .sendBroadcast(cancelIntent);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
+
+ /**
+ * Internal exception to allow provisioning process to terminal quickly and
+ * cleanly on first error, rather than continuing to process despite errors
+ * occurring.
+ */
+ private static class ProvisioningException extends Exception {
+ public ProvisioningException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
}