From c8132a943723f56fb4850b96779ac39d41fec336 Mon Sep 17 00:00:00 2001 From: Samiul Islam Date: Thu, 22 Jul 2021 16:41:09 +0100 Subject: [PATCH 1/3] Open up new API in StagingManager to get information about staged APEX These APIs will be later used by PackageManagerNative service to open up the information to native clients. The current implementation is a bit wasteful since everytime the client makes a request, we ask the ApexManager to fetch us information about the staged sessions N times, where N is the number of staged sessions that are marked ready. We can optimize it by caching the result of getStagedApexInfos(session) method. This will be done in a separate CL. Bug: 187444679 Test: atest StagingManagerTest Change-Id: I76803382db26a029f6d9cabb0d9ce9a2bf9c8ea1 --- .../com/android/server/pm/StagingManager.java | 84 ++++++++ .../android/server/pm/StagingManagerTest.java | 185 +++++++++++++++++- 2 files changed, 267 insertions(+), 2 deletions(-) diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java index 9dcae3da6a31b..0a416b0484ee2 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; @@ -52,6 +53,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 +82,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; @@ -1093,6 +1097,86 @@ public class StagingManager { return session; } + /** + * Returns ApexInfo about APEX contained inside the session as a {@code Map}, + * 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 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 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 getStagedApexModuleNames() { + List 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 + ApexInfo 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 result = getStagedApexInfos(session).get(moduleName); + if (result != null) { + return result; + } + } + } + return null; + } + private final class PreRebootVerificationHandler extends Handler { PreRebootVerificationHandler(Looper looper) { 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 ba5a58ffc22d4..04764f7ff6b67 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,9 +31,10 @@ 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.PackageInstaller; @@ -41,6 +43,7 @@ import android.content.pm.PackageInstaller.SessionInfo.StagedSessionErrorCode; 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 +58,19 @@ 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.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 +534,174 @@ 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 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 result = mStagingManager.getStagedApexInfos(session); + assertThat(result).isEmpty(); + } + } + + private ApexInfo[] getFakeApexInfo(List moduleNames) { + List 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 result = mStagingManager.getStagedApexInfos(validSession); + assertThat(result).containsExactly(fakeApexInfos[0].moduleName, fakeApexInfos[0]); + + ArgumentCaptor 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 result = mStagingManager.getStagedApexInfos(parentSession); + assertThat(result).containsExactly(fakeApexInfos[0].moduleName, fakeApexInfos[0], + fakeApexInfos[1].moduleName, fakeApexInfos[1]); + + ArgumentCaptor 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); + + // Make mApexManager return ApexInfo with same module name as the sessionId + // of the parameter that was passed into it + when(mApexManager.getStagedApexInfos(any())).thenAnswer(new Answer() { + @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 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]); + } + }); + + List result = mStagingManager.getStagedApexModuleNames(); + assertThat(result).containsExactly("239", "123", "124"); + verify(mApexManager, times(2)).getStagedApexInfos(any()); + } + + @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 + ApexInfo 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).isEqualTo(fakeApexInfos[0]); + verify(mApexManager, times(2)).getStagedApexInfos(any()); + } + private StagingManager.StagedSession createSession(int sessionId, String packageName, long committedMillis) { PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( @@ -591,10 +766,16 @@ public class StagingManagerTest { private String mPackageName; private boolean mIsAbandonded = false; private boolean mVerificationStarted = false; - private final List mChildSessions = new ArrayList<>(); + private final List mChildSessions; private FakeStagedSession(int sessionId) { mSessionId = sessionId; + mChildSessions = new ArrayList<>(); + } + + private FakeStagedSession(int sessionId, List childSessions) { + mSessionId = sessionId; + mChildSessions = childSessions; } private void setParentSessionId(int parentSessionId) { From 9e7de2a0d01b6abfe28093add494322429f87a36 Mon Sep 17 00:00:00 2001 From: Samiul Islam Date: Wed, 4 Aug 2021 13:12:57 +0100 Subject: [PATCH 2/3] Notify StagedApexObservers when there is a change in set of staged APEX The set of staged APEX is changed whenever: - a session containing APEX succesfully passes pre-reboot verification and gets marked ready - a staged session gets abandoned Bug: 187444679 Test: atest StagingManagerTest Change-Id: I5c5bb4523cdab46e22fe3c8e2373289d8619c10e --- .../com/android/server/pm/StagingManager.java | 44 ++++++- .../android/server/pm/StagingManagerTest.java | 124 ++++++++++++++++-- 2 files changed, 156 insertions(+), 12 deletions(-) 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() { From 5ac0ee8278ea33a6f0bf14b96821d31576e81df4 Mon Sep 17 00:00:00 2001 From: Samiul Islam Date: Wed, 4 Aug 2021 20:05:40 +0100 Subject: [PATCH 3/3] Hook the new APIs in StagingManager to PackageManagerNative service Also note, if a StagedApexObserver is observing through binder, they might not be able to send the original observing object for unregistration. As such, for binder observer we clean them up when they die. Bug: 187444679 Test: atest StagingManagerTest Test: atest StagedInstallInternalTest Change-Id: Ie2e01b01690a5882574282f3158e454a9b6056e7 --- .../server/pm/PackageInstallerService.java | 4 + .../server/pm/PackageManagerService.java | 27 ++++++- .../com/android/server/pm/StagingManager.java | 31 +++++++- .../android/server/pm/StagingManagerTest.java | 8 +- tests/StagedInstallTest/Android.bp | 1 + .../StagedInstallInternalTest.java | 79 ++++++++++++++++++- .../host/StagedInstallInternalTest.java | 15 ++++ 7 files changed, 156 insertions(+), 9 deletions(-) diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index fe419a613b018..b34a3a2b8dcbf 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 b44e2ed3dd886..ef7cad8428d7b 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; @@ -22996,6 +22998,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) { @@ -24964,5 +24989,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 f22e0698e73f3..4dfb6b8ea4530 100644 --- a/services/core/java/com/android/server/pm/StagingManager.java +++ b/services/core/java/com/android/server/pm/StagingManager.java @@ -39,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; @@ -215,6 +216,23 @@ public class StagingManager { } 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); } @@ -1181,7 +1199,7 @@ public class StagingManager { * Returns ApexInfo of the {@code moduleInfo} provided if it is staged, otherwise returns null. */ @Nullable - ApexInfo getStagedApexInfo(String moduleName) { + StagedApexInfo getStagedApexInfo(String moduleName) { synchronized (mStagedSessions) { for (int i = 0; i < mStagedSessions.size(); i++) { final StagedSession session = mStagedSessions.valueAt(i); @@ -1189,9 +1207,14 @@ public class StagingManager { || session.hasParentSessionId() || !session.containsApexSession()) { continue; } - ApexInfo result = getStagedApexInfos(session).get(moduleName); - if (result != null) { - return result; + 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; } } } 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 5646da175f559..b60e0c38cf7a5 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java @@ -42,6 +42,7 @@ 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; @@ -701,12 +702,15 @@ public class StagingManagerTest { when(mApexManager.getStagedApexInfos(any())).thenReturn(fakeApexInfos); // Verify null is returned if module name is not found - ApexInfo result = mStagingManager.getStagedApexInfo("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).isEqualTo(fakeApexInfos[0]); + 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()); } diff --git a/tests/StagedInstallTest/Android.bp b/tests/StagedInstallTest/Android.bp index cac14a72a7061..558798dd8ec58 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 4684f0182d034..c610641932df8 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 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 5021009f65ae8..31021031b47aa 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 getStagingDirectories() throws DeviceNotAvailableException { String baseDir = "/data/app-staging"; try {