diff options
7 files changed, 566 insertions, 10 deletions
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index fe419a613b01..b34a3a2b8dcb 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -261,6 +261,10 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements new Lifecycle(context, this)); } + StagingManager getStagingManager() { + return mStagingManager; + } + boolean okToSendBroadcasts() { return mOkToSendBroadcasts; } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index d2c15ab23c83..94306ce1e501 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -163,6 +163,7 @@ import android.content.pm.IPackageManager; import android.content.pm.IPackageManagerNative; import android.content.pm.IPackageMoveObserver; import android.content.pm.IPackageStatsObserver; +import android.content.pm.IStagedApexObserver; import android.content.pm.IncrementalStatesInfo; import android.content.pm.InstallSourceInfo; import android.content.pm.InstantAppInfo; @@ -202,6 +203,7 @@ import android.content.pm.Signature; import android.content.pm.SigningDetails; import android.content.pm.SigningDetails.SignatureSchemeVersion; import android.content.pm.SigningInfo; +import android.content.pm.StagedApexInfo; import android.content.pm.SuspendDialogInfo; import android.content.pm.TestUtilityService; import android.content.pm.UserInfo; @@ -23074,6 +23076,29 @@ public class PackageManagerService extends IPackageManager.Stub public boolean hasSystemFeature(String featureName, int version) { return PackageManagerService.this.hasSystemFeature(featureName, version); } + + @Override + public void registerStagedApexObserver(IStagedApexObserver observer) { + mInstallerService.getStagingManager().registerStagedApexObserver(observer); + } + + @Override + public void unregisterStagedApexObserver(IStagedApexObserver observer) { + mInstallerService.getStagingManager().unregisterStagedApexObserver(observer); + } + + @Override + public String[] getStagedApexModuleNames() { + return mInstallerService.getStagingManager() + .getStagedApexModuleNames().toArray(new String[0]); + } + + @Override + @Nullable + public StagedApexInfo getStagedApexInfo(String moduleName) { + return mInstallerService.getStagingManager().getStagedApexInfo(moduleName); + } + } private AndroidPackage getPackage(String packageName) { @@ -25049,5 +25074,3 @@ public class PackageManagerService extends IPackageManager.Stub } } } - - diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java index 9dcae3da6a31..4dfb6b8ea453 100644 --- a/services/core/java/com/android/server/pm/StagingManager.java +++ b/services/core/java/com/android/server/pm/StagingManager.java @@ -17,6 +17,7 @@ package com.android.server.pm; import android.annotation.NonNull; +import android.annotation.Nullable; import android.apex.ApexInfo; import android.apex.ApexInfoList; import android.apex.ApexSessionInfo; @@ -28,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; @@ -36,6 +39,7 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.SigningDetails; import android.content.pm.SigningDetails.SignatureSchemeVersion; +import android.content.pm.StagedApexInfo; import android.content.pm.parsing.PackageInfoWithoutStateUtils; import android.content.pm.parsing.result.ParseResult; import android.content.pm.parsing.result.ParseTypeImpl; @@ -52,6 +56,7 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.IntArray; import android.util.Slog; @@ -80,7 +85,9 @@ import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.LinkedBlockingQueue; @@ -99,7 +106,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<PackageParser2> mPackageParserSupplier; private final File mFailureReasonFile = new File("/metadata/staged-install/failure_reason.txt"); @@ -115,6 +123,9 @@ public class StagingManager { @GuardedBy("mSuccessfulStagedSessionIds") private final List<Integer> mSuccessfulStagedSessionIds = new ArrayList<>(); + @GuardedBy("mStagedApexObservers") + private final List<IStagedApexObserver> mStagedApexObservers = new ArrayList<>(); + private final CompletableFuture<Void> mBootCompleted = new CompletableFuture<>(); interface StagedSession { @@ -204,6 +215,35 @@ public class StagingManager { mApexManager.markBootCompleted(); } + void registerStagedApexObserver(IStagedApexObserver observer) { + if (observer == null) { + return; + } + if (observer.asBinder() != null) { + try { + observer.asBinder().linkToDeath(new IBinder.DeathRecipient() { + @Override + public void binderDied() { + synchronized (mStagedApexObservers) { + mStagedApexObservers.remove(observer); + } + } + }, 0); + } catch (RemoteException re) { + Slog.w(TAG, re.getMessage()); + } + } + 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 * @@ -808,6 +848,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 @@ -1093,7 +1136,107 @@ public class StagingManager { return session; } - private final class PreRebootVerificationHandler extends Handler { + /** + * Returns ApexInfo about APEX contained inside the session as a {@code Map<String, ApexInfo>}, + * where the key of the map is the module name of the ApexInfo. + * + * Returns an empty map if there is any error. + */ + @VisibleForTesting + @NonNull + Map<String, ApexInfo> getStagedApexInfos(@NonNull StagedSession session) { + Preconditions.checkArgument(session != null, "Session is null"); + Preconditions.checkArgument(!session.hasParentSessionId(), + session.sessionId() + " session has parent session"); + Preconditions.checkArgument(session.containsApexSession(), + session.sessionId() + " session does not contain apex"); + + // Even if caller calls this method on ready session, the session could be abandoned + // right after this method is called. + if (!session.isSessionReady() || session.isDestroyed()) { + return Collections.emptyMap(); + } + + ApexSessionParams params = new ApexSessionParams(); + params.sessionId = session.sessionId(); + final IntArray childSessionIds = new IntArray(); + if (session.isMultiPackage()) { + for (StagedSession s : session.getChildSessions()) { + if (s.isApexSession()) { + childSessionIds.add(s.sessionId()); + } + } + } + params.childSessionIds = childSessionIds.toArray(); + + ApexInfo[] infos = mApexManager.getStagedApexInfos(params); + Map<String, ApexInfo> result = new ArrayMap<>(); + for (ApexInfo info : infos) { + result.put(info.moduleName, info); + } + return result; + } + + /** + * Returns apex module names of all packages that are staged ready + */ + List<String> getStagedApexModuleNames() { + List<String> result = new ArrayList<>(); + synchronized (mStagedSessions) { + for (int i = 0; i < mStagedSessions.size(); i++) { + final StagedSession session = mStagedSessions.valueAt(i); + if (!session.isSessionReady() || session.isDestroyed() + || session.hasParentSessionId() || !session.containsApexSession()) { + continue; + } + result.addAll(getStagedApexInfos(session).keySet()); + } + } + return result; + } + + /** + * Returns ApexInfo of the {@code moduleInfo} provided if it is staged, otherwise returns null. + */ + @Nullable + StagedApexInfo getStagedApexInfo(String moduleName) { + synchronized (mStagedSessions) { + for (int i = 0; i < mStagedSessions.size(); i++) { + final StagedSession session = mStagedSessions.valueAt(i); + if (!session.isSessionReady() || session.isDestroyed() + || session.hasParentSessionId() || !session.containsApexSession()) { + continue; + } + ApexInfo ai = getStagedApexInfos(session).get(moduleName); + if (ai != null) { + StagedApexInfo info = new StagedApexInfo(); + info.moduleName = ai.moduleName; + info.diskImagePath = ai.modulePath; + info.versionCode = ai.versionCode; + info.versionName = ai.versionName; + return info; + } + } + } + return null; + } + + 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); @@ -1114,7 +1257,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) { @@ -1314,6 +1458,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 ba5a58ffc22d..b60e0c38cf7a 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java @@ -18,6 +18,7 @@ package com.android.server.pm; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; @@ -30,17 +31,23 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.testng.Assert.assertThrows; +import android.apex.ApexInfo; 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.content.pm.StagedApexInfo; +import android.os.Message; import android.os.SystemProperties; import android.os.storage.IStorageManager; import android.platform.test.annotations.Presubmit; +import android.util.IntArray; import android.util.SparseArray; import com.android.dx.mockito.inline.extended.ExtendedMockito; @@ -55,15 +62,20 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; 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; import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.function.Predicate; @Presubmit @@ -527,6 +539,281 @@ public class StagingManagerTest { assertThat(mStagingManager.getSessionIdByPackageName("com.bar")).isEqualTo(-1); } + @Test + public void getStagedApexInfos_validatePreConditions() throws Exception { + // Invalid session: null session + { + // Call and verify + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> mStagingManager.getStagedApexInfos(null)); + assertThat(thrown).hasMessageThat().contains("Session is null"); + } + // Invalid session: has parent + { + FakeStagedSession session = new FakeStagedSession(241); + session.setParentSessionId(239); + session.setSessionReady(); + // Call and verify + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> mStagingManager.getStagedApexInfos(session)); + assertThat(thrown).hasMessageThat().contains("241 session has parent"); + } + + // Invalid session: does not contain apex + { + FakeStagedSession session = new FakeStagedSession(241); + session.setSessionReady(); + // Call and verify + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> mStagingManager.getStagedApexInfos(session)); + assertThat(thrown).hasMessageThat().contains("241 session does not contain apex"); + } + // Invalid session: not ready + { + FakeStagedSession session = new FakeStagedSession(239); + session.setIsApex(true); + // Call and verify + Map<String, ApexInfo> result = mStagingManager.getStagedApexInfos(session); + assertThat(result).isEmpty(); + } + // Invalid session: destroyed + { + FakeStagedSession session = new FakeStagedSession(240); + session.setSessionReady(); + session.setIsApex(true); + session.setDestroyed(true); + // Call and verify + Map<String, ApexInfo> result = mStagingManager.getStagedApexInfos(session); + assertThat(result).isEmpty(); + } + } + + private ApexInfo[] getFakeApexInfo(List<String> moduleNames) { + List<ApexInfo> result = new ArrayList<>(); + for (String moduleName : moduleNames) { + ApexInfo info = new ApexInfo(); + info.moduleName = moduleName; + result.add(info); + } + return result.toArray(new ApexInfo[0]); + } + + @Test + public void getStagedApexInfos_nonParentSession() throws Exception { + FakeStagedSession validSession = new FakeStagedSession(239); + validSession.setIsApex(true); + validSession.setSessionReady(); + ApexInfo[] fakeApexInfos = getFakeApexInfo(Arrays.asList("module1")); + when(mApexManager.getStagedApexInfos(any())).thenReturn(fakeApexInfos); + + // Call and verify + Map<String, ApexInfo> result = mStagingManager.getStagedApexInfos(validSession); + assertThat(result).containsExactly(fakeApexInfos[0].moduleName, fakeApexInfos[0]); + + ArgumentCaptor<ApexSessionParams> argumentCaptor = + ArgumentCaptor.forClass(ApexSessionParams.class); + verify(mApexManager, times(1)).getStagedApexInfos(argumentCaptor.capture()); + ApexSessionParams params = argumentCaptor.getValue(); + assertThat(params.sessionId).isEqualTo(239); + } + + @Test + public void getStagedApexInfos_parentSession() throws Exception { + FakeStagedSession childSession1 = new FakeStagedSession(201); + childSession1.setIsApex(true); + FakeStagedSession childSession2 = new FakeStagedSession(202); + childSession2.setIsApex(true); + FakeStagedSession nonApexChild = new FakeStagedSession(203); + FakeStagedSession parentSession = new FakeStagedSession(239, + Arrays.asList(childSession1, childSession2, nonApexChild)); + parentSession.setSessionReady(); + ApexInfo[] fakeApexInfos = getFakeApexInfo(Arrays.asList("module1", "module2")); + when(mApexManager.getStagedApexInfos(any())).thenReturn(fakeApexInfos); + + // Call and verify + Map<String, ApexInfo> result = mStagingManager.getStagedApexInfos(parentSession); + assertThat(result).containsExactly(fakeApexInfos[0].moduleName, fakeApexInfos[0], + fakeApexInfos[1].moduleName, fakeApexInfos[1]); + + ArgumentCaptor<ApexSessionParams> argumentCaptor = + ArgumentCaptor.forClass(ApexSessionParams.class); + verify(mApexManager, times(1)).getStagedApexInfos(argumentCaptor.capture()); + ApexSessionParams params = argumentCaptor.getValue(); + assertThat(params.sessionId).isEqualTo(239); + assertThat(params.childSessionIds).asList().containsExactly(201, 202); + } + + @Test + public void getStagedApexModuleNames_returnsStagedApexModules() throws Exception { + FakeStagedSession validSession1 = new FakeStagedSession(239); + validSession1.setIsApex(true); + validSession1.setSessionReady(); + mStagingManager.createSession(validSession1); + + FakeStagedSession childSession1 = new FakeStagedSession(123); + childSession1.setIsApex(true); + FakeStagedSession childSession2 = new FakeStagedSession(124); + childSession2.setIsApex(true); + FakeStagedSession nonApexChild = new FakeStagedSession(125); + FakeStagedSession parentSession = new FakeStagedSession(240, + Arrays.asList(childSession1, childSession2, nonApexChild)); + parentSession.setSessionReady(); + mStagingManager.createSession(parentSession); + + mockApexManagerGetStagedApexInfoWithSessionId(); + + List<String> 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<ApexInfo[]>() { + @Override + public ApexInfo[] answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + ApexSessionParams params = (ApexSessionParams) args[0]; + IntArray sessionsToProcess = new IntArray(); + if (params.childSessionIds.length == 0) { + sessionsToProcess.add(params.sessionId); + } else { + sessionsToProcess.addAll(params.childSessionIds); + } + List<ApexInfo> result = new ArrayList<>(); + for (int session : sessionsToProcess.toArray()) { + ApexInfo info = new ApexInfo(); + info.moduleName = String.valueOf(session); + result.add(info); + } + return result.toArray(new ApexInfo[0]); + } + }); + } + + @Test + public void getStagedApexInfo() throws Exception { + FakeStagedSession validSession1 = new FakeStagedSession(239); + validSession1.setIsApex(true); + validSession1.setSessionReady(); + mStagingManager.createSession(validSession1); + ApexInfo[] fakeApexInfos = getFakeApexInfo(Arrays.asList("module1")); + when(mApexManager.getStagedApexInfos(any())).thenReturn(fakeApexInfos); + + // Verify null is returned if module name is not found + StagedApexInfo result = mStagingManager.getStagedApexInfo("not found"); + assertThat(result).isNull(); + verify(mApexManager, times(1)).getStagedApexInfos(any()); + // Otherwise, the correct object is returned + result = mStagingManager.getStagedApexInfo("module1"); + assertThat(result.moduleName).isEqualTo(fakeApexInfos[0].moduleName); + assertThat(result.diskImagePath).isEqualTo(fakeApexInfos[0].modulePath); + assertThat(result.versionCode).isEqualTo(fakeApexInfos[0].versionCode); + assertThat(result.versionName).isEqualTo(fakeApexInfos[0].versionName); + 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<ApexStagedEvent> 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<ApexStagedEvent> 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<ApexStagedEvent> 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( @@ -591,10 +878,16 @@ public class StagingManagerTest { private String mPackageName; private boolean mIsAbandonded = false; private boolean mVerificationStarted = false; - private final List<StagingManager.StagedSession> mChildSessions = new ArrayList<>(); + private final List<StagingManager.StagedSession> mChildSessions; private FakeStagedSession(int sessionId) { mSessionId = sessionId; + mChildSessions = new ArrayList<>(); + } + + private FakeStagedSession(int sessionId, List<StagingManager.StagedSession> childSessions) { + mSessionId = sessionId; + mChildSessions = childSessions; } private void setParentSessionId(int parentSessionId) { @@ -777,9 +1070,7 @@ public class StagingManagerTest { } @Override - public void notifyEndPreRebootVerification() { - throw new UnsupportedOperationException(); - } + public void notifyEndPreRebootVerification() {} @Override public void verifySession() { diff --git a/tests/StagedInstallTest/Android.bp b/tests/StagedInstallTest/Android.bp index cac14a72a706..558798dd8ec5 100644 --- a/tests/StagedInstallTest/Android.bp +++ b/tests/StagedInstallTest/Android.bp @@ -37,6 +37,7 @@ android_test_helper_app { ":test.rebootless_apex_v1", ":test.rebootless_apex_v2", ], + platform_apis: true, } java_test_host { diff --git a/tests/StagedInstallTest/app/src/com/android/tests/stagedinstallinternal/StagedInstallInternalTest.java b/tests/StagedInstallTest/app/src/com/android/tests/stagedinstallinternal/StagedInstallInternalTest.java index 4684f0182d03..c610641932df 100644 --- a/tests/StagedInstallTest/app/src/com/android/tests/stagedinstallinternal/StagedInstallInternalTest.java +++ b/tests/StagedInstallTest/app/src/com/android/tests/stagedinstallinternal/StagedInstallInternalTest.java @@ -17,16 +17,27 @@ package com.android.tests.stagedinstallinternal; import static com.android.cts.install.lib.InstallUtils.getPackageInstaller; +import static com.android.cts.install.lib.InstallUtils.waitForSessionReady; import static com.android.cts.shim.lib.ShimPackage.SHIM_APEX_PACKAGE_NAME; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; + import android.Manifest; +import android.content.pm.ApexStagedEvent; import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManagerNative; +import android.content.pm.IStagedApexObserver; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; +import android.content.pm.StagedApexInfo; +import android.os.IBinder; +import android.os.ServiceManager; import androidx.test.platform.app.InstrumentationRegistry; @@ -39,6 +50,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -401,9 +414,73 @@ public class StagedInstallInternalTest { AssertionError.class, "Staged session " + sessionId + " already contains " + SHIM_APEX_PACKAGE_NAME, Install.single(APEX_V2)); + } + + @Test + public void testGetStagedModuleNames() throws Exception { + // Before staging a session + String[] result = getPackageManagerNative().getStagedApexModuleNames(); + assertThat(result).hasLength(0); + // Stage an apex + int sessionId = Install.single(APEX_V2).setStaged().commit(); + waitForSessionReady(sessionId); + result = getPackageManagerNative().getStagedApexModuleNames(); + assertThat(result).hasLength(1); + assertThat(result).isEqualTo(new String[]{SHIM_APEX_PACKAGE_NAME}); + // Abandon the session + InstallUtils.openPackageInstallerSession(sessionId).abandon(); + result = getPackageManagerNative().getStagedApexModuleNames(); + assertThat(result).hasLength(0); + } + + @Test + public void testGetStagedApexInfo() throws Exception { + // Ask for non-existing module + StagedApexInfo result = getPackageManagerNative().getStagedApexInfo("not found"); + assertThat(result).isNull(); + // Stage an apex + int sessionId = Install.single(APEX_V2).setStaged().commit(); + waitForSessionReady(sessionId); + // Query proper module name + result = getPackageManagerNative().getStagedApexInfo(SHIM_APEX_PACKAGE_NAME); + assertThat(result.moduleName).isEqualTo(SHIM_APEX_PACKAGE_NAME); + InstallUtils.openPackageInstallerSession(sessionId).abandon(); + } + public static class MockStagedApexObserver extends IStagedApexObserver.Stub { + @Override + public void onApexStaged(ApexStagedEvent event) { + assertThat(event).isNotNull(); + } } + @Test + public void testStagedApexObserver() throws Exception { + MockStagedApexObserver realObserver = new MockStagedApexObserver(); + IStagedApexObserver observer = spy(realObserver); + assertThat(observer).isNotNull(); + getPackageManagerNative().registerStagedApexObserver(observer); + + // Stage an apex and verify observer was called + int sessionId = Install.single(APEX_V2).setStaged().commit(); + waitForSessionReady(sessionId); + ArgumentCaptor<ApexStagedEvent> captor = ArgumentCaptor.forClass(ApexStagedEvent.class); + verify(observer, timeout(5000)).onApexStaged(captor.capture()); + assertThat(captor.getValue().stagedApexModuleNames).isEqualTo( + new String[] {SHIM_APEX_PACKAGE_NAME}); + + // Abandon and verify observer is called + Mockito.clearInvocations(observer); + InstallUtils.openPackageInstallerSession(sessionId).abandon(); + verify(observer, timeout(5000)).onApexStaged(captor.capture()); + assertThat(captor.getValue().stagedApexModuleNames).hasLength(0); + } + + private IPackageManagerNative getPackageManagerNative() { + IBinder binder = ServiceManager.waitForService("package_native"); + assertThat(binder).isNotNull(); + return IPackageManagerNative.Stub.asInterface(binder); + } private static void assertSessionApplied(int sessionId) { assertSessionState(sessionId, (session) -> { assertThat(session.isStagedSessionApplied()).isTrue(); diff --git a/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java b/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java index 5021009f65ae..31021031b47a 100644 --- a/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java +++ b/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java @@ -478,6 +478,21 @@ public class StagedInstallInternalTest extends BaseHostJUnit4Test { runPhase("testRebootlessUpdate_hasStagedSessionWithSameApex_fails"); } + @Test + public void testGetStagedModuleNames() throws Exception { + runPhase("testGetStagedModuleNames"); + } + + @Test + public void testGetStagedApexInfo() throws Exception { + runPhase("testGetStagedApexInfo"); + } + + @Test + public void testStagedApexObserver() throws Exception { + runPhase("testStagedApexObserver"); + } + private List<String> getStagingDirectories() throws DeviceNotAvailableException { String baseDir = "/data/app-staging"; try { |