From 43d5b8b3f7c8da668f9e21d96c93f7e9dfba1b10 Mon Sep 17 00:00:00 2001 From: Samiul Islam Date: Wed, 21 Jul 2021 18:24:28 +0100 Subject: [PATCH 1/4] Connect the new API from ApexService to ApexManager Bug: 187444679 Test: atest ApexManagerTest Change-Id: I60482a180d873a5f65887d0a5bb4230f75cb55df Merged-In: I60482a180d873a5f65887d0a5bb4230f75cb55df (cherry picked from commit 2bcdaf91d59cec2b007e211bd4e0107c7ec07147) --- .../com/android/server/pm/ApexManager.java | 26 +++++++++++++++++++ .../android/server/pm/ApexManagerTest.java | 16 ++++++++++++ 2 files changed, 42 insertions(+) diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java index 378405ffdb4a2..f6f3db3d55a44 100644 --- a/services/core/java/com/android/server/pm/ApexManager.java +++ b/services/core/java/com/android/server/pm/ApexManager.java @@ -237,6 +237,14 @@ public abstract class ApexManager { abstract ApexInfoList submitStagedSession(ApexSessionParams params) throws PackageManagerException; + /** + * Returns {@code ApeInfo} about apex sessions that have been marked ready via + * {@link #markStagedSessionReady(int)} + * + * Returns empty array if there is no staged apex session or if there is any error. + */ + abstract ApexInfo[] getStagedApexInfos(ApexSessionParams params); + /** * Mark a staged session previously submitted using {@code submitStagedSession} as ready to be * applied at next reboot. @@ -706,6 +714,19 @@ public abstract class ApexManager { } } + @Override + ApexInfo[] getStagedApexInfos(ApexSessionParams params) { + try { + return waitForApexService().getStagedApexInfos(params); + } catch (RemoteException re) { + Slog.w(TAG, "Unable to contact apexservice" + re.getMessage()); + throw new RuntimeException(re); + } catch (Exception e) { + Slog.w(TAG, "Failed to collect staged apex infos" + e.getMessage()); + return new ApexInfo[0]; + } + } + @Override void markStagedSessionReady(int sessionId) throws PackageManagerException { try { @@ -1099,6 +1120,11 @@ public abstract class ApexManager { "Device doesn't support updating APEX"); } + @Override + ApexInfo[] getStagedApexInfos(ApexSessionParams params) { + throw new UnsupportedOperationException(); + } + @Override void markStagedSessionReady(int sessionId) { throw new UnsupportedOperationException(); diff --git a/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java index 72afca0300cd0..b6f79221ceadb 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/ApexManagerTest.java @@ -37,6 +37,7 @@ import android.apex.IApexService; import android.content.Context; import android.content.pm.PackageInfo; import android.os.RemoteException; +import android.os.ServiceSpecificException; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; @@ -227,6 +228,21 @@ public class ApexManagerTest { () -> mApexManager.submitStagedSession(testParamsWithChildren())); } + @Test + public void testGetStagedApexInfos_throwRunTimeException() throws RemoteException { + doThrow(RemoteException.class).when(mApexService).getStagedApexInfos(any()); + + assertThrows(RuntimeException.class, + () -> mApexManager.getStagedApexInfos(testParamsWithChildren())); + } + + @Test + public void testGetStagedApexInfos_returnsEmptyArrayOnError() throws RemoteException { + doThrow(ServiceSpecificException.class).when(mApexService).getStagedApexInfos(any()); + + assertThat(mApexManager.getStagedApexInfos(testParamsWithChildren())).hasLength(0); + } + @Test public void testMarkStagedSessionReady_throwPackageManagerException() throws RemoteException { doAnswer(invocation -> { From 9a68bedca7ab3c21f7418f88a290f54e354d3e3f Mon Sep 17 00:00:00 2001 From: Samiul Islam Date: Thu, 22 Jul 2021 16:41:09 +0100 Subject: [PATCH 2/4] 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. (Note: Unit tests have been removed to resolve cherry-pick conflict) Bug: 187444679 Test: atest StagedInstallInternalTests Change-Id: I76803382db26a029f6d9cabb0d9ce9a2bf9c8ea1 Merged-In: I76803382db26a029f6d9cabb0d9ce9a2bf9c8ea1 (cherry picked from commit c8132a943723f56fb4850b96779ac39d41fec336) --- .../com/android/server/pm/StagingManager.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java index 4038bf2901610..f1e300891eeb3 100644 --- a/services/core/java/com/android/server/pm/StagingManager.java +++ b/services/core/java/com/android/server/pm/StagingManager.java @@ -58,6 +58,7 @@ import android.os.UserManagerInternal; import android.os.storage.IStorageManager; import android.os.storage.StorageManager; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.IntArray; import android.util.Slog; import android.util.SparseArray; @@ -66,8 +67,10 @@ import android.util.SparseIntArray; import android.util.apk.ApkSignatureVerifier; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageHelper; import com.android.internal.os.BackgroundThread; +import com.android.internal.util.Preconditions; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.SystemServiceManager; @@ -84,7 +87,9 @@ import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -1299,6 +1304,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 PackageInstallerSession session) { + Preconditions.checkArgument(session != null, "Session is null"); + Preconditions.checkArgument(!session.hasParentSessionId(), + session.sessionId + " session has parent session"); + Preconditions.checkArgument(sessionContainsApex(session), + 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.isStagedSessionReady() || session.isDestroyed()) { + return Collections.emptyMap(); + } + + ApexSessionParams params = new ApexSessionParams(); + params.sessionId = session.sessionId; + final IntArray childSessionIds = new IntArray(); + if (session.isMultiPackage()) { + for (int id : session.getChildSessionIds()) { + if (isApexSession(getStagedSession(id))) { + childSessionIds.add(id); + } + } + } + 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 PackageInstallerSession session = mStagedSessions.valueAt(i); + if (!session.isStagedSessionReady() || session.isDestroyed() + || session.hasParentSessionId() || !sessionContainsApex(session)) { + 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 PackageInstallerSession session = mStagedSessions.valueAt(i); + if (!session.isStagedSessionReady() || session.isDestroyed() + || session.hasParentSessionId() || !sessionContainsApex(session)) { + continue; + } + ApexInfo result = getStagedApexInfos(session).get(moduleName); + if (result != null) { + return result; + } + } + } + return null; + } + private final class PreRebootVerificationHandler extends Handler { // Hold session ids before handler gets ready to do the verification. private IntArray mPendingSessionIds; From 7675b5c15405392c2d279b10d79506ab8a9d919b Mon Sep 17 00:00:00 2001 From: Samiul Islam Date: Wed, 4 Aug 2021 13:12:57 +0100 Subject: [PATCH 3/4] 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 (Note: Unit tests have been removed to resolve cherry-pick conflict) Bug: 187444679 Test: atest StagedInstallInternalTests Change-Id: I5c5bb4523cdab46e22fe3c8e2373289d8619c10e Merged-In: I5c5bb4523cdab46e22fe3c8e2373289d8619c10e --- .../com/android/server/pm/StagingManager.java | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java index f1e300891eeb3..b531ed1b6c43b 100644 --- a/services/core/java/com/android/server/pm/StagingManager.java +++ b/services/core/java/com/android/server/pm/StagingManager.java @@ -29,7 +29,9 @@ 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.ApplicationInfo; +import android.content.pm.IStagedApexObserver; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; @@ -108,7 +110,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"); @@ -127,6 +130,9 @@ public class StagingManager { @GuardedBy("mSuccessfulStagedSessionIds") private final List mSuccessfulStagedSessionIds = new ArrayList<>(); + @GuardedBy("mStagedApexObservers") + private final List mStagedApexObservers = new ArrayList<>(); + StagingManager(PackageInstallerService pi, Context context, Supplier packageParserSupplier) { mPi = pi; @@ -189,6 +195,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 * @@ -1080,6 +1098,9 @@ public class StagingManager { Slog.w(TAG, "Could not contact apexd to abort staged session " + sessionId); } } + if (sessionContainsApex(session)) { + notifyStagedApexObservers(); + } } // Session was successfully aborted from apexd (if required) and pre-reboot verification @@ -1384,7 +1405,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 { // Hold session ids before handler gets ready to do the verification. private IntArray mPendingSessionIds; private boolean mIsReady; @@ -1412,7 +1448,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_APK = 3; - private static final int MSG_PRE_REBOOT_VERIFICATION_END = 4; + @VisibleForTesting + static final int MSG_PRE_REBOOT_VERIFICATION_END = 4; @Override public void handleMessage(Message msg) { @@ -1664,6 +1701,7 @@ public class StagingManager { if (hasApex) { try { mApexManager.markStagedSessionReady(session.sessionId); + notifyStagedApexObservers(); } catch (PackageManagerException e) { session.setStagedSessionFailed(e.error, e.getMessage()); return; From 33dce2cffd06651a4ab6def32f18e47043f52599 Mon Sep 17 00:00:00 2001 From: Samiul Islam Date: Wed, 4 Aug 2021 20:05:40 +0100 Subject: [PATCH 4/4] 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. (Note: Unit tests have been removed to resolve merge conflict) Bug: 187444679 Test: atest StagedInstallInternalTest Change-Id: Ie2e01b01690a5882574282f3158e454a9b6056e7 Merged-In: Ie2e01b01690a5882574282f3158e454a9b6056e7 (cherry picked from commit 5ac0ee8278ea33a6f0bf14b96821d31576e81df4) --- .../server/pm/PackageInstallerService.java | 4 + .../server/pm/PackageManagerService.java | 28 ++++++- .../com/android/server/pm/StagingManager.java | 31 ++++++- tests/StagedInstallTest/Android.bp | 14 +++- .../StagedInstallInternalTest.java | 84 +++++++++++++++++++ .../host/StagedInstallInternalTest.java | 15 ++++ 6 files changed, 166 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 f8115d39b375f..0b07bb6f408e9 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -224,6 +224,10 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements mStagingManager = new StagingManager(this, context, apexParserSupplier); } + 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 e4723adabe71f..1173df6297962 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -21,14 +21,12 @@ import static android.Manifest.permission.INSTALL_PACKAGES; import static android.Manifest.permission.MANAGE_DEVICE_ADMINS; import static android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS; import static android.Manifest.permission.QUERY_ALL_PACKAGES; -import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.REQUEST_DELETE_PACKAGES; import static android.Manifest.permission.SET_HARMFUL_APP_WARNINGS; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_DEFAULT; import static android.app.AppOpsManager.MODE_IGNORED; import static android.content.Intent.ACTION_MAIN; -import static android.content.Intent.CATEGORY_BROWSABLE; import static android.content.Intent.CATEGORY_DEFAULT; import static android.content.Intent.CATEGORY_HOME; import static android.content.Intent.EXTRA_LONG_VERSION_CODE; @@ -36,8 +34,6 @@ import static android.content.Intent.EXTRA_PACKAGE_NAME; import static android.content.Intent.EXTRA_VERSION_CODE; import static android.content.pm.PackageManager.CERT_INPUT_RAW_X509; import static android.content.pm.PackageManager.CERT_INPUT_SHA256; -import static android.content.Intent.CATEGORY_BROWSABLE; -import static android.content.Intent.CATEGORY_DEFAULT; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED; @@ -181,6 +177,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.InstallSourceInfo; import android.content.pm.InstantAppInfo; import android.content.pm.InstantAppRequest; @@ -220,6 +217,7 @@ import android.content.pm.ServiceInfo; import android.content.pm.SharedLibraryInfo; import android.content.pm.Signature; import android.content.pm.SigningInfo; +import android.content.pm.StagedApexInfo; import android.content.pm.SuspendDialogInfo; import android.content.pm.UserInfo; import android.content.pm.VerifierDeviceIdentity; @@ -24119,6 +24117,28 @@ public class PackageManagerService extends IPackageManager.Stub public String getModuleMetadataPackageName() throws RemoteException { return PackageManagerService.this.mModuleInfoProvider.getPackageName(); } + + @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 class PackageManagerInternalImpl extends PackageManagerInternal { diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java index b531ed1b6c43b..02397daebb16e 100644 --- a/services/core/java/com/android/server/pm/StagingManager.java +++ b/services/core/java/com/android/server/pm/StagingManager.java @@ -40,6 +40,7 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.PackageParser.PackageParserException; import android.content.pm.PackageParser.SigningDetails; import android.content.pm.PackageParser.SigningDetails.SignatureSchemeVersion; +import android.content.pm.StagedApexInfo; import android.content.pm.parsing.PackageInfoWithoutStateUtils; import android.content.rollback.IRollbackManager; import android.content.rollback.RollbackInfo; @@ -196,6 +197,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); } @@ -1388,7 +1406,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 PackageInstallerSession session = mStagedSessions.valueAt(i); @@ -1396,9 +1414,14 @@ public class StagingManager { || session.hasParentSessionId() || !sessionContainsApex(session)) { 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/tests/StagedInstallTest/Android.bp b/tests/StagedInstallTest/Android.bp index 243c3015b5594..840a588bfe884 100644 --- a/tests/StagedInstallTest/Android.bp +++ b/tests/StagedInstallTest/Android.bp @@ -25,14 +25,24 @@ android_test_helper_app { name: "StagedInstallInternalTestApp", manifest: "app/AndroidManifest.xml", srcs: ["app/src/**/*.java"], - static_libs: ["androidx.test.rules", "cts-install-lib"], + static_libs: [ + "androidx.test.rules", + "cts-install-lib", + ], test_suites: ["general-tests"], + java_resources: [ + ":StagedInstallTestApexV2", + ], + platform_apis: true, } java_test_host { name: "StagedInstallInternalTest", srcs: ["src/**/*.java"], - libs: ["tradefed", "cts-shim-host-lib"], + libs: [ + "tradefed", + "cts-shim-host-lib", + ], static_libs: [ "testng", "compatibility-tradefed", 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 e67354982b05d..a0f4e0ad7e6a9 100644 --- a/tests/StagedInstallTest/app/src/com/android/tests/stagedinstallinternal/StagedInstallInternalTest.java +++ b/tests/StagedInstallTest/app/src/com/android/tests/stagedinstallinternal/StagedInstallInternalTest.java @@ -17,12 +17,24 @@ 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.IPackageManagerNative; +import android.content.pm.IStagedApexObserver; import android.content.pm.PackageInstaller; +import android.content.pm.StagedApexInfo; +import android.os.IBinder; +import android.os.ServiceManager; import androidx.test.platform.app.InstrumentationRegistry; @@ -35,6 +47,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; @@ -50,6 +64,10 @@ public class StagedInstallInternalTest { private static final String TAG = StagedInstallInternalTest.class.getSimpleName(); + private static final TestApp APEX_V2 = new TestApp( + "ApexV2", SHIM_APEX_PACKAGE_NAME, 2, /* isApex= */ true, + "com.android.apex.cts.shim.v2.apex"); + private File mTestStateFile = new File( InstrumentationRegistry.getInstrumentation().getContext().getFilesDir(), "stagedinstall_state"); @@ -109,6 +127,72 @@ public class StagedInstallInternalTest { Install.multi(TestApp.AIncompleteSplit, TestApp.B1, TestApp.Apex1).setStaged()); } + @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 assertSessionReady(int sessionId) { assertSessionState(sessionId, (session) -> assertThat(session.isStagedSessionReady()).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 dddb317f31e53..f92c31c0ba6a2 100644 --- a/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java +++ b/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java @@ -216,6 +216,21 @@ public class StagedInstallInternalTest extends BaseHostJUnit4Test { assertThat(getStagingDirectories()).isEmpty(); } + @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 {