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 }