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) {