Multi-user profile perf-tests

Tests that time how long it takes to
create/start/install-an-app/launch-an-app in a secondary profile.

Also, in the test, adds newly created users to the remove list, so that
even if the test crashes, the user will get removed by test cleanup.

Test: atest multiuser.UserLifecycleTests
Bug: 129857107
Change-Id: I13f7cb1628bf4a97771cb1b143ab524d7d0a2073
diff --git a/apct-tests/perftests/multiuser/AndroidManifest.xml b/apct-tests/perftests/multiuser/AndroidManifest.xml
index e96771c..b2a9524 100644
--- a/apct-tests/perftests/multiuser/AndroidManifest.xml
+++ b/apct-tests/perftests/multiuser/AndroidManifest.xml
@@ -17,7 +17,9 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.android.perftests.multiuser">
 
+    <uses-permission android:name="android.permission.CONTROL_KEYGUARD" />
     <uses-permission android:name="android.permission.MANAGE_USERS" />
+    <uses-permission android:name="android.permission.INSTALL_PACKAGES" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
     <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
 
diff --git a/apct-tests/perftests/multiuser/AndroidTest.xml b/apct-tests/perftests/multiuser/AndroidTest.xml
index 512dbc9..d8e3f01 100644
--- a/apct-tests/perftests/multiuser/AndroidTest.xml
+++ b/apct-tests/perftests/multiuser/AndroidTest.xml
@@ -19,6 +19,7 @@
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="MultiUserPerfTests.apk" />
+        <option name="test-file-name" value="PerftestMultiuserDummyApp.apk" />
     </target_preparer>
 
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
diff --git a/apct-tests/perftests/multiuser/apps/Android.mk b/apct-tests/perftests/multiuser/apps/Android.mk
new file mode 100644
index 0000000..e01225b
--- /dev/null
+++ b/apct-tests/perftests/multiuser/apps/Android.mk
@@ -0,0 +1,20 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# Build the test APKs using their own makefiles
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/apct-tests/perftests/multiuser/apps/dummyapp/Android.mk b/apct-tests/perftests/multiuser/apps/dummyapp/Android.mk
new file mode 100644
index 0000000..31e799e
--- /dev/null
+++ b/apct-tests/perftests/multiuser/apps/dummyapp/Android.mk
@@ -0,0 +1,26 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SDK_VERSION := current
+LOCAL_MIN_SDK_VERSION := 19
+
+LOCAL_PACKAGE_NAME := PerftestMultiuserDummyApp
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_PACKAGE)
\ No newline at end of file
diff --git a/apct-tests/perftests/multiuser/apps/dummyapp/AndroidManifest.xml b/apct-tests/perftests/multiuser/apps/dummyapp/AndroidManifest.xml
new file mode 100644
index 0000000..d4d37f97
--- /dev/null
+++ b/apct-tests/perftests/multiuser/apps/dummyapp/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="perftests.multiuser.apps.dummyapp" >
+
+    <application android:label="Perftest Multiuser Dummy App">
+
+        <activity
+            android:name=".DummyForegroundActivity"
+            android:label="Perftest Multiuser Dummy App"
+            android:exported="true"
+            android:launchMode="singleTop" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/apct-tests/perftests/multiuser/apps/dummyapp/src/com/android/multiuser/test/DummyForegroundActivity.java b/apct-tests/perftests/multiuser/apps/dummyapp/src/com/android/multiuser/test/DummyForegroundActivity.java
new file mode 100644
index 0000000..497d242
--- /dev/null
+++ b/apct-tests/perftests/multiuser/apps/dummyapp/src/com/android/multiuser/test/DummyForegroundActivity.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package perftests.multiuser.apps.dummyapp;
+
+import android.app.Activity;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.SystemClock;
+
+/** An activity. */
+public class DummyForegroundActivity extends Activity {
+    private static final String TAG = DummyForegroundActivity.class.getSimpleName();
+
+    public static final int TOP_SLEEP_TIME_MS = 2_000;
+
+    @Override
+    public void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+        doSleepWhileTop(TOP_SLEEP_TIME_MS);
+    }
+
+    /** Does nothing, but asynchronously. */
+    private void doSleepWhileTop(int sleepTime) {
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... params) {
+                SystemClock.sleep(sleepTime);
+                return null;
+            }
+
+            @Override
+            protected void onPostExecute(Void nothing) {
+                finish();
+            }
+        }.execute();
+    }
+}
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
index 2807940..6b09a9f 100644
--- a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
+++ b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java
@@ -16,18 +16,30 @@
 package android.multiuser;
 
 import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.app.AppGlobals;
 import android.app.IActivityManager;
 import android.app.IStopUserCallback;
 import android.app.UserSwitchObserver;
+import android.app.WaitResult;
 import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.IIntentReceiver;
+import android.content.IIntentSender;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.content.pm.IPackageInstaller;
+import android.content.pm.PackageManager;
 import android.content.pm.UserInfo;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.IProgressListener;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Log;
+import android.view.WindowManagerGlobal;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.LargeTest;
@@ -66,8 +78,10 @@
 public class UserLifecycleTests {
     private static final String TAG = UserLifecycleTests.class.getSimpleName();
 
-    private final int TIMEOUT_IN_SECOND = 30;
-    private final int CHECK_USER_REMOVED_INTERVAL_MS = 200;
+    private static final int TIMEOUT_IN_SECOND = 30;
+    private static final int CHECK_USER_REMOVED_INTERVAL_MS = 200;
+
+    private static final String DUMMY_PACKAGE_NAME = "perftests.multiuser.apps.dummyapp";
 
     private UserManager mUm;
     private ActivityManager mAm;
@@ -101,15 +115,16 @@
     @Test
     public void createAndStartUser() throws Exception {
         while (mRunner.keepRunning()) {
-            final UserInfo userInfo = mUm.createUser("TestUser", 0);
+            final int userId = createUser();
 
             final CountDownLatch latch = new CountDownLatch(1);
-            registerBroadcastReceiver(Intent.ACTION_USER_STARTED, latch, userInfo.id);
-            mIam.startUserInBackground(userInfo.id);
+            registerBroadcastReceiver(Intent.ACTION_USER_STARTED, latch, userId);
+            // Don't use this.startUserInBackground() since only waiting until ACTION_USER_STARTED.
+            mIam.startUserInBackground(userId);
             latch.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS);
 
             mRunner.pauseTiming();
-            removeUser(userInfo.id);
+            removeUser(userId);
             mRunner.resumeTiming();
         }
     }
@@ -119,14 +134,14 @@
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
             final int startUser = mAm.getCurrentUser();
-            final UserInfo userInfo = mUm.createUser("TestUser", 0);
+            final int userId = createUser();
             mRunner.resumeTiming();
 
-            switchUser(userInfo.id);
+            switchUser(userId);
 
             mRunner.pauseTiming();
             switchUser(startUser);
-            removeUser(userInfo.id);
+            removeUser(userId);
             mRunner.resumeTiming();
         }
     }
@@ -176,17 +191,17 @@
     public void stopUser() throws Exception {
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
-            final UserInfo userInfo = mUm.createUser("TestUser", 0);
+            final int userId = createUser();
             final CountDownLatch latch = new CountDownLatch(1);
-            registerBroadcastReceiver(Intent.ACTION_USER_STARTED, latch, userInfo.id);
-            mIam.startUserInBackground(userInfo.id);
+            registerBroadcastReceiver(Intent.ACTION_USER_STARTED, latch, userId);
+            mIam.startUserInBackground(userId);
             latch.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS);
             mRunner.resumeTiming();
 
-            stopUser(userInfo.id, false);
+            stopUser(userId, false);
 
             mRunner.pauseTiming();
-            removeUser(userInfo.id);
+            removeUser(userId);
             mRunner.resumeTiming();
         }
     }
@@ -196,17 +211,17 @@
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
             final int startUser = mAm.getCurrentUser();
-            final UserInfo userInfo = mUm.createUser("TestUser", 0);
+            final int userId = createUser();
             final CountDownLatch latch = new CountDownLatch(1);
-            registerUserSwitchObserver(null, latch, userInfo.id);
+            registerUserSwitchObserver(null, latch, userId);
             mRunner.resumeTiming();
 
-            mAm.switchUser(userInfo.id);
+            mAm.switchUser(userId);
             latch.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS);
 
             mRunner.pauseTiming();
             switchUser(startUser);
-            removeUser(userInfo.id);
+            removeUser(userId);
             mRunner.resumeTiming();
         }
     }
@@ -216,15 +231,14 @@
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
             final int startUser = mAm.getCurrentUser();
-            final UserInfo userInfo = mUm.createUser("TestUser",
-                    UserInfo.FLAG_EPHEMERAL | UserInfo.FLAG_DEMO);
-            switchUser(userInfo.id);
+            final int userId = createUser(UserInfo.FLAG_EPHEMERAL | UserInfo.FLAG_DEMO);
+            switchUser(userId);
             final CountDownLatch latch = new CountDownLatch(1);
             InstrumentationRegistry.getContext().registerReceiver(new BroadcastReceiver() {
                 @Override
                 public void onReceive(Context context, Intent intent) {
                     if (Intent.ACTION_USER_STOPPED.equals(intent.getAction()) && intent.getIntExtra(
-                            Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL) == userInfo.id) {
+                            Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL) == userId) {
                         latch.countDown();
                     }
                 }
@@ -238,79 +252,173 @@
 
             mRunner.pauseTiming();
             switchLatch.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS);
-            removeUser(userInfo.id);
+            removeUser(userId);
             mRunner.resumeTiming();
         }
     }
 
+    /** Tests creating a new profile. */
+    @Test
+    public void managedProfileCreate() throws Exception {
+        while (mRunner.keepRunning()) {
+            final int userId = createManagedProfile();
+
+            mRunner.pauseTiming();
+            attestTrue("Failed creating profile " + userId, mUm.isManagedProfile(userId));
+            removeUser(userId);
+            mRunner.resumeTiming();
+        }
+    }
+
+    /** Tests starting (unlocking) a newly-created profile. */
     @Test
     public void managedProfileUnlock() throws Exception {
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
-            final UserInfo userInfo = mUm.createProfileForUser("TestUser",
-                    UserInfo.FLAG_MANAGED_PROFILE, mAm.getCurrentUser());
-            final CountDownLatch latch = new CountDownLatch(1);
-            registerBroadcastReceiver(Intent.ACTION_USER_UNLOCKED, latch, userInfo.id);
+            final int userId = createManagedProfile();
             mRunner.resumeTiming();
 
-            mIam.startUserInBackground(userInfo.id);
-            latch.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS);
+            startUserInBackground(userId);
 
             mRunner.pauseTiming();
-            removeUser(userInfo.id);
+            removeUser(userId);
             mRunner.resumeTiming();
         }
     }
 
-    /** Tests starting an already-created, but no-longer-running, profile. */
+    /** Tests starting (unlocking) an already-created, but no-longer-running, profile. */
     @Test
     public void managedProfileUnlock_stopped() throws Exception {
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
-            final UserInfo userInfo = mUm.createProfileForUser("TestUser",
-                    UserInfo.FLAG_MANAGED_PROFILE, mAm.getCurrentUser());
+            final int userId = createManagedProfile();
             // Start the profile initially, then stop it. Similar to setQuietModeEnabled.
-            final CountDownLatch latch1 = new CountDownLatch(1);
-            registerBroadcastReceiver(Intent.ACTION_USER_UNLOCKED, latch1, userInfo.id);
-            mIam.startUserInBackground(userInfo.id);
-            latch1.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS);
-            stopUser(userInfo.id, true);
-
-            // Now we restart the profile.
-            final CountDownLatch latch2 = new CountDownLatch(1);
-            registerBroadcastReceiver(Intent.ACTION_USER_UNLOCKED, latch2, userInfo.id);
+            startUserInBackground(userId);
+            stopUser(userId, true);
             mRunner.resumeTiming();
 
-            mIam.startUserInBackground(userInfo.id);
-            latch2.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS);
+            startUserInBackground(userId);
 
             mRunner.pauseTiming();
-            removeUser(userInfo.id);
+            removeUser(userId);
             mRunner.resumeTiming();
         }
     }
 
+    /**
+     * Tests starting (unlocking) and launching an already-installed app in a newly-created profile.
+     */
+    @Test
+    public void managedProfileUnlockAndLaunchApp() throws Exception {
+        while (mRunner.keepRunning()) {
+            mRunner.pauseTiming();
+            final int userId = createManagedProfile();
+            WindowManagerGlobal.getWindowManagerService().dismissKeyguard(null, null);
+            installPreexistingApp(userId, DUMMY_PACKAGE_NAME);
+            mRunner.resumeTiming();
 
+            startUserInBackground(userId);
+            startApp(userId, DUMMY_PACKAGE_NAME);
+
+            mRunner.pauseTiming();
+            removeUser(userId);
+            mRunner.resumeTiming();
+        }
+    }
+
+    /** Tests installing a pre-existing app in a newly-created profile. */
+    @Test
+    public void managedProfileInstall() throws Exception {
+        while (mRunner.keepRunning()) {
+            mRunner.pauseTiming();
+            final int userId = createManagedProfile();
+            mRunner.resumeTiming();
+
+            installPreexistingApp(userId, DUMMY_PACKAGE_NAME);
+
+            mRunner.pauseTiming();
+            removeUser(userId);
+            mRunner.resumeTiming();
+        }
+    }
+
+    /**
+     * Tests creating a new profile, starting (unlocking) it, installing an app,
+     * and launching that app in it.
+     */
+    @Test
+    public void managedProfileCreateUnlockInstallAndLaunchApp() throws Exception {
+        final String packageName = "perftests.multiuser.apps.dummyapp";
+        while (mRunner.keepRunning()) {
+            mRunner.pauseTiming();
+            WindowManagerGlobal.getWindowManagerService().dismissKeyguard(null, null);
+            mRunner.resumeTiming();
+
+            final int userId = createManagedProfile();
+            startUserInBackground(userId);
+            installPreexistingApp(userId, DUMMY_PACKAGE_NAME);
+            startApp(userId, DUMMY_PACKAGE_NAME);
+
+            mRunner.pauseTiming();
+            removeUser(userId);
+            mRunner.resumeTiming();
+        }
+    }
+
+    /** Tests stopping a profile. */
     @Test
     public void managedProfileStopped() throws Exception {
         while (mRunner.keepRunning()) {
             mRunner.pauseTiming();
-            final UserInfo userInfo = mUm.createProfileForUser("TestUser",
-                    UserInfo.FLAG_MANAGED_PROFILE, mAm.getCurrentUser());
-            final CountDownLatch latch = new CountDownLatch(1);
-            registerBroadcastReceiver(Intent.ACTION_USER_UNLOCKED, latch, userInfo.id);
-            mIam.startUserInBackground(userInfo.id);
-            latch.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS);
+            final int userId = createManagedProfile();
+            startUserInBackground(userId);
             mRunner.resumeTiming();
 
-            stopUser(userInfo.id, true);
+            stopUser(userId, true);
 
             mRunner.pauseTiming();
-            removeUser(userInfo.id);
+            removeUser(userId);
             mRunner.resumeTiming();
         }
     }
 
+    /** Creates a new user, returning its userId. */
+    private int createUser() {
+        return createUser(0);
+    }
+
+    /** Creates a new user with the given flags, returning its userId. */
+    private int createUser(int flags) {
+        int userId = mUm.createUser("TestUser", flags).id;
+        mUsersToRemove.add(userId);
+        return userId;
+    }
+
+    /** Creates a managed (work) profile under the current user, returning its userId. */
+    private int createManagedProfile() {
+        final UserInfo userInfo = mUm.createProfileForUser("TestProfile",
+                UserInfo.FLAG_MANAGED_PROFILE, mAm.getCurrentUser());
+        mUsersToRemove.add(userInfo.id);
+        return userInfo.id;
+    }
+
+    /**
+     * Start user in background and wait for it to unlock (equivalent to ACTION_USER_UNLOCKED).
+     * To start in foreground instead, see {@link #switchUser(int)}.
+     * This should always be used for profiles since profiles cannot be started in foreground.
+     */
+    private void startUserInBackground(int userId) {
+        final ProgressWaiter waiter = new ProgressWaiter();
+        try {
+            mIam.startUserInBackgroundWithListener(userId, waiter);
+            boolean success = waiter.waitForFinish(TIMEOUT_IN_SECOND);
+            attestTrue("Failed to start user " + userId + " in background.", success);
+        } catch (RemoteException e) {
+            Log.e(TAG, "startUserInBackground failed", e);
+        }
+    }
+
+    /** Starts the given user in the foreground. */
     private void switchUser(int userId) throws Exception {
         final CountDownLatch latch = new CountDownLatch(1);
         registerUserSwitchObserver(latch, null, userId);
@@ -343,7 +451,7 @@
     private int initializeNewUserAndSwitchBack(boolean stopNewUser) throws Exception {
         final int origUser = mAm.getCurrentUser();
         // First, create and switch to testUser, waiting for its ACTION_USER_UNLOCKED
-        final int testUser = mUm.createUser("TestUser", 0).id;
+        final int testUser = createUser();
         final CountDownLatch latch1 = new CountDownLatch(1);
         registerBroadcastReceiver(Intent.ACTION_USER_UNLOCKED, latch1, testUser);
         mAm.switchUser(testUser);
@@ -362,6 +470,45 @@
         return testUser;
     }
 
+    /**
+     * Installs the given package in the given user.
+     */
+    private void installPreexistingApp(int userId, String packageName) throws RemoteException {
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        final IntentSender sender = new IntentSender((IIntentSender) new IIntentSender.Stub() {
+            @Override
+            public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken,
+                    IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
+                latch.countDown();
+            }
+        });
+
+        final IPackageInstaller installer = AppGlobals.getPackageManager().getPackageInstaller();
+        installer.installExistingPackage(packageName, 0, PackageManager.INSTALL_REASON_UNKNOWN,
+                sender, userId);
+
+        try {
+            latch.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            Log.e(TAG, "Thread interrupted unexpectedly.", e);
+        }
+    }
+
+    /**
+     * Launches the given package in the given user.
+     * Make sure the keyguard has been dismissed prior to calling.
+     */
+    private void startApp(int userId, String packageName) throws RemoteException {
+        final Context context = InstrumentationRegistry.getContext();
+        final WaitResult result = ActivityTaskManager.getService().startActivityAndWait(null, null,
+                context.getPackageManager().getLaunchIntentForPackage(packageName),
+                null, null, null, 0, 0, null, null,
+                userId);
+        attestTrue("User " + userId + " failed to start " + packageName,
+                result.result == ActivityManager.START_SUCCESS);
+    }
+
     private void registerUserSwitchObserver(final CountDownLatch switchLatch,
             final CountDownLatch bootCompleteLatch, final int userId) throws Exception {
         ActivityManager.getService().registerUserSwitchObserver(
@@ -395,6 +542,30 @@
         }, UserHandle.of(userId), new IntentFilter(action), null, null);
     }
 
+    private class ProgressWaiter extends IProgressListener.Stub {
+        private final CountDownLatch mFinishedLatch = new CountDownLatch(1);
+
+        @Override
+        public void onStarted(int id, Bundle extras) {}
+
+        @Override
+        public void onProgress(int id, int progress, Bundle extras) {}
+
+        @Override
+        public void onFinished(int id, Bundle extras) {
+            mFinishedLatch.countDown();
+        }
+
+        public boolean waitForFinish(long timeoutSecs) {
+            try {
+                return mFinishedLatch.await(timeoutSecs, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                Log.e(TAG, "Thread interrupted unexpectedly.", e);
+                return false;
+            }
+        }
+    }
+
     private void removeUser(int userId) {
         try {
             mUm.removeUser(userId);
@@ -414,13 +585,13 @@
         }
     }
 
-    private void attestTrue(String message, boolean attestion) {
-        if (!attestion) {
+    private void attestTrue(String message, boolean assertion) {
+        if (!assertion) {
             Log.w(TAG, message);
         }
     }
 
-    private void attestFalse(String message, boolean attestion) {
-        attestTrue(message, !attestion);
+    private void attestFalse(String message, boolean assertion) {
+        attestTrue(message, !assertion);
     }
 }