diff options
| author | 2022-08-31 17:50:06 +0000 | |
|---|---|---|
| committer | 2022-09-29 21:34:17 +0000 | |
| commit | c25df89ca763312978f404cd7cecf43d24bc8ab2 (patch) | |
| tree | 0186208c49d8256718f3928d3ce8ec44bd27981a | |
| parent | 677d18848ecbfe6301ab1661de695ac3737fa14d (diff) | |
Add PackageManagerLocal snapshot APIs
Exposes 2 separate types, one for unfiltered, another for filtered,
for the package data snapshot APIs.
Unfiltered can be reused for multiple callers by calling filtered()
on it.
Each type exposes the most performant variant of the API available.
Unfiltered offers the entire PackageState map as-is, as it doesn't
require any filtering or copying.
The filtered variant offers getPackageState which will do filtering
at call time, or forAllPackageStates if someone really needs all
of them. The hope is that this method naming causes the caller to
only invoke it if the majority of states are actually needed.
Bug: 235462722
Test: atest PackageManagerLocalSnapshotTest
Change-Id: Ib0bf4f05f12cace681020015d099866b3740dbfe
6 files changed, 514 insertions, 26 deletions
diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java index 591619698295..15cd63927648 100644 --- a/services/core/java/com/android/server/pm/Computer.java +++ b/services/core/java/com/android/server/pm/Computer.java @@ -187,6 +187,8 @@ public interface Computer extends PackageDataSnapshot { PackageStateInternal getPackageStateInternal(String packageName); PackageStateInternal getPackageStateInternal(String packageName, int callingUid); + PackageStateInternal getPackageStateFiltered(@NonNull String packageName, int callingUid, + @UserIdInt int userId); ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId); ResolveInfo createForwardingResolveInfoUnchecked(WatchedIntentFilter filter, int sourceUserId, int targetUserId); diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 50bb05161ffd..da6f042ca462 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -1816,6 +1816,19 @@ public class ComputerEngine implements Computer { return mSettings.getPackage(packageName); } + @Override + public PackageStateInternal getPackageStateFiltered(@NonNull String packageName, + int callingUid, @UserIdInt int userId) { + packageName = resolveInternalPackageNameInternalLocked( + packageName, PackageManager.VERSION_CODE_HIGHEST, callingUid); + var packageState = mSettings.getPackage(packageName); + if (shouldFilterApplication(packageState, callingUid, userId)) { + return null; + } else { + return packageState; + } + } + public final ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId) { final int callingUid = Binder.getCallingUid(); if (getInstantAppPackageName(callingUid) != null) { diff --git a/services/core/java/com/android/server/pm/PackageManagerLocal.java b/services/core/java/com/android/server/pm/PackageManagerLocal.java index 39cc37ea2e7c..c9b48bf3c2a1 100644 --- a/services/core/java/com/android/server/pm/PackageManagerLocal.java +++ b/services/core/java/com/android/server/pm/PackageManagerLocal.java @@ -20,11 +20,17 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; +import android.os.Binder; +import android.os.UserHandle; + +import com.android.server.pm.pkg.PackageState; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; +import java.util.Map; +import java.util.function.Consumer; /** * In-process API for server side PackageManager related infrastructure. @@ -57,10 +63,10 @@ public interface PackageManagerLocal { FLAG_STORAGE_CE, }) @Retention(RetentionPolicy.SOURCE) - public @interface StorageFlags {} + @interface StorageFlags {} /** - * Reconcile sdk data sub-directories for the given {@code packagName}. + * Reconcile sdk data sub-directories for the given {@code packageName}. * * Sub directories are created if they do not exist already. If there is an existing per- * sdk directory that is missing from {@code subDirNames}, then it is removed. @@ -80,4 +86,100 @@ public interface PackageManagerLocal { void reconcileSdkData(@Nullable String volumeUuid, @NonNull String packageName, @NonNull List<String> subDirNames, int userId, int appId, int previousAppId, @NonNull String seInfo, @StorageFlags int flags) throws IOException; + + /** + * Provides a snapshot scoped class to access snapshot-aware APIs. Should be short-term use and + * closed as soon as possible. + * <p/> + * All reachable types in the snapshot are read-only. + * <p/> + * The snapshot assumes the caller is acting on behalf of the system and will not filter any + * results. + * + * @hide + */ + @NonNull + UnfilteredSnapshot withUnfilteredSnapshot(); + + /** + * {@link #withFilteredSnapshot(int, UserHandle)} that infers the UID and user from the + * caller through {@link Binder#getCallingUid()} and {@link Binder#getCallingUserHandle()}. + * + * @see #withFilteredSnapshot(int, UserHandle) + * @hide + */ + @NonNull + FilteredSnapshot withFilteredSnapshot(); + + /** + * Provides a snapshot scoped class to access snapshot-aware APIs. Should be short-term use and + * closed as soon as possible. + * <p/> + * All reachable types in the snapshot are read-only. + * + * @param callingUid The caller UID to filter results based on. This includes package visibility + * and permissions, including cross-user enforcement. + * @param user The user to query as, should usually be the user that the caller was + * invoked from. + * @hide + */ + @SuppressWarnings("UserHandleName") // Ignore naming convention, not invoking action as user + @NonNull + FilteredSnapshot withFilteredSnapshot(int callingUid, @NonNull UserHandle user); + + /** + * @hide + */ + interface UnfilteredSnapshot extends AutoCloseable { + + /** + * Allows re-use of this snapshot, but in a filtered context. This allows a caller to invoke + * itself as multiple other actual callers without having to re-take a snapshot. + * <p/> + * Note that closing the parent snapshot closes any filtered children generated from it. + * + * @return An isolated instance of {@link FilteredSnapshot} which can be closed without + * affecting this parent snapshot or any sibling snapshots. + */ + @SuppressWarnings("UserHandleName") // Ignore naming convention, not invoking action as user + @NonNull + FilteredSnapshot filtered(int callingUid, @NonNull UserHandle user); + + /** + * Returns a map of all {@link PackageState PackageStates} on the device. + * + * @return Mapping of package name to {@link PackageState}. + */ + @NonNull + Map<String, PackageState> getPackageStates(); + + @Override + void close(); + } + + /** + * @hide + */ + interface FilteredSnapshot extends AutoCloseable { + + /** + * @return {@link PackageState} for the {@code packageName}, filtered if applicable. + */ + @Nullable + PackageState getPackageState(@NonNull String packageName); + + /** + * Iterates on all states. This should only be used when either the target package name + * is not known or the large majority of the states are expected to be used. + * + * This will cause app visibility filtering to be invoked on each state on the device, + * which can be expensive. + * + * @param consumer Block to accept each state as it becomes available post-filtering. + */ + void forAllPackageStates(@NonNull Consumer<PackageState> consumer); + + @Override + void close(); + } } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 2c460f858d36..d939ca61fa08 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -200,6 +200,7 @@ import com.android.server.pm.dex.ArtManagerService; import com.android.server.pm.dex.ArtUtils; import com.android.server.pm.dex.DexManager; import com.android.server.pm.dex.ViewCompiler; +import com.android.server.pm.local.PackageManagerLocalImpl; import com.android.server.pm.parsing.PackageInfoUtils; import com.android.server.pm.parsing.PackageParser2; import com.android.server.pm.parsing.pkg.AndroidPackageInternal; @@ -1071,10 +1072,27 @@ public class PackageManagerService implements PackageSender, TestUtilityService @VisibleForTesting(visibility = Visibility.PACKAGE) @NonNull public Computer snapshotComputer() { - if (Thread.holdsLock(mLock)) { - // If the current thread holds mLock then it may have modified state but not - // yet invalidated the snapshot. Always give the thread the live computer. - return mLiveComputer; + return snapshotComputer(true /*allowLiveComputer*/); + } + + /** + * This method should only ever be called from {@link PackageManagerLocal#snapshot()}. + * + * @param allowLiveComputer Whether to allow a live computer instance based on caller {@link + * #mLock} hold state. In certain cases, like for {@link + * PackageManagerLocal} API, it must be enforced that the caller gets + * a snapshot at time, and never the live variant. + * @deprecated Use {@link #snapshotComputer()} + */ + @Deprecated + @NonNull + public Computer snapshotComputer(boolean allowLiveComputer) { + if (allowLiveComputer) { + if (Thread.holdsLock(mLock)) { + // If the current thread holds mLock then it may have modified state but not + // yet invalidated the snapshot. Always give the thread the live computer. + return mLiveComputer; + } } var oldSnapshot = sSnapshot.get(); @@ -1531,7 +1549,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService ServiceManager.addService("package", iPackageManager); final PackageManagerNative pmn = new PackageManagerNative(m); ServiceManager.addService("package_native", pmn); - LocalManagerRegistry.addManager(PackageManagerLocal.class, m.new PackageManagerLocalImpl()); + LocalManagerRegistry.addManager(PackageManagerLocal.class, + new PackageManagerLocalImpl(m)); return Pair.create(m, iPackageManager); } @@ -6017,25 +6036,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService } } - private class PackageManagerLocalImpl implements PackageManagerLocal { - @Override - public void reconcileSdkData(@Nullable String volumeUuid, @NonNull String packageName, - @NonNull List<String> subDirNames, int userId, int appId, int previousAppId, - @NonNull String seInfo, int flags) throws IOException { - synchronized (mInstallLock) { - ReconcileSdkDataArgs args = mInstaller.buildReconcileSdkDataArgs(volumeUuid, - packageName, subDirNames, userId, appId, seInfo, - flags); - args.previousAppId = previousAppId; - try { - mInstaller.reconcileSdkData(args); - } catch (InstallerException e) { - throw new IOException(e.getMessage()); - } - } - } - } - private class PackageManagerInternalImpl extends PackageManagerInternalBase { public PackageManagerInternalImpl() { @@ -7296,4 +7296,20 @@ public class PackageManagerService implements PackageSender, TestUtilityService mSettings.addInstallerPackageNames(installSource); } } + + public void reconcileSdkData(@Nullable String volumeUuid, @NonNull String packageName, + @NonNull List<String> subDirNames, int userId, int appId, int previousAppId, + @NonNull String seInfo, int flags) throws IOException { + synchronized (mInstallLock) { + ReconcileSdkDataArgs args = mInstaller.buildReconcileSdkDataArgs(volumeUuid, + packageName, subDirNames, userId, appId, seInfo, + flags); + args.previousAppId = previousAppId; + try { + mInstaller.reconcileSdkData(args); + } catch (InstallerException e) { + throw new IOException(e.getMessage()); + } + } + } } diff --git a/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java b/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java new file mode 100644 index 000000000000..00c8f84b340b --- /dev/null +++ b/services/core/java/com/android/server/pm/local/PackageManagerLocalImpl.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm.local; + +import android.annotation.CallSuper; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.os.Binder; +import android.os.UserHandle; + +import com.android.server.pm.Computer; +import com.android.server.pm.PackageManagerLocal; +import com.android.server.pm.PackageManagerService; +import com.android.server.pm.pkg.PackageState; +import com.android.server.pm.snapshot.PackageDataSnapshot; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** @hide */ +public class PackageManagerLocalImpl implements PackageManagerLocal { + + private final PackageManagerService mService; + + public PackageManagerLocalImpl(PackageManagerService service) { + mService = service; + } + + @Override + public void reconcileSdkData(@Nullable String volumeUuid, @NonNull String packageName, + @NonNull List<String> subDirNames, int userId, int appId, int previousAppId, + @NonNull String seInfo, int flags) throws IOException { + mService.reconcileSdkData(volumeUuid, packageName, subDirNames, userId, appId, + previousAppId, seInfo, flags); + } + + @NonNull + @Override + public UnfilteredSnapshotImpl withUnfilteredSnapshot() { + return new UnfilteredSnapshotImpl(mService.snapshotComputer(false /*allowLiveComputer*/)); + } + + @NonNull + @Override + public FilteredSnapshotImpl withFilteredSnapshot() { + return withFilteredSnapshot(Binder.getCallingUid(), Binder.getCallingUserHandle()); + } + + @NonNull + @Override + public FilteredSnapshotImpl withFilteredSnapshot(int callingUid, @NonNull UserHandle user) { + return new FilteredSnapshotImpl(callingUid, user, + mService.snapshotComputer(false /*allowLiveComputer*/), null); + } + + private abstract static class BaseSnapshotImpl implements AutoCloseable { + + private boolean mClosed; + + @NonNull + protected Computer mSnapshot; + + private BaseSnapshotImpl(@NonNull PackageDataSnapshot snapshot) { + mSnapshot = (Computer) snapshot; + } + + @Override + public void close() { + mClosed = true; + mSnapshot = null; + // TODO: Recycle snapshots? + } + + @CallSuper + protected void checkClosed() { + if (mClosed) { + throw new IllegalStateException("Snapshot already closed"); + } + } + } + + private static class UnfilteredSnapshotImpl extends BaseSnapshotImpl implements + UnfilteredSnapshot { + + private UnfilteredSnapshotImpl(@NonNull PackageDataSnapshot snapshot) { + super(snapshot); + } + + @Override + public FilteredSnapshot filtered(int callingUid, @NonNull UserHandle user) { + return new FilteredSnapshotImpl(callingUid, user, mSnapshot, this); + } + + @SuppressWarnings("RedundantSuppression") + @NonNull + @Override + public Map<String, PackageState> getPackageStates() { + checkClosed(); + + //noinspection unchecked, RedundantCast + return (Map<String, PackageState>) (Map<?, ?>) mSnapshot.getPackageStates(); + } + } + + private static class FilteredSnapshotImpl extends BaseSnapshotImpl implements + FilteredSnapshot { + + private final int mCallingUid; + + @UserIdInt + private final int mUserId; + + @Nullable + private ArrayList<PackageState> mFilteredPackageStates; + + @Nullable + private final UnfilteredSnapshotImpl mParentSnapshot; + + private FilteredSnapshotImpl(int callingUid, @NonNull UserHandle user, + @NonNull PackageDataSnapshot snapshot, + @Nullable UnfilteredSnapshotImpl parentSnapshot) { + super(snapshot); + mCallingUid = callingUid; + mUserId = user.getIdentifier(); + mParentSnapshot = parentSnapshot; + } + + @Override + protected void checkClosed() { + if (mParentSnapshot != null) { + mParentSnapshot.checkClosed(); + } + + super.checkClosed(); + } + + @Nullable + @Override + public PackageState getPackageState(@NonNull String packageName) { + checkClosed(); + return mSnapshot.getPackageStateFiltered(packageName, mCallingUid, mUserId); + } + + @Override + public void forAllPackageStates(@NonNull Consumer<PackageState> consumer) { + checkClosed(); + + if (mFilteredPackageStates == null) { + var packageStates = mSnapshot.getPackageStates(); + var filteredPackageStates = new ArrayList<PackageState>(); + for (int index = 0, size = packageStates.size(); index < size; index++) { + var packageState = packageStates.valueAt(index); + if (!mSnapshot.shouldFilterApplication(packageState, mCallingUid, mUserId)) { + filteredPackageStates.add(packageState); + } + } + mFilteredPackageStates = filteredPackageStates; + } + + for (int index = 0, size = mFilteredPackageStates.size(); index < size; index++) { + var packageState = mFilteredPackageStates.get(index); + consumer.accept(packageState); + } + } + } +} diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/PackageManagerLocalSnapshotTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/PackageManagerLocalSnapshotTest.kt new file mode 100644 index 000000000000..faa235254665 --- /dev/null +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/PackageManagerLocalSnapshotTest.kt @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm.test + +import android.os.UserHandle +import android.util.ArrayMap +import com.android.server.pm.Computer +import com.android.server.pm.PackageManagerLocal +import com.android.server.pm.PackageManagerService +import com.android.server.pm.local.PackageManagerLocalImpl +import com.android.server.pm.pkg.PackageState +import com.android.server.pm.pkg.PackageStateInternal +import com.android.server.testutils.mockThrowOnUnmocked +import com.android.server.testutils.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import kotlin.test.assertFailsWith + +class PackageManagerLocalSnapshotTest { + + private val service = mockThrowOnUnmocked<PackageManagerService> { + @Suppress("DEPRECATION") + whenever(snapshotComputer(false)) { mockSnapshot() } + } + + private val packageStateAll = mockPackageState("com.package.all") + private val packageStateUser0 = mockPackageState("com.package.zero") + private val packageStateUser10 = mockPackageState("com.package.ten") + + @Test + fun unfiltered() { + val pmLocal = pmLocal() + val snapshot = pmLocal.withUnfilteredSnapshot() + val filteredOne: PackageManagerLocal.FilteredSnapshot + val filteredTwo: PackageManagerLocal.FilteredSnapshot + snapshot.use { + val packageStates = it.packageStates + + // Check contents + assertThat(packageStates).containsExactly( + packageStateAll.packageName, packageStateAll, + packageStateUser0.packageName, packageStateUser0, + packageStateUser10.packageName, packageStateUser10, + ) + + // Check further calls get the same object + assertThat(it.packageStates).isSameInstanceAs(packageStates) + + // Generate 3 filtered children (2 for the same caller, 1 for different) + filteredOne = it.filtered(1000, UserHandle.getUserHandleForUid(1000)) + filteredTwo = it.filtered(1000, UserHandle.getUserHandleForUid(1000)) + val filteredThree = it.filtered(20000, UserHandle.getUserHandleForUid(1001000)) + + // Check that siblings, even for the same input, are isolated + assertThat(filteredOne).isNotSameInstanceAs(filteredTwo) + + assertThat(filteredOne.getPackageState(packageStateAll.packageName)) + .isEqualTo(packageStateAll) + assertThat(filteredOne.getPackageState(packageStateUser0.packageName)) + .isEqualTo(packageStateUser0) + assertThat(filteredOne.getPackageState(packageStateUser10.packageName)).isNull() + + filteredThree.use { + val statesList = mutableListOf<PackageState>() + assertThat(it.forAllPackageStates { statesList += it }) + assertThat(statesList).containsExactly(packageStateAll, packageStateUser10) + } + + // Call after child close, parent open fails + assertClosedFailure { + filteredThree.getPackageState(packageStateAll.packageName) + } + + // Manually close first sibling and check that second still works + filteredOne.close() + assertThat(filteredTwo.getPackageState(packageStateAll.packageName)) + .isEqualTo(packageStateAll) + } + + // Call after close fails + assertClosedFailure { snapshot.packageStates } + assertClosedFailure { filteredOne.forAllPackageStates {} } + assertClosedFailure { + filteredTwo.getPackageState(packageStateAll.packageName) + } + + // Check newly taken snapshot is different + assertThat(pmLocal.withUnfilteredSnapshot()).isNotSameInstanceAs(snapshot) + } + + @Test + fun filtered() { + val pmLocal = pmLocal() + val snapshot = pmLocal.withFilteredSnapshot(1000, UserHandle.getUserHandleForUid(1000)) + snapshot.use { + assertThat(it.getPackageState(packageStateAll.packageName)) + .isEqualTo(packageStateAll) + assertThat(it.getPackageState(packageStateUser0.packageName)) + .isEqualTo(packageStateUser0) + assertThat(it.getPackageState(packageStateUser10.packageName)).isNull() + + val statesList = mutableListOf<PackageState>() + assertThat(it.forAllPackageStates { statesList += it }) + assertThat(statesList).containsExactly(packageStateAll, packageStateUser0) + } + + // Call after close fails + assertClosedFailure { + snapshot.getPackageState(packageStateAll.packageName) + } + + // Check newly taken snapshot is different + assertThat(pmLocal.withFilteredSnapshot()).isNotSameInstanceAs(snapshot) + } + + private fun pmLocal(): PackageManagerLocal = PackageManagerLocalImpl(service) + + private fun mockSnapshot() = mockThrowOnUnmocked<Computer> { + val packageStates = ArrayMap<String, PackageStateInternal>().apply { + put(packageStateAll.packageName, packageStateAll) + put(packageStateUser0.packageName, packageStateUser0) + put(packageStateUser10.packageName, packageStateUser10) + } + whenever(this.packageStates) { packageStates } + whenever(getPackageStateFiltered(anyString(), anyInt(), anyInt())) { + packageStates[arguments[0]]?.takeUnless { + shouldFilterApplication(it, arguments[1] as Int, arguments[2] as Int) + } + } + + whenever( + shouldFilterApplication(any(PackageStateInternal::class.java), anyInt(), anyInt()) + ) { + val packageState = arguments[0] as PackageState + val user = arguments[2] as Int + + when (packageState) { + packageStateAll -> false + packageStateUser0 -> user != 0 + packageStateUser10 -> user != 10 + else -> true + } + } + } + + private fun mockPackageState(packageName: String) = mockThrowOnUnmocked<PackageStateInternal> { + whenever(this.packageName) { packageName } + whenever(toString()) { packageName } + } + + private fun assertClosedFailure(block: () -> Unit) = + assertFailsWith(IllegalStateException::class, block) + .run { assertThat(message).isEqualTo("Snapshot already closed") } +} |