diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java index 0a416b0484ee2..f22e0698e73f3 100644 --- a/services/core/java/com/android/server/pm/StagingManager.java +++ b/services/core/java/com/android/server/pm/StagingManager.java @@ -29,6 +29,8 @@ import android.content.IIntentSender; import android.content.Intent; import android.content.IntentFilter; import android.content.IntentSender; +import android.content.pm.ApexStagedEvent; +import android.content.pm.IStagedApexObserver; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; @@ -103,7 +105,8 @@ public class StagingManager { private final ApexManager mApexManager; private final PowerManager mPowerManager; private final Context mContext; - private final PreRebootVerificationHandler mPreRebootVerificationHandler; + @VisibleForTesting + final PreRebootVerificationHandler mPreRebootVerificationHandler; private final Supplier mPackageParserSupplier; private final File mFailureReasonFile = new File("/metadata/staged-install/failure_reason.txt"); @@ -119,6 +122,9 @@ public class StagingManager { @GuardedBy("mSuccessfulStagedSessionIds") private final List mSuccessfulStagedSessionIds = new ArrayList<>(); + @GuardedBy("mStagedApexObservers") + private final List mStagedApexObservers = new ArrayList<>(); + private final CompletableFuture mBootCompleted = new CompletableFuture<>(); interface StagedSession { @@ -208,6 +214,18 @@ public class StagingManager { mApexManager.markBootCompleted(); } + void registerStagedApexObserver(IStagedApexObserver observer) { + synchronized (mStagedApexObservers) { + mStagedApexObservers.add(observer); + } + } + + void unregisterStagedApexObserver(IStagedApexObserver observer) { + synchronized (mStagedApexObservers) { + mStagedApexObservers.remove(observer); + } + } + /** * Validates the signature used to sign the container of the new apex package * @@ -812,6 +830,9 @@ public class StagingManager { // Also, cleaning up the stageDir prevents the apex from being activated. Slog.e(TAG, "Failed to abort apex session " + session.sessionId()); } + if (session.containsApexSession()) { + notifyStagedApexObservers(); + } } // Session was successfully aborted from apexd (if required) and pre-reboot verification @@ -1177,7 +1198,22 @@ public class StagingManager { return null; } - private final class PreRebootVerificationHandler extends Handler { + private void notifyStagedApexObservers() { + synchronized (mStagedApexObservers) { + for (IStagedApexObserver observer : mStagedApexObservers) { + ApexStagedEvent event = new ApexStagedEvent(); + event.stagedApexModuleNames = getStagedApexModuleNames().toArray(new String[0]); + try { + observer.onApexStaged(event); + } catch (RemoteException re) { + Slog.w(TAG, "Failed to contact the observer " + re.getMessage()); + } + } + } + } + + @VisibleForTesting + final class PreRebootVerificationHandler extends Handler { PreRebootVerificationHandler(Looper looper) { super(looper); @@ -1198,7 +1234,8 @@ public class StagingManager { */ private static final int MSG_PRE_REBOOT_VERIFICATION_START = 1; private static final int MSG_PRE_REBOOT_VERIFICATION_APEX = 2; - private static final int MSG_PRE_REBOOT_VERIFICATION_END = 3; + @VisibleForTesting + static final int MSG_PRE_REBOOT_VERIFICATION_END = 3; @Override public void handleMessage(Message msg) { @@ -1398,6 +1435,7 @@ public class StagingManager { if (hasApex) { try { mApexManager.markStagedSessionReady(session.sessionId()); + notifyStagedApexObservers(); } catch (PackageManagerException e) { session.setSessionFailed(e.error, e.getMessage()); return; diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java index 04764f7ff6b67..5646da175f559 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java @@ -37,9 +37,12 @@ import android.apex.ApexSessionInfo; import android.apex.ApexSessionParams; import android.content.Context; import android.content.IntentSender; +import android.content.pm.ApexStagedEvent; +import android.content.pm.IStagedApexObserver; import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageInstaller.SessionInfo.StagedSessionErrorCode; +import android.os.Message; import android.os.SystemProperties; import android.os.storage.IStorageManager; import android.platform.test.annotations.Presubmit; @@ -60,6 +63,7 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.MockitoSession; import org.mockito.invocation.InvocationOnMock; @@ -655,8 +659,16 @@ public class StagingManagerTest { parentSession.setSessionReady(); mStagingManager.createSession(parentSession); - // Make mApexManager return ApexInfo with same module name as the sessionId - // of the parameter that was passed into it + mockApexManagerGetStagedApexInfoWithSessionId(); + + List result = mStagingManager.getStagedApexModuleNames(); + assertThat(result).containsExactly("239", "123", "124"); + verify(mApexManager, times(2)).getStagedApexInfos(any()); + } + + // Make mApexManager return ApexInfo with same module name as the sessionId + // of the parameter that was passed into it + private void mockApexManagerGetStagedApexInfoWithSessionId() { when(mApexManager.getStagedApexInfos(any())).thenAnswer(new Answer() { @Override public ApexInfo[] answer(InvocationOnMock invocation) throws Throwable { @@ -677,10 +689,6 @@ public class StagingManagerTest { return result.toArray(new ApexInfo[0]); } }); - - List result = mStagingManager.getStagedApexModuleNames(); - assertThat(result).containsExactly("239", "123", "124"); - verify(mApexManager, times(2)).getStagedApexInfos(any()); } @Test @@ -702,6 +710,106 @@ public class StagingManagerTest { verify(mApexManager, times(2)).getStagedApexInfos(any()); } + @Test + public void registeredStagedApexObserverIsNotifiedOnPreRebootVerificationCompletion() + throws Exception { + // Register observer + IStagedApexObserver observer = Mockito.mock(IStagedApexObserver.class); + mStagingManager.registerStagedApexObserver(observer); + + // Create one staged session and trigger end of pre-reboot verification + { + FakeStagedSession session = new FakeStagedSession(239); + session.setIsApex(true); + mStagingManager.createSession(session); + + mockApexManagerGetStagedApexInfoWithSessionId(); + triggerEndOfPreRebootVerification(session); + + assertThat(session.isSessionReady()).isTrue(); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass( + ApexStagedEvent.class); + verify(observer, times(1)).onApexStaged(argumentCaptor.capture()); + assertThat(argumentCaptor.getValue().stagedApexModuleNames).isEqualTo( + new String[]{"239"}); + } + + // Create another staged session and verify observers are notified of union + { + Mockito.clearInvocations(observer); + FakeStagedSession session = new FakeStagedSession(240); + session.setIsApex(true); + mStagingManager.createSession(session); + + triggerEndOfPreRebootVerification(session); + + assertThat(session.isSessionReady()).isTrue(); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass( + ApexStagedEvent.class); + verify(observer, times(1)).onApexStaged(argumentCaptor.capture()); + assertThat(argumentCaptor.getValue().stagedApexModuleNames).isEqualTo( + new String[]{"239", "240"}); + } + + // Finally, verify that once unregistered, observer is not notified + mStagingManager.unregisterStagedApexObserver(observer); + { + Mockito.clearInvocations(observer); + FakeStagedSession session = new FakeStagedSession(241); + session.setIsApex(true); + mStagingManager.createSession(session); + + triggerEndOfPreRebootVerification(session); + + assertThat(session.isSessionReady()).isTrue(); + verify(observer, never()).onApexStaged(any()); + } + } + + @Test + public void registeredStagedApexObserverIsNotifiedOnSessionAbandon() throws Exception { + // Register observer + IStagedApexObserver observer = Mockito.mock(IStagedApexObserver.class); + mStagingManager.registerStagedApexObserver(observer); + + // Create a ready session and abandon it + FakeStagedSession session = new FakeStagedSession(239); + session.setIsApex(true); + session.setSessionReady(); + session.setDestroyed(true); + mStagingManager.createSession(session); + + mStagingManager.abortCommittedSession(session); + + assertThat(session.isSessionReady()).isTrue(); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass( + ApexStagedEvent.class); + verify(observer, times(1)).onApexStaged(argumentCaptor.capture()); + assertThat(argumentCaptor.getValue().stagedApexModuleNames).hasLength(0); + } + + @Test + public void stagedApexObserverIsOnlyCalledForApexSessions() throws Exception { + IStagedApexObserver observer = Mockito.mock(IStagedApexObserver.class); + mStagingManager.registerStagedApexObserver(observer); + + // Trigger end of pre-reboot verification + FakeStagedSession session = new FakeStagedSession(239); + mStagingManager.createSession(session); + + triggerEndOfPreRebootVerification(session); + assertThat(session.isSessionReady()).isTrue(); + verify(observer, never()).onApexStaged(any()); + } + + private void triggerEndOfPreRebootVerification(StagingManager.StagedSession session) { + StagingManager.PreRebootVerificationHandler handler = + mStagingManager.mPreRebootVerificationHandler; + Message msg = handler.obtainMessage( + handler.MSG_PRE_REBOOT_VERIFICATION_END, session.sessionId(), -1, session); + handler.handleMessage(msg); + } + private StagingManager.StagedSession createSession(int sessionId, String packageName, long committedMillis) { PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( @@ -958,9 +1066,7 @@ public class StagingManagerTest { } @Override - public void notifyEndPreRebootVerification() { - throw new UnsupportedOperationException(); - } + public void notifyEndPreRebootVerification() {} @Override public void verifySession() {