From acea5f3b545a7621b8e943dea36f8f0f8fa5e593 Mon Sep 17 00:00:00 2001 From: Tianjie Date: Sun, 1 Nov 2020 20:00:23 -0800 Subject: [PATCH] Add multi client ror support Detailed design in http://go/multi-client-ror. This cl adds internal states in RecoverySystemService to keep track of multiple RoR callers. The general principles works as: 1. Each client should call prepare before reboot 2. One client cannot clear the preparation of another client 3. If multiple clients have prepared the update, the subsequent reboot will be first come, first served. Bug: 170664917 Test: atest FrameworksServicesTests:RecoverySystemServiceTest Change-Id: Ic8c7438f8a0300870ceed8857c46425fc0d006b9 --- core/java/android/os/IRecoverySystem.aidl | 7 +- core/java/android/os/RecoverySystem.java | 61 +++-- .../recoverysystem/RecoverySystemService.java | 236 ++++++++++++++---- .../RecoverySystemShellCommand.java | 21 +- .../RecoverySystemServiceTest.java | 164 +++++++++--- 5 files changed, 365 insertions(+), 124 deletions(-) diff --git a/core/java/android/os/IRecoverySystem.aidl b/core/java/android/os/IRecoverySystem.aidl index 2561e1ea69c6f..5f8b932832690 100644 --- a/core/java/android/os/IRecoverySystem.aidl +++ b/core/java/android/os/IRecoverySystem.aidl @@ -27,7 +27,8 @@ interface IRecoverySystem { boolean setupBcb(in String command); boolean clearBcb(); void rebootRecoveryWithCommand(in String command); - boolean requestLskf(in String updateToken, in IntentSender sender); - boolean clearLskf(); - boolean rebootWithLskf(in String updateToken, in String reason); + boolean requestLskf(in String packageName, in IntentSender sender); + boolean clearLskf(in String packageName); + boolean isLskfCaptured(in String packageName); + boolean rebootWithLskf(in String packageName, in String reason, in boolean slotSwitch); } diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index 38e170402ae9d..71b9f15be2974 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -632,17 +632,11 @@ public class RecoverySystem { * Prepare to apply an unattended update by asking the user for their Lock Screen Knowledge * Factor (LSKF). If supplied, the {@code intentSender} will be called when the system is setup * and ready to apply the OTA. - *

- * When the system is already prepared for update and this API is called again with the same - * {@code updateToken}, it will not call the intent sender nor request the user enter their Lock - * Screen Knowledge Factor. - *

- * When this API is called again with a different {@code updateToken}, the prepared-for-update - * status is reset and process repeats as though it's the initial call to this method as - * described in the first paragraph. * * @param context the Context to use. - * @param updateToken token used to indicate which update was prepared + * @param updateToken this parameter is deprecated and won't be used. See details in + * http://go/multi-client-ror + * TODO(xunchang) update the link of document with the public doc. * @param intentSender the intent to call when the update is prepared; may be {@code null} * @throws IOException if there were any errors setting up unattended update * @hide @@ -655,7 +649,7 @@ public class RecoverySystem { throw new NullPointerException("updateToken == null"); } RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE); - if (!rs.requestLskf(updateToken, intentSender)) { + if (!rs.requestLskf(context.getPackageName(), intentSender)) { throw new IOException("preparation for update failed"); } } @@ -673,18 +667,18 @@ public class RecoverySystem { public static void clearPrepareForUnattendedUpdate(@NonNull Context context) throws IOException { RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE); - if (!rs.clearLskf()) { + if (!rs.clearLskf(context.getPackageName())) { throw new IOException("could not reset unattended update state"); } } /** - * Request that the device reboot and apply the update that has been prepared. The - * {@code updateToken} must match what was given for {@link #prepareForUnattendedUpdate} or - * this will return {@code false}. + * Request that the device reboot and apply the update that has been prepared. * * @param context the Context to use. - * @param updateToken the token used to call {@link #prepareForUnattendedUpdate} before + * @param updateToken this parameter is deprecated and won't be used. See details in + * http://go/multi-client-ror + * TODO(xunchang) update the link of document with the public doc. * @param reason the reboot reason to give to the {@link PowerManager} * @throws IOException if the reboot couldn't proceed because the device wasn't ready for an * unattended reboot or if the {@code updateToken} did not match the previously @@ -699,7 +693,8 @@ public class RecoverySystem { throw new NullPointerException("updateToken == null"); } RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE); - if (!rs.rebootWithLskf(updateToken, reason)) { + // OTA is the sole user before S, and a slot switch is required for ota update. + if (!rs.rebootWithLskf(context.getPackageName(), reason, true)) { throw new IOException("system not prepared to apply update"); } } @@ -1283,16 +1278,15 @@ public class RecoverySystem { /** * Begins the process of asking the user for the Lock Screen Knowledge Factor. * - * @param updateToken token that will be used in calls to {@link #rebootAndApply} to ensure - * that the preparation was for the correct update + * @param packageName the package name of the caller who requests resume on reboot * @return true if the request was correct * @throws IOException if the recovery system service could not be contacted */ - private boolean requestLskf(String updateToken, IntentSender sender) throws IOException { + private boolean requestLskf(String packageName, IntentSender sender) throws IOException { try { - return mService.requestLskf(updateToken, sender); + return mService.requestLskf(packageName, sender); } catch (RemoteException e) { - throw new IOException("could request update"); + throw new IOException("could request LSKF capture"); } } @@ -1302,22 +1296,37 @@ public class RecoverySystem { * @return true if the setup for OTA was cleared * @throws IOException if the recovery system service could not be contacted */ - private boolean clearLskf() throws IOException { + private boolean clearLskf(String packageName) throws IOException { try { - return mService.clearLskf(); + return mService.clearLskf(packageName); } catch (RemoteException e) { throw new IOException("could not clear LSKF"); } } + /** + * Queries if the resume on reboot has been prepared for a given caller. + * + * @param packageName the identifier of the caller who requests resume on reboot + * @return true if resume on reboot is prepared. + * @throws IOException if the recovery system service could not be contacted + */ + private boolean isLskfCaptured(String packageName) throws IOException { + try { + return mService.isLskfCaptured(packageName); + } catch (RemoteException e) { + throw new IOException("could not get LSKF capture state"); + } + } + /** * Calls the recovery system service to reboot and apply update. * - * @param updateToken the update token for which the update was prepared */ - private boolean rebootWithLskf(String updateToken, String reason) throws IOException { + private boolean rebootWithLskf(String packageName, String reason, boolean slotSwitch) + throws IOException { try { - return mService.rebootWithLskf(updateToken, reason); + return mService.rebootWithLskf(packageName, reason, slotSwitch); } catch (RemoteException e) { throw new IOException("could not reboot for update"); } diff --git a/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java b/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java index e0701e867cad7..990055ebda9a8 100644 --- a/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java +++ b/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java @@ -16,8 +16,10 @@ package com.android.server.recoverysystem; +import android.annotation.IntDef; import android.content.Context; import android.content.IntentSender; +import android.content.pm.PackageManager; import android.net.LocalSocket; import android.net.LocalSocketAddress; import android.os.Binder; @@ -32,6 +34,7 @@ import android.os.ShellCallback; import android.os.SystemProperties; import android.util.Slog; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.LockSettingsInternal; import com.android.internal.widget.RebootEscrowListener; @@ -46,6 +49,10 @@ import java.io.FileDescriptor; import java.io.FileWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; /** * The recovery system service is responsible for coordinating recovery related @@ -76,9 +83,53 @@ public class RecoverySystemService extends IRecoverySystem.Stub implements Reboo private final Injector mInjector; private final Context mContext; - private boolean mPreparedForReboot; - private String mUnattendedRebootToken; - private IntentSender mPreparedForRebootIntentSender; + @GuardedBy("this") + private final Map mCallerPendingRequest = new HashMap<>(); + @GuardedBy("this") + private final Set mCallerPreparedForReboot = new HashSet<>(); + + /** + * Need to prepare for resume on reboot. + */ + private static final int ROR_NEED_PREPARATION = 0; + /** + * Resume on reboot has been prepared, notify the caller. + */ + private static final int ROR_SKIP_PREPARATION_AND_NOTIFY = 1; + /** + * Resume on reboot has been requested. Caller won't be notified until the preparation is done. + */ + private static final int ROR_SKIP_PREPARATION_NOT_NOTIFY = 2; + + /** + * The caller never requests for resume on reboot, no need for clear. + */ + private static final int ROR_NOT_REQUESTED = 0; + /** + * Clear the resume on reboot preparation state. + */ + private static final int ROR_REQUESTED_NEED_CLEAR = 1; + /** + * The caller has requested for resume on reboot. No need for clear since other callers may + * exist. + */ + private static final int ROR_REQUESTED_SKIP_CLEAR = 2; + + /** + * The action to perform upon new resume on reboot prepare request for a given client. + */ + @IntDef({ ROR_NEED_PREPARATION, + ROR_SKIP_PREPARATION_AND_NOTIFY, + ROR_SKIP_PREPARATION_NOT_NOTIFY }) + @interface ResumeOnRebootActionsOnRequest {} + + /** + * The action to perform upon resume on reboot clear request for a given client. + */ + @IntDef({ROR_NOT_REQUESTED, + ROR_REQUESTED_NEED_CLEAR, + ROR_REQUESTED_SKIP_CLEAR}) + @interface ResumeOnRebootActionsOnClear{} static class Injector { protected final Context mContext; @@ -286,47 +337,92 @@ public class RecoverySystemService extends IRecoverySystem.Stub implements Reboo } } - @Override // Binder call - public boolean requestLskf(String updateToken, IntentSender intentSender) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null); + private void enforcePermissionForResumeOnReboot() { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.RECOVERY) + != PackageManager.PERMISSION_GRANTED + && mContext.checkCallingOrSelfPermission(android.Manifest.permission.REBOOT) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Caller or self must have " + + android.Manifest.permission.RECOVERY + " or " + + android.Manifest.permission.REBOOT + " for resume on reboot."); + } + } - if (updateToken == null) { + @Override // Binder call + public boolean requestLskf(String packageName, IntentSender intentSender) { + enforcePermissionForResumeOnReboot(); + + if (packageName == null) { + Slog.w(TAG, "Missing packageName when requesting lskf."); return false; } - // No need to prepare again for the same token. - if (mPreparedForReboot && updateToken.equals(mUnattendedRebootToken)) { - return true; + @ResumeOnRebootActionsOnRequest int action = updateRoRPreparationStateOnNewRequest( + packageName, intentSender); + switch (action) { + case ROR_SKIP_PREPARATION_AND_NOTIFY: + // We consider the preparation done if someone else has prepared. + sendPreparedForRebootIntentIfNeeded(intentSender); + return true; + case ROR_SKIP_PREPARATION_NOT_NOTIFY: + return true; + case ROR_NEED_PREPARATION: + final long origId = Binder.clearCallingIdentity(); + try { + mInjector.getLockSettingsService().prepareRebootEscrow(); + } finally { + Binder.restoreCallingIdentity(origId); + } + return true; + default: + throw new IllegalStateException("Unsupported action type on new request " + action); + } + } + + // Checks and updates the resume on reboot preparation state. + private synchronized @ResumeOnRebootActionsOnRequest int updateRoRPreparationStateOnNewRequest( + String packageName, IntentSender intentSender) { + if (!mCallerPreparedForReboot.isEmpty()) { + if (mCallerPreparedForReboot.contains(packageName)) { + Slog.i(TAG, "RoR already has prepared for " + packageName); + } + + // Someone else has prepared. Consider the preparation done, and send back the intent. + mCallerPreparedForReboot.add(packageName); + return ROR_SKIP_PREPARATION_AND_NOTIFY; } - mPreparedForReboot = false; - mUnattendedRebootToken = updateToken; - mPreparedForRebootIntentSender = intentSender; - - final long origId = Binder.clearCallingIdentity(); - try { - mInjector.getLockSettingsService().prepareRebootEscrow(); - } finally { - Binder.restoreCallingIdentity(origId); + boolean needPreparation = mCallerPendingRequest.isEmpty(); + if (mCallerPendingRequest.containsKey(packageName)) { + Slog.i(TAG, "Duplicate RoR preparation request for " + packageName); } - - return true; + // Update the request with the new intentSender. + mCallerPendingRequest.put(packageName, intentSender); + return needPreparation ? ROR_NEED_PREPARATION : ROR_SKIP_PREPARATION_NOT_NOTIFY; } @Override public void onPreparedForReboot(boolean ready) { - if (mUnattendedRebootToken == null) { - Slog.w(TAG, "onPreparedForReboot called when mUnattendedRebootToken is null"); - } - - mPreparedForReboot = ready; - if (ready) { - sendPreparedForRebootIntentIfNeeded(); + if (!ready) { + return; } + updateRoRPreparationStateOnPreparedForReboot(); } - private void sendPreparedForRebootIntentIfNeeded() { - final IntentSender intentSender = mPreparedForRebootIntentSender; + private synchronized void updateRoRPreparationStateOnPreparedForReboot() { + if (!mCallerPreparedForReboot.isEmpty()) { + Slog.w(TAG, "onPreparedForReboot called when some clients have prepared."); + } + + // Send intents to notify callers + for (Map.Entry entry : mCallerPendingRequest.entrySet()) { + sendPreparedForRebootIntentIfNeeded(entry.getValue()); + mCallerPreparedForReboot.add(entry.getKey()); + } + mCallerPendingRequest.clear(); + } + + private void sendPreparedForRebootIntentIfNeeded(IntentSender intentSender) { if (intentSender != null) { try { intentSender.sendIntent(null, 0, null, null, null); @@ -337,37 +433,61 @@ public class RecoverySystemService extends IRecoverySystem.Stub implements Reboo } @Override // Binder call - public boolean clearLskf() { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null); - - mPreparedForReboot = false; - mUnattendedRebootToken = null; - mPreparedForRebootIntentSender = null; - - final long origId = Binder.clearCallingIdentity(); - try { - mInjector.getLockSettingsService().clearRebootEscrow(); - } finally { - Binder.restoreCallingIdentity(origId); + public boolean clearLskf(String packageName) { + enforcePermissionForResumeOnReboot(); + if (packageName == null) { + Slog.w(TAG, "Missing packageName when clearing lskf."); + return false; } - return true; + @ResumeOnRebootActionsOnClear int action = updateRoRPreparationStateOnClear(packageName); + switch (action) { + case ROR_NOT_REQUESTED: + Slog.w(TAG, "RoR clear called before preparation for caller " + packageName); + return true; + case ROR_REQUESTED_SKIP_CLEAR: + return true; + case ROR_REQUESTED_NEED_CLEAR: + final long origId = Binder.clearCallingIdentity(); + try { + mInjector.getLockSettingsService().clearRebootEscrow(); + } finally { + Binder.restoreCallingIdentity(origId); + } + return true; + default: + throw new IllegalStateException("Unsupported action type on clear " + action); + } + } + + private synchronized @ResumeOnRebootActionsOnClear int updateRoRPreparationStateOnClear( + String packageName) { + if (!mCallerPreparedForReboot.contains(packageName) && !mCallerPendingRequest.containsKey( + packageName)) { + Slog.w(TAG, packageName + " hasn't prepared for resume on reboot"); + return ROR_NOT_REQUESTED; + } + mCallerPendingRequest.remove(packageName); + mCallerPreparedForReboot.remove(packageName); + + // Check if others have prepared ROR. + boolean needClear = mCallerPendingRequest.isEmpty() && mCallerPreparedForReboot.isEmpty(); + return needClear ? ROR_REQUESTED_NEED_CLEAR : ROR_REQUESTED_SKIP_CLEAR; } @Override // Binder call - public boolean rebootWithLskf(String updateToken, String reason) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null); - - if (!mPreparedForReboot) { - Slog.i(TAG, "Reboot requested before prepare completed"); - return false; - } - - if (updateToken != null && !updateToken.equals(mUnattendedRebootToken)) { - Slog.i(TAG, "Reboot requested after preparation, but with mismatching token"); + public boolean rebootWithLskf(String packageName, String reason, boolean slotSwitch) { + enforcePermissionForResumeOnReboot(); + if (packageName == null) { + Slog.w(TAG, "Missing packageName when rebooting with lskf."); + return false; + } + if (!isLskfCaptured(packageName)) { return false; } + // TODO(xunchang) check the slot to boot into, and fail the reboot upon slot mismatch. + // TODO(xunchang) write the vbmeta digest along with the escrowKey before reboot. if (!mInjector.getLockSettingsService().armRebootEscrow()) { Slog.w(TAG, "Failure to escrow key for reboot"); return false; @@ -378,6 +498,16 @@ public class RecoverySystemService extends IRecoverySystem.Stub implements Reboo return true; } + @Override // Binder call + public synchronized boolean isLskfCaptured(String packageName) { + enforcePermissionForResumeOnReboot(); + if (!mCallerPreparedForReboot.contains(packageName)) { + Slog.i(TAG, "Reboot requested before prepare completed for caller " + packageName); + return false; + } + return true; + } + /** * Check if any of the init services is still running. If so, we cannot * start a new uncrypt/setup-bcb/clear-bcb service right away; otherwise diff --git a/services/core/java/com/android/server/recoverysystem/RecoverySystemShellCommand.java b/services/core/java/com/android/server/recoverysystem/RecoverySystemShellCommand.java index c6905b5c7dd20..f20d80d57476b 100644 --- a/services/core/java/com/android/server/recoverysystem/RecoverySystemShellCommand.java +++ b/services/core/java/com/android/server/recoverysystem/RecoverySystemShellCommand.java @@ -56,26 +56,31 @@ public class RecoverySystemShellCommand extends ShellCommand { } private int requestLskf() throws RemoteException { - String updateToken = getNextArgRequired(); - boolean success = mService.requestLskf(updateToken, null); + String packageName = getNextArgRequired(); + boolean success = mService.requestLskf(packageName, null); PrintWriter pw = getOutPrintWriter(); - pw.println("Request LSKF status: " + (success ? "success" : "failure")); + pw.printf("Request LSKF for packageName: %s, status: %s\n", packageName, + success ? "success" : "failure"); return 0; } private int clearLskf() throws RemoteException { - boolean success = mService.clearLskf(); + String packageName = getNextArgRequired(); + boolean success = mService.clearLskf(packageName); PrintWriter pw = getOutPrintWriter(); - pw.println("Clear LSKF: " + (success ? "success" : "failure")); + pw.printf("Clear LSKF for packageName: %s, status: %s\n", packageName, + success ? "success" : "failure"); return 0; } private int rebootAndApply() throws RemoteException { - String updateToken = getNextArgRequired(); + String packageName = getNextArgRequired(); String rebootReason = getNextArgRequired(); - boolean success = mService.rebootWithLskf(updateToken, rebootReason); + boolean success = mService.rebootWithLskf(packageName, rebootReason, true); PrintWriter pw = getOutPrintWriter(); - pw.println("Reboot and apply status: " + (success ? "success" : "failure")); + // Keep the old message for cts test. + pw.printf("%s Reboot and apply status: %s\n", packageName, + success ? "success" : "failure"); return 0; } diff --git a/services/tests/servicestests/src/com/android/server/recoverysystem/RecoverySystemServiceTest.java b/services/tests/servicestests/src/com/android/server/recoverysystem/RecoverySystemServiceTest.java index 035a2f11112cb..b07b8fa059d1e 100644 --- a/services/tests/servicestests/src/com/android/server/recoverysystem/RecoverySystemServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/recoverysystem/RecoverySystemServiceTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; @@ -33,6 +34,7 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.content.IntentSender; +import android.content.pm.PackageManager; import android.os.Handler; import android.os.IPowerManager; import android.os.IRecoverySystemProgressListener; @@ -67,6 +69,9 @@ public class RecoverySystemServiceTest { private FileWriter mUncryptUpdateFileWriter; private LockSettingsInternal mLockSettingsInternal; + private static final String FAKE_OTA_PACKAGE_NAME = "fake.ota.package"; + private static final String FAKE_OTHER_PACKAGE_NAME = "fake.other.package"; + @Before public void setup() { mContext = mock(Context.class); @@ -209,65 +214,99 @@ public class RecoverySystemServiceTest { @Test(expected = SecurityException.class) public void requestLskf_protected() { - doThrow(SecurityException.class).when(mContext).enforceCallingOrSelfPermission( - eq(android.Manifest.permission.RECOVERY), any()); - mRecoverySystemService.requestLskf("test", null); - } - - - @Test - public void requestLskf_nullToken_failure() { - assertThat(mRecoverySystemService.requestLskf(null, null), is(false)); + when(mContext.checkCallingOrSelfPermission(anyString())).thenReturn( + PackageManager.PERMISSION_DENIED); + mRecoverySystemService.requestLskf(FAKE_OTA_PACKAGE_NAME, null); } @Test public void requestLskf_success() throws Exception { IntentSender intentSender = mock(IntentSender.class); - assertThat(mRecoverySystemService.requestLskf("test", intentSender), is(true)); + assertThat(mRecoverySystemService.requestLskf(FAKE_OTA_PACKAGE_NAME, intentSender), + is(true)); mRecoverySystemService.onPreparedForReboot(true); verify(intentSender).sendIntent(any(), anyInt(), any(), any(), any()); } @Test - public void requestLskf_subsequentRequestClearsPrepared() throws Exception { + public void requestLskf_subsequentRequestNotClearPrepared() throws Exception { IntentSender intentSender = mock(IntentSender.class); - assertThat(mRecoverySystemService.requestLskf("test", intentSender), is(true)); + assertThat(mRecoverySystemService.requestLskf(FAKE_OTA_PACKAGE_NAME, intentSender), + is(true)); mRecoverySystemService.onPreparedForReboot(true); verify(intentSender).sendIntent(any(), anyInt(), any(), any(), any()); - assertThat(mRecoverySystemService.requestLskf("test2", null), is(true)); - assertThat(mRecoverySystemService.rebootWithLskf("test", null), is(false)); - assertThat(mRecoverySystemService.rebootWithLskf("test2", "foobar"), is(false)); - - mRecoverySystemService.onPreparedForReboot(true); - assertThat(mRecoverySystemService.rebootWithLskf("test2", "foobar"), is(true)); - verify(intentSender).sendIntent(any(), anyInt(), any(), any(), any()); + assertThat(mRecoverySystemService.requestLskf(FAKE_OTA_PACKAGE_NAME, null), is(true)); + assertThat(mRecoverySystemService.rebootWithLskf(FAKE_OTA_PACKAGE_NAME, "foobar", true), + is(true)); verify(mIPowerManager).reboot(anyBoolean(), eq("foobar"), anyBoolean()); } - @Test public void requestLskf_requestedButNotPrepared() throws Exception { IntentSender intentSender = mock(IntentSender.class); - assertThat(mRecoverySystemService.requestLskf("test", intentSender), is(true)); + assertThat(mRecoverySystemService.requestLskf(FAKE_OTA_PACKAGE_NAME, intentSender), + is(true)); verify(intentSender, never()).sendIntent(any(), anyInt(), any(), any(), any()); } + @Test + public void isLskfCaptured_requestedButNotPrepared() throws Exception { + IntentSender intentSender = mock(IntentSender.class); + assertThat(mRecoverySystemService.requestLskf(FAKE_OTA_PACKAGE_NAME, intentSender), + is(true)); + assertThat(mRecoverySystemService.isLskfCaptured(FAKE_OTA_PACKAGE_NAME), is(false)); + } + + @Test + public void isLskfCaptured_Prepared() throws Exception { + IntentSender intentSender = mock(IntentSender.class); + assertThat(mRecoverySystemService.requestLskf(FAKE_OTA_PACKAGE_NAME, intentSender), + is(true)); + mRecoverySystemService.onPreparedForReboot(true); + verify(intentSender).sendIntent(any(), anyInt(), any(), any(), any()); + assertThat(mRecoverySystemService.isLskfCaptured(FAKE_OTA_PACKAGE_NAME), is(true)); + } + @Test(expected = SecurityException.class) public void clearLskf_protected() { - doThrow(SecurityException.class).when(mContext).enforceCallingOrSelfPermission( - eq(android.Manifest.permission.RECOVERY), any()); - mRecoverySystemService.clearLskf(); + when(mContext.checkCallingOrSelfPermission(anyString())).thenReturn( + PackageManager.PERMISSION_DENIED); + mRecoverySystemService.clearLskf(FAKE_OTA_PACKAGE_NAME); } @Test public void clearLskf_requestedThenCleared() throws Exception { IntentSender intentSender = mock(IntentSender.class); - assertThat(mRecoverySystemService.requestLskf("test", intentSender), is(true)); + assertThat(mRecoverySystemService.requestLskf(FAKE_OTA_PACKAGE_NAME, intentSender), + is(true)); mRecoverySystemService.onPreparedForReboot(true); verify(intentSender).sendIntent(any(), anyInt(), any(), any(), any()); - assertThat(mRecoverySystemService.clearLskf(), is(true)); + assertThat(mRecoverySystemService.clearLskf(FAKE_OTA_PACKAGE_NAME), is(true)); + verify(mLockSettingsInternal).clearRebootEscrow(); + } + + @Test + public void clearLskf_callerNotRequested_Success() throws Exception { + IntentSender intentSender = mock(IntentSender.class); + assertThat(mRecoverySystemService.requestLskf(FAKE_OTA_PACKAGE_NAME, intentSender), + is(true)); + assertThat(mRecoverySystemService.clearLskf(FAKE_OTHER_PACKAGE_NAME), is(true)); + verify(mLockSettingsInternal, never()).clearRebootEscrow(); + } + + @Test + public void clearLskf_multiClient_BothClientsClear() throws Exception { + IntentSender intentSender = mock(IntentSender.class); + assertThat(mRecoverySystemService.requestLskf(FAKE_OTA_PACKAGE_NAME, intentSender), + is(true)); + assertThat(mRecoverySystemService.requestLskf(FAKE_OTHER_PACKAGE_NAME, intentSender), + is(true)); + + assertThat(mRecoverySystemService.clearLskf(FAKE_OTA_PACKAGE_NAME), is(true)); + verify(mLockSettingsInternal, never()).clearRebootEscrow(); + assertThat(mRecoverySystemService.clearLskf(FAKE_OTHER_PACKAGE_NAME), is(true)); verify(mLockSettingsInternal).clearRebootEscrow(); } @@ -279,27 +318,84 @@ public class RecoverySystemServiceTest { @Test(expected = SecurityException.class) public void rebootWithLskf_protected() { - doThrow(SecurityException.class).when(mContext).enforceCallingOrSelfPermission( - eq(android.Manifest.permission.RECOVERY), any()); - mRecoverySystemService.rebootWithLskf("test1", null); + when(mContext.checkCallingOrSelfPermission(anyString())).thenReturn( + PackageManager.PERMISSION_DENIED); + mRecoverySystemService.rebootWithLskf(FAKE_OTA_PACKAGE_NAME, null, true); } @Test public void rebootWithLskf_Success() throws Exception { - assertThat(mRecoverySystemService.requestLskf("test", null), is(true)); + assertThat(mRecoverySystemService.requestLskf(FAKE_OTA_PACKAGE_NAME, null), is(true)); mRecoverySystemService.onPreparedForReboot(true); - assertThat(mRecoverySystemService.rebootWithLskf("test", "ab-update"), is(true)); + assertThat(mRecoverySystemService.rebootWithLskf(FAKE_OTA_PACKAGE_NAME, "ab-update", true), + is(true)); verify(mIPowerManager).reboot(anyBoolean(), eq("ab-update"), anyBoolean()); } @Test public void rebootWithLskf_withoutPrepare_Failure() throws Exception { - assertThat(mRecoverySystemService.rebootWithLskf("test1", null), is(false)); + assertThat(mRecoverySystemService.rebootWithLskf(FAKE_OTA_PACKAGE_NAME, null, true), + is(false)); } @Test - public void rebootWithLskf_withNullUpdateToken_Failure() throws Exception { - assertThat(mRecoverySystemService.rebootWithLskf(null, null), is(false)); + public void rebootWithLskf_withNullCallerId_Failure() throws Exception { + assertThat(mRecoverySystemService.rebootWithLskf(null, null, true), is(false)); verifyNoMoreInteractions(mIPowerManager); } + + @Test + public void rebootWithLskf_multiClient_ClientASuccess() throws Exception { + assertThat(mRecoverySystemService.requestLskf(FAKE_OTA_PACKAGE_NAME, null), is(true)); + assertThat(mRecoverySystemService.requestLskf(FAKE_OTHER_PACKAGE_NAME, null), is(true)); + mRecoverySystemService.onPreparedForReboot(true); + + // Client B's clear won't affect client A's preparation. + assertThat(mRecoverySystemService.clearLskf(FAKE_OTHER_PACKAGE_NAME), is(true)); + assertThat(mRecoverySystemService.rebootWithLskf(FAKE_OTA_PACKAGE_NAME, "ab-update", true), + is(true)); + verify(mIPowerManager).reboot(anyBoolean(), eq("ab-update"), anyBoolean()); + } + + + @Test + public void rebootWithLskf_multiClient_ClientBSuccess() throws Exception { + assertThat(mRecoverySystemService.requestLskf(FAKE_OTA_PACKAGE_NAME, null), is(true)); + mRecoverySystemService.onPreparedForReboot(true); + assertThat(mRecoverySystemService.requestLskf(FAKE_OTHER_PACKAGE_NAME, null), is(true)); + + assertThat(mRecoverySystemService.clearLskf(FAKE_OTA_PACKAGE_NAME), is(true)); + assertThat(mRecoverySystemService.rebootWithLskf(FAKE_OTA_PACKAGE_NAME, null, true), + is(false)); + verifyNoMoreInteractions(mIPowerManager); + + assertThat(mRecoverySystemService.requestLskf(FAKE_OTHER_PACKAGE_NAME, null), is(true)); + assertThat( + mRecoverySystemService.rebootWithLskf(FAKE_OTHER_PACKAGE_NAME, "ab-update", true), + is(true)); + verify(mIPowerManager).reboot(anyBoolean(), eq("ab-update"), anyBoolean()); + } + + @Test + public void rebootWithLskf_multiClient_BothClientsClear_Failure() throws Exception { + assertThat(mRecoverySystemService.requestLskf(FAKE_OTA_PACKAGE_NAME, null), is(true)); + mRecoverySystemService.onPreparedForReboot(true); + assertThat(mRecoverySystemService.requestLskf(FAKE_OTHER_PACKAGE_NAME, null), is(true)); + + // Client A clears + assertThat(mRecoverySystemService.clearLskf(FAKE_OTA_PACKAGE_NAME), is(true)); + assertThat(mRecoverySystemService.rebootWithLskf(FAKE_OTA_PACKAGE_NAME, null, true), + is(false)); + verifyNoMoreInteractions(mIPowerManager); + + // Client B clears + assertThat(mRecoverySystemService.clearLskf(FAKE_OTHER_PACKAGE_NAME), is(true)); + verify(mLockSettingsInternal).clearRebootEscrow(); + assertThat( + mRecoverySystemService.rebootWithLskf(FAKE_OTHER_PACKAGE_NAME, "ab-update", true), + is(false)); + verifyNoMoreInteractions(mIPowerManager); + } + + // TODO(xunchang) add more multi client tests }