From 9f80efac3932e5c45224c2067e6045cd71a28a44 Mon Sep 17 00:00:00 2001 From: Samiul Islam Date: Thu, 22 Jul 2021 16:41:09 +0100 Subject: [PATCH] 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 Merged-In: I76803382db26a029f6d9cabb0d9ce9a2bf9c8ea1 (cherry picked from commit c8132a943723f56fb4850b96779ac39d41fec336) --- .../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 bdbcb277e8d66..ce40d95189ceb 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.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -1134,6 +1138,86 @@ public class StagingManager { mPreRebootVerificationHandler.notifyPreRebootVerification_Apk_Complete(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 { // Hold sessions before handler gets ready to do the verification. private List mPendingSessions; 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 521be70df633f..8dca89d76f20d 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.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -28,9 +29,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; @@ -39,6 +41,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; @@ -53,15 +56,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 @@ -519,6 +526,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( @@ -582,10 +757,16 @@ public class StagingManagerTest { private String mPackageName; private boolean mIsAbandonded = false; private boolean mPreRebootVerificationStarted = 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) {