Initial app filtering logic
This change adds support for filtering apps from one another based on
the declared <queries> tags they have in their manifest. By default,
enforcement of the checks will be bypassed via app op.
Test: atest AppsFilterTest
Test: visibility limited after `adb shell appops set --uid <pkg> QUERY_ALL_PACKAGES ignore
Test: blocking logged after `adb shell appops set --uid <pkg> QUERY_ALL_PACKAGES deny
Bug: 136675067
Change-Id: I7b0c3714298aac183dfe2ae9e94097587648d59f
diff --git a/api/current.txt b/api/current.txt
index 5d23b8f..919434e 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -103,6 +103,7 @@
field public static final String PACKAGE_USAGE_STATS = "android.permission.PACKAGE_USAGE_STATS";
field @Deprecated public static final String PERSISTENT_ACTIVITY = "android.permission.PERSISTENT_ACTIVITY";
field @Deprecated public static final String PROCESS_OUTGOING_CALLS = "android.permission.PROCESS_OUTGOING_CALLS";
+ field public static final String QUERY_ALL_PACKAGES = "android.permission.QUERY_ALL_PACKAGES";
field public static final String READ_CALENDAR = "android.permission.READ_CALENDAR";
field public static final String READ_CALL_LOG = "android.permission.READ_CALL_LOG";
field public static final String READ_CONTACTS = "android.permission.READ_CONTACTS";
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index fb72e65..d20cc41 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -834,9 +834,12 @@
public static final int OP_ACCESS_ACCESSIBILITY = 88;
/** @hide Read the device identifiers (IMEI / MEID, IMSI, SIM / Build serial) */
public static final int OP_READ_DEVICE_IDENTIFIERS = 89;
+ /** @hide Query all apps on device, regardless of declarations in the calling app manifest */
+ public static final int OP_QUERY_ALL_PACKAGES = 90;
+
/** @hide */
@UnsupportedAppUsage
- public static final int _NUM_OP = 90;
+ public static final int _NUM_OP = 91;
/** Access to coarse location information. */
public static final String OPSTR_COARSE_LOCATION = "android:coarse_location";
@@ -1112,6 +1115,8 @@
public static final String OPSTR_ACCESS_ACCESSIBILITY = "android:access_accessibility";
/** @hide Read device identifiers */
public static final String OPSTR_READ_DEVICE_IDENTIFIERS = "android:read_device_identifiers";
+ /** @hide Query all packages on device */
+ public static final String OPSTR_QUERY_ALL_PACKAGES = "android:query_all_packages";
// Warning: If an permission is added here it also has to be added to
// com.android.packageinstaller.permission.utils.EventLogger
@@ -1273,6 +1278,7 @@
OP_LEGACY_STORAGE, // LEGACY_STORAGE
OP_ACCESS_ACCESSIBILITY, // ACCESS_ACCESSIBILITY
OP_READ_DEVICE_IDENTIFIERS, // READ_DEVICE_IDENTIFIERS
+ OP_QUERY_ALL_PACKAGES, // QUERY_ALL_PACKAGES
};
/**
@@ -1369,6 +1375,7 @@
OPSTR_LEGACY_STORAGE,
OPSTR_ACCESS_ACCESSIBILITY,
OPSTR_READ_DEVICE_IDENTIFIERS,
+ OPSTR_QUERY_ALL_PACKAGES,
};
/**
@@ -1466,6 +1473,7 @@
"LEGACY_STORAGE",
"ACCESS_ACCESSIBILITY",
"READ_DEVICE_IDENTIFIERS",
+ "QUERY_ALL_PACKAGES",
};
/**
@@ -1564,6 +1572,7 @@
null, // no permission for OP_LEGACY_STORAGE
null, // no permission for OP_ACCESS_ACCESSIBILITY
null, // no direct permission for OP_READ_DEVICE_IDENTIFIERS
+ null, // no permission for OP_QUERY_ALL_PACKAGES
};
/**
@@ -1662,6 +1671,7 @@
null, // LEGACY_STORAGE
null, // ACCESS_ACCESSIBILITY
null, // READ_DEVICE_IDENTIFIERS
+ null, // QUERY_ALL_PACKAGES
};
/**
@@ -1759,6 +1769,7 @@
false, // LEGACY_STORAGE
false, // ACCESS_ACCESSIBILITY
false, // READ_DEVICE_IDENTIFIERS
+ false, // QUERY_ALL_PACKAGES
};
/**
@@ -1855,6 +1866,7 @@
AppOpsManager.MODE_DEFAULT, // LEGACY_STORAGE
AppOpsManager.MODE_ALLOWED, // ACCESS_ACCESSIBILITY
AppOpsManager.MODE_ERRORED, // READ_DEVICE_IDENTIFIERS
+ AppOpsManager.MODE_DEFAULT, // QUERY_ALL_PACKAGES
};
/**
@@ -1955,6 +1967,7 @@
false, // LEGACY_STORAGE
false, // ACCESS_ACCESSIBILITY
false, // READ_DEVICE_IDENTIFIERS
+ false, // QUERY_ALL_PACKAGES
};
/**
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index aa440d3..6a20484 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -4599,6 +4599,11 @@
<permission android:name="android.permission.MONITOR_INPUT"
android:protectionLevel="signature" />
+ <!-- Allows query of any normal app on the device, regardless of manifest declarations. -->
+ <permission android:name="android.permission.QUERY_ALL_PACKAGES"
+ android:protectionLevel="normal" />
+ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
+
<application android:process="system"
android:persistent="true"
android:hasCode="false"
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index dce4bf3..b34c422 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1700,11 +1700,16 @@
<!-- Add packages here -->
</string-array>
+ <!-- The set of system packages on device that are queryable regardless of the contents of their
+ manifest. -->
<string-array name="config_forceQueryablePackages" translatable="false">
<item>com.android.settings</item>
<!-- Add packages here -->
</string-array>
+ <!-- If true, will force all packages on any system partition as queryable regardless of the
+ contents of their manifest. -->
+ <bool name="config_forceSystemPackagesQueryable">false</bool>
<!-- Component name of the default wallpaper. This will be ImageWallpaper if not
specified -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 123deeb..f7eb360 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -787,6 +787,7 @@
<java-symbol type="string" name="emergency_calls_only" />
<java-symbol type="array" name="config_ephemeralResolverPackage" />
<java-symbol type="array" name="config_forceQueryablePackages" />
+ <java-symbol type="bool" name="config_forceSystemPackagesQueryable" />
<java-symbol type="string" name="eventTypeAnniversary" />
<java-symbol type="string" name="eventTypeBirthday" />
<java-symbol type="string" name="eventTypeCustom" />
diff --git a/services/core/java/com/android/server/pm/AppsFilter.java b/services/core/java/com/android/server/pm/AppsFilter.java
new file mode 100644
index 0000000..ab8cc53
--- /dev/null
+++ b/services/core/java/com/android/server/pm/AppsFilter.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2019 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;
+
+import android.Manifest;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageParser;
+import android.os.Build;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.permission.IPermissionManager;
+import android.provider.DeviceConfig;
+import android.util.ArraySet;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The entity responsible for filtering visibility between apps based on declarations in their
+ * manifests.
+ */
+class AppsFilter {
+
+ private static final String TAG = PackageManagerService.TAG;
+ /**
+ * This contains a list of packages that are implicitly queryable because another app explicitly
+ * interacted with it. For example, if application A starts a service in application B,
+ * application B is implicitly allowed to query for application A; regardless of any manifest
+ * entries.
+ */
+ private final SparseArray<HashMap<String, ArrayList<String>>> mImplicitlyQueryable =
+ new SparseArray<>();
+
+ /**
+ * A mapping from the set of packages that query other packages via package name to the
+ * list of packages that they can see.
+ */
+ private final HashMap<String, List<String>> mQueriesViaPackage = new HashMap<>();
+
+ /**
+ * A mapping from the set of packages that query others via intent to the list
+ * of packages that the intents resolve to.
+ */
+ private final HashMap<String, List<String>> mQueriesViaIntent = new HashMap<>();
+
+ /**
+ * A set of packages that are always queryable by any package, regardless of their manifest
+ * content.
+ */
+ private final HashSet<String> mForceQueryable;
+ /**
+ * A set of packages that are always queryable by any package, regardless of their manifest
+ * content.
+ */
+ private final Set<String> mForceQueryableByDevice;
+
+ /** True if all system apps should be made queryable by default. */
+ private final boolean mSystemAppsQueryable;
+
+ private final IPermissionManager mPermissionManager;
+
+ private final AppOpsManager mAppOpsManager;
+ private final ConfigProvider mConfigProvider;
+
+ AppsFilter(ConfigProvider configProvider, IPermissionManager permissionManager,
+ AppOpsManager appOpsManager, String[] forceQueryableWhitelist,
+ boolean systemAppsQueryable) {
+ mConfigProvider = configProvider;
+ mAppOpsManager = appOpsManager;
+ final HashSet<String> forceQueryableByDeviceSet = new HashSet<>();
+ Collections.addAll(forceQueryableByDeviceSet, forceQueryableWhitelist);
+ this.mForceQueryableByDevice = Collections.unmodifiableSet(forceQueryableByDeviceSet);
+ this.mForceQueryable = new HashSet<>();
+ mPermissionManager = permissionManager;
+ mSystemAppsQueryable = systemAppsQueryable;
+ }
+
+ public static AppsFilter create(Context context) {
+ final boolean forceSystemAppsQueryable =
+ context.getResources().getBoolean(R.bool.config_forceSystemPackagesQueryable);
+ final ConfigProvider configProvider = () -> DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE,
+ "package_query_filtering_enabled",
+ false);
+ final String[] forcedQueryablePackageNames;
+ if (forceSystemAppsQueryable) {
+ // all system apps already queryable, no need to read and parse individual exceptions
+ forcedQueryablePackageNames = new String[]{};
+ } else {
+ forcedQueryablePackageNames =
+ context.getResources().getStringArray(R.array.config_forceQueryablePackages);
+ for (int i = 0; i < forcedQueryablePackageNames.length; i++) {
+ forcedQueryablePackageNames[i] = forcedQueryablePackageNames[i].intern();
+ }
+ }
+ IPermissionManager permissionmgr =
+ (IPermissionManager) ServiceManager.getService("permissionmgr");
+ return new AppsFilter(configProvider, permissionmgr,
+ context.getSystemService(AppOpsManager.class), forcedQueryablePackageNames,
+ forceSystemAppsQueryable);
+ }
+
+ /** Returns true if the querying package may query for the potential target package */
+ private static boolean canQuery(PackageParser.Package querying,
+ PackageParser.Package potentialTarget) {
+ if (querying.mQueriesIntents == null) {
+ return false;
+ }
+ for (Intent intent : querying.mQueriesIntents) {
+ for (PackageParser.Activity activity : potentialTarget.activities) {
+ if (activity.intents != null) {
+ for (PackageParser.ActivityIntentInfo filter : activity.intents) {
+ if (matches(intent, filter)) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /** Returns true if the given intent matches the given filter. */
+ private static boolean matches(Intent intent, PackageParser.ActivityIntentInfo filter) {
+ return filter.match(intent.getAction(), intent.getType(), intent.getScheme(),
+ intent.getData(), intent.getCategories(), "AppsFilter") > 0;
+ }
+
+ /**
+ * Marks that a package initiated an interaction with another package, granting visibility of
+ * the prior from the former.
+ *
+ * @param initiatingPackage the package initiating the interaction
+ * @param targetPackage the package being interacted with and thus gaining visibility of the
+ * initiating package.
+ * @param userId the user in which this interaction was taking place
+ */
+ private void markAppInteraction(
+ PackageSetting initiatingPackage, PackageSetting targetPackage, int userId) {
+ HashMap<String, ArrayList<String>> currentUser = mImplicitlyQueryable.get(userId);
+ if (currentUser == null) {
+ currentUser = new HashMap<>();
+ mImplicitlyQueryable.put(userId, currentUser);
+ }
+ if (!currentUser.containsKey(targetPackage.pkg.packageName)) {
+ currentUser.put(targetPackage.pkg.packageName, new ArrayList<>());
+ }
+ currentUser.get(targetPackage.pkg.packageName).add(initiatingPackage.pkg.packageName);
+ }
+
+ /**
+ * Adds a package that should be considered when filtering visibility between apps.
+ *
+ * @param newPkg the new package being added
+ * @param existing all other packages currently on the device.
+ */
+ public void addPackage(PackageParser.Package newPkg,
+ Map<String, PackageParser.Package> existing) {
+ // let's re-evaluate the ability of already added packages to see this new package
+ if (newPkg.mForceQueryable
+ || (mSystemAppsQueryable && (newPkg.isSystem() || newPkg.isUpdatedSystemApp()))) {
+ mForceQueryable.add(newPkg.packageName);
+ } else {
+ for (String packageName : mQueriesViaIntent.keySet()) {
+ if (packageName == newPkg.packageName) {
+ continue;
+ }
+ final PackageParser.Package existingPackage = existing.get(packageName);
+ if (canQuery(existingPackage, newPkg)) {
+ mQueriesViaIntent.get(packageName).add(newPkg.packageName);
+ }
+ }
+ }
+ // if the new package declares them, let's evaluate its ability to see existing packages
+ mQueriesViaIntent.put(newPkg.packageName, new ArrayList<>());
+ for (PackageParser.Package existingPackage : existing.values()) {
+ if (existingPackage.packageName == newPkg.packageName) {
+ continue;
+ }
+ if (existingPackage.mForceQueryable
+ || (mSystemAppsQueryable
+ && (newPkg.isSystem() || newPkg.isUpdatedSystemApp()))) {
+ continue;
+ }
+ if (canQuery(newPkg, existingPackage)) {
+ mQueriesViaIntent.get(newPkg.packageName).add(existingPackage.packageName);
+ }
+ }
+ final ArrayList<String> queriesPackages = new ArrayList<>(
+ newPkg.mQueriesPackages == null ? 0 : newPkg.mQueriesPackages.size());
+ if (newPkg.mQueriesPackages != null) {
+ queriesPackages.addAll(newPkg.mQueriesPackages);
+ }
+ mQueriesViaPackage.put(newPkg.packageName, queriesPackages);
+ }
+
+ /**
+ * Removes a package for consideration when filtering visibility between apps.
+ *
+ * @param packageName the name of the package being removed.
+ */
+ public void removePackage(String packageName) {
+ mForceQueryable.remove(packageName);
+
+ for (int i = 0; i < mImplicitlyQueryable.size(); i++) {
+ mImplicitlyQueryable.valueAt(i).remove(packageName);
+ for (ArrayList<String> initiators : mImplicitlyQueryable.valueAt(i).values()) {
+ initiators.remove(packageName);
+ }
+ }
+
+ mQueriesViaIntent.remove(packageName);
+ for (List<String> declarators : mQueriesViaIntent.values()) {
+ declarators.remove(packageName);
+ }
+
+ mQueriesViaPackage.remove(packageName);
+ }
+
+ /**
+ * Returns true if the calling package should not be able to see the target package, false if no
+ * filtering should be done.
+ *
+ * @param callingUid the uid of the caller attempting to access a package
+ * @param callingSetting the setting attempting to access a package or null if it could not be
+ * found
+ * @param targetPkgSetting the package being accessed
+ * @param userId the user in which this access is being attempted
+ */
+ public boolean shouldFilterApplication(int callingUid, @Nullable SettingBase callingSetting,
+ PackageSetting targetPkgSetting, int userId) {
+ if (callingUid < Process.FIRST_APPLICATION_UID) {
+ return false;
+ }
+ if (callingSetting == null) {
+ Slog.wtf(TAG, "No setting found for non system uid " + callingUid);
+ return true;
+ }
+ PackageSetting callingPkgSetting = null;
+ if (callingSetting instanceof PackageSetting) {
+ callingPkgSetting = (PackageSetting) callingSetting;
+ if (!shouldFilterApplicationInternal(callingPkgSetting, targetPkgSetting,
+ userId)) {
+ // TODO: actually base this on a start / launch (not just a query)
+ markAppInteraction(callingPkgSetting, targetPkgSetting, userId);
+ return false;
+ }
+ } else if (callingSetting instanceof SharedUserSetting) {
+ final ArraySet<PackageSetting> packageSettings =
+ ((SharedUserSetting) callingSetting).packages;
+ if (packageSettings != null && packageSettings.size() > 0) {
+ for (PackageSetting packageSetting : packageSettings) {
+ if (!shouldFilterApplicationInternal(packageSetting, targetPkgSetting,
+ userId)) {
+ // TODO: actually base this on a start / launch (not just a query)
+ markAppInteraction(packageSetting, targetPkgSetting, userId);
+ return false;
+ }
+ if (callingPkgSetting == null && packageSetting.pkg != null) {
+ callingPkgSetting = packageSetting;
+ }
+ }
+ } else {
+ return true;
+ }
+ }
+ if (callingPkgSetting == null) {
+ Slog.wtf(TAG, "What... " + callingSetting);
+ return true;
+ }
+ final int mode = mAppOpsManager
+ .checkOpNoThrow(AppOpsManager.OP_QUERY_ALL_PACKAGES, callingUid,
+ callingPkgSetting.pkg.packageName);
+ switch (mode) {
+ case AppOpsManager.MODE_DEFAULT:
+ // if default, let's rely on remote feature toggle to determine whether to
+ // actually filter
+ return mConfigProvider.isEnabled();
+ case AppOpsManager.MODE_ALLOWED:
+ // explicitly allowed to see all packages, don't filter
+ return false;
+ case AppOpsManager.MODE_ERRORED:
+ // deny / error: let's log so developer can audit usages
+ Slog.i(TAG, callingPkgSetting.pkg.packageName
+ + " blocked from accessing " + targetPkgSetting.pkg.packageName);
+ case AppOpsManager.MODE_IGNORED:
+ // fall through
+ default:
+ return true;
+ }
+ }
+
+ private boolean shouldFilterApplicationInternal(
+ PackageSetting callingPkgSetting, PackageSetting targetPkgSetting, int userId) {
+ final String callingName = callingPkgSetting.pkg.packageName;
+ final String targetName = targetPkgSetting.pkg.packageName;
+ if (callingPkgSetting.pkg.applicationInfo.targetSdkVersion < Build.VERSION_CODES.R) {
+ return false;
+ }
+ if (isImplicitlyQueryableSystemApp(targetPkgSetting)) {
+ return false;
+ }
+ if (targetPkgSetting.pkg.mForceQueryable) {
+ return false;
+ }
+ if (mForceQueryable.contains(targetName)) {
+ return false;
+ }
+ if (mQueriesViaPackage.containsKey(callingName)
+ && mQueriesViaPackage.get(callingName).contains(
+ targetName)) {
+ // the calling package has explicitly declared the target package; allow
+ return false;
+ } else if (mQueriesViaIntent.containsKey(callingName)
+ && mQueriesViaIntent.get(callingName).contains(targetName)) {
+ return false;
+ }
+ if (mImplicitlyQueryable.get(userId) != null
+ && mImplicitlyQueryable.get(userId).containsKey(callingName)
+ && mImplicitlyQueryable.get(userId).get(callingName).contains(targetName)) {
+ return false;
+ }
+ try {
+ if (mPermissionManager.checkPermission(
+ Manifest.permission.QUERY_ALL_PACKAGES, callingName, userId)
+ == PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ } catch (RemoteException e) {
+ return true;
+ }
+ return true;
+ }
+
+ private boolean isImplicitlyQueryableSystemApp(PackageSetting targetPkgSetting) {
+ return targetPkgSetting.isSystem() && (mSystemAppsQueryable
+ || mForceQueryableByDevice.contains(targetPkgSetting.pkg.packageName));
+ }
+
+ public interface ConfigProvider {
+ boolean isEnabled();
+ }
+
+}
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index ebbb193..7c10ec7 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -768,6 +768,8 @@
}
}
+ private final AppsFilter mAppsFilter;
+
class PackageParserCallback implements PackageParser.Callback {
@Override public final boolean hasFeature(String feature) {
return PackageManagerService.this.hasSystemFeature(feature, 0);
@@ -2426,6 +2428,8 @@
mProtectedPackages = new ProtectedPackages(mContext);
mApexManager = ApexManager.create(context);
+ mAppsFilter = AppsFilter.create(context);
+
// CHECKSTYLE:OFF IndentationCheck
synchronized (mInstallLock) {
// writer
@@ -4332,7 +4336,9 @@
return !mInstantAppRegistry.isInstantAccessGranted(
userId, UserHandle.getAppId(callingUid), ps.appId);
}
- return false;
+ int appId = UserHandle.getAppId(callingUid);
+ final SettingBase callingPs = mSettings.getSettingLPr(appId);
+ return mAppsFilter.shouldFilterApplication(callingUid, callingPs, ps, userId);
}
/**
@@ -11673,6 +11679,7 @@
ksms.addScannedPackageLPw(pkg);
mComponentResolver.addAllComponents(pkg, chatty);
+ mAppsFilter.addPackage(pkg, mPackages);
// Don't allow ephemeral applications to define new permissions groups.
if ((scanFlags & SCAN_AS_INSTANT_APP) != 0) {
@@ -11883,7 +11890,7 @@
void cleanPackageDataStructuresLILPw(PackageParser.Package pkg, boolean chatty) {
mComponentResolver.removeAllComponents(pkg, chatty);
-
+ mAppsFilter.removePackage(pkg.packageName);
mPermissionManager.removeAllPermissions(pkg, chatty);
final int instrumentationSize = pkg.instrumentation.size();
diff --git a/services/tests/servicestests/src/com/android/server/pm/AppsFilterTest.java b/services/tests/servicestests/src/com/android/server/pm/AppsFilterTest.java
new file mode 100644
index 0000000..65f9e32
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/AppsFilterTest.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2019 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;
+
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageParser;
+import android.os.Build;
+import android.os.Process;
+import android.permission.IPermissionManager;
+import android.util.ArrayMap;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public class AppsFilterTest {
+
+ private static final int DUMMY_CALLING_UID = 10345;
+
+ @Mock
+ IPermissionManager mPermissionManagerMock;
+
+ @Mock
+ AppsFilter.ConfigProvider mConfigProviderMock;
+
+ @Mock
+ AppOpsManager mAppOpsManager;
+
+ private Map<String, PackageParser.Package> mExisting = new ArrayMap<>();
+
+ private static PackageBuilder pkg(String packageName) {
+ return new PackageBuilder(packageName)
+ .setApplicationInfoTargetSdkVersion(Build.VERSION_CODES.R);
+ }
+
+ private static PackageBuilder pkg(String packageName, Intent... queries) {
+ return pkg(packageName).setQueriesIntents(queries);
+ }
+
+ private static PackageBuilder pkg(String packageName, String... queriesPackages) {
+ return pkg(packageName).setQueriesPackages(queriesPackages);
+ }
+
+ private static PackageBuilder pkg(String packageName, IntentFilter... filters) {
+ final PackageBuilder packageBuilder = pkg(packageName).addActivity(
+ pkg -> new PackageParser.ParseComponentArgs(pkg, new String[1], 0, 0, 0, 0, 0, 0,
+ new String[]{packageName}, 0, 0, 0), new ActivityInfo());
+ for (IntentFilter filter : filters) {
+ packageBuilder.addActivityIntentInfo(0 /* index */, activity -> {
+ final PackageParser.ActivityIntentInfo info =
+ new PackageParser.ActivityIntentInfo(activity);
+ if (filter.countActions() > 0) {
+ filter.actionsIterator().forEachRemaining(info::addAction);
+ }
+ if (filter.countCategories() > 0) {
+ filter.actionsIterator().forEachRemaining(info::addAction);
+ }
+ if (filter.countDataAuthorities() > 0) {
+ filter.authoritiesIterator().forEachRemaining(info::addDataAuthority);
+ }
+ if (filter.countDataSchemes() > 0) {
+ filter.schemesIterator().forEachRemaining(info::addDataScheme);
+ }
+ return info;
+ });
+ }
+ return packageBuilder;
+ }
+
+ @Before
+ public void setup() throws Exception {
+ mExisting = new ArrayMap<>();
+
+ MockitoAnnotations.initMocks(this);
+ when(mPermissionManagerMock
+ .checkPermission(anyString(), anyString(), anyInt()))
+ .thenReturn(PackageManager.PERMISSION_DENIED);
+ when(mConfigProviderMock.isEnabled()).thenReturn(true);
+ when(mAppOpsManager.checkOpNoThrow(eq(AppOpsManager.OP_QUERY_ALL_PACKAGES), eq(
+ DUMMY_CALLING_UID), anyString())).thenReturn(AppOpsManager.MODE_DEFAULT);
+ }
+
+ @Test
+ public void testQueriesAction_FilterMatches() {
+ final AppsFilter appsFilter =
+ new AppsFilter(mConfigProviderMock, mPermissionManagerMock, mAppOpsManager,
+ new String[]{}, false);
+
+ PackageSetting target = simulateAddPackage(appsFilter,
+ pkg("com.some.package", new IntentFilter("TEST_ACTION"))).build();
+ PackageSetting calling = simulateAddPackage(appsFilter,
+ pkg("com.some.other.package", new Intent("TEST_ACTION"))).build();
+
+ assertFalse(appsFilter.shouldFilterApplication(DUMMY_CALLING_UID, calling, target, 0));
+ }
+
+ @Test
+ public void testQueriesAction_NoMatchingAction_Filters() {
+ final AppsFilter appsFilter =
+ new AppsFilter(mConfigProviderMock, mPermissionManagerMock, mAppOpsManager,
+ new String[]{}, false);
+
+ PackageSetting target = simulateAddPackage(appsFilter,
+ pkg("com.some.package")).build();
+ PackageSetting calling = simulateAddPackage(appsFilter,
+ pkg("com.some.other.package", new Intent("TEST_ACTION"))).build();
+
+ assertTrue(appsFilter.shouldFilterApplication(DUMMY_CALLING_UID, calling, target, 0));
+ }
+
+ @Test
+ public void testQueriesAction_NoMatchingActionFilterLowSdk_DoesntFilter() {
+ final AppsFilter appsFilter =
+ new AppsFilter(mConfigProviderMock, mPermissionManagerMock, mAppOpsManager,
+ new String[]{}, false);
+
+ PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package")).build();
+ PackageSetting calling = simulateAddPackage(appsFilter, pkg("com.some.other.package",
+ new Intent("TEST_ACTION")).setApplicationInfoTargetSdkVersion(
+ Build.VERSION_CODES.P)).build();
+
+ assertFalse(appsFilter.shouldFilterApplication(DUMMY_CALLING_UID, calling, target, 0));
+ }
+
+ @Test
+ public void testNoQueries_Filters() {
+ final AppsFilter appsFilter =
+ new AppsFilter(mConfigProviderMock, mPermissionManagerMock, mAppOpsManager,
+ new String[]{}, false);
+
+ PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package")).build();
+ PackageSetting calling = simulateAddPackage(appsFilter,
+ pkg("com.some.other.package")).build();
+
+ assertTrue(appsFilter.shouldFilterApplication(DUMMY_CALLING_UID, calling, target, 0));
+ }
+
+ @Test
+ public void testForceQueryable_DoesntFilter() {
+ final AppsFilter appsFilter =
+ new AppsFilter(mConfigProviderMock, mPermissionManagerMock, mAppOpsManager,
+ new String[]{}, false);
+
+ PackageSetting target =
+ simulateAddPackage(appsFilter, pkg("com.some.package").setForceQueryable(true))
+ .build();
+ PackageSetting calling = simulateAddPackage(appsFilter,
+ pkg("com.some.other.package")).build();
+
+ assertFalse(appsFilter.shouldFilterApplication(DUMMY_CALLING_UID, calling, target, 0));
+ }
+
+ @Test
+ public void testForceQueryableByDevice_SystemCaller_DoesntFilter() {
+ final AppsFilter appsFilter =
+ new AppsFilter(mConfigProviderMock, mPermissionManagerMock, mAppOpsManager,
+ new String[]{"com.some.package"}, false);
+
+ PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package"))
+ .setPkgFlags(ApplicationInfo.FLAG_SYSTEM)
+ .build();
+ PackageSetting calling = simulateAddPackage(appsFilter,
+ pkg("com.some.other.package")).build();
+
+ assertFalse(appsFilter.shouldFilterApplication(DUMMY_CALLING_UID, calling, target, 0));
+ }
+
+ @Test
+ public void testForceQueryableByDevice_NonSystemCaller_Filters() {
+ final AppsFilter appsFilter =
+ new AppsFilter(mConfigProviderMock, mPermissionManagerMock, mAppOpsManager,
+ new String[]{"com.some.package"}, false);
+
+ PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package")).build();
+ PackageSetting calling = simulateAddPackage(appsFilter,
+ pkg("com.some.other.package")).build();
+
+ assertTrue(appsFilter.shouldFilterApplication(DUMMY_CALLING_UID, calling, target, 0));
+ }
+
+
+ @Test
+ public void testSystemQueryable_DoesntFilter() {
+ final AppsFilter appsFilter =
+ new AppsFilter(mConfigProviderMock, mPermissionManagerMock, mAppOpsManager,
+ new String[]{}, true /* system force queryable */);
+
+ PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package"))
+ .setPkgFlags(ApplicationInfo.FLAG_SYSTEM)
+ .build();
+ PackageSetting calling = simulateAddPackage(appsFilter,
+ pkg("com.some.other.package")).build();
+
+ assertFalse(appsFilter.shouldFilterApplication(DUMMY_CALLING_UID, calling, target, 0));
+ }
+
+ @Test
+ public void testQueriesPackage_DoesntFilter() {
+ final AppsFilter appsFilter =
+ new AppsFilter(mConfigProviderMock, mPermissionManagerMock, mAppOpsManager,
+ new String[]{}, false);
+
+ PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package")).build();
+ PackageSetting calling = simulateAddPackage(appsFilter,
+ pkg("com.some.other.package", "com.some.package")).build();
+
+ assertFalse(appsFilter.shouldFilterApplication(DUMMY_CALLING_UID, calling, target, 0));
+ }
+
+ @Test
+ public void testNoQueries_AppOpModeDeny_Filters() {
+ when(mAppOpsManager.checkOpNoThrow(eq(AppOpsManager.OP_QUERY_ALL_PACKAGES), eq(
+ DUMMY_CALLING_UID), anyString())).thenReturn(AppOpsManager.MODE_ERRORED);
+ final AppsFilter appsFilter =
+ new AppsFilter(mConfigProviderMock, mPermissionManagerMock, mAppOpsManager,
+ new String[]{}, false);
+
+ PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package")).build();
+ PackageSetting calling = simulateAddPackage(appsFilter,
+ pkg("com.some.other.package")).build();
+
+ assertTrue(appsFilter.shouldFilterApplication(DUMMY_CALLING_UID, calling, target, 0));
+ }
+
+ @Test
+ public void testNoQueries_AppOpModeAllow_DoesntFilter() {
+ when(mAppOpsManager.checkOpNoThrow(eq(AppOpsManager.OP_QUERY_ALL_PACKAGES), eq(
+ DUMMY_CALLING_UID), anyString())).thenReturn(AppOpsManager.MODE_ALLOWED);
+ final AppsFilter appsFilter =
+ new AppsFilter(mConfigProviderMock, mPermissionManagerMock, mAppOpsManager,
+ new String[]{}, false);
+
+ PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package")).build();
+ PackageSetting calling = simulateAddPackage(appsFilter,
+ pkg("com.some.other.package")).build();
+
+ assertFalse(appsFilter.shouldFilterApplication(DUMMY_CALLING_UID, calling, target, 0));
+ }
+
+ @Test
+ public void testNoQueries_AppOpModeIgnore_Filters() {
+ when(mAppOpsManager.checkOpNoThrow(eq(AppOpsManager.OP_QUERY_ALL_PACKAGES), eq(
+ DUMMY_CALLING_UID), anyString())).thenReturn(AppOpsManager.MODE_IGNORED);
+ final AppsFilter appsFilter =
+ new AppsFilter(mConfigProviderMock, mPermissionManagerMock, mAppOpsManager,
+ new String[]{}, false);
+
+ PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package")).build();
+ PackageSetting calling = simulateAddPackage(appsFilter,
+ pkg("com.some.other.package")).build();
+
+ assertTrue(appsFilter.shouldFilterApplication(DUMMY_CALLING_UID, calling, target, 0));
+ }
+
+ @Test
+ public void testNoQueries_FeatureOff_DoesntFilter() {
+ when(mConfigProviderMock.isEnabled()).thenReturn(false);
+ final AppsFilter appsFilter =
+ new AppsFilter(mConfigProviderMock, mPermissionManagerMock, mAppOpsManager,
+ new String[]{}, false);
+
+ PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package")).build();
+ PackageSetting calling = simulateAddPackage(appsFilter,
+ pkg("com.some.other.package")).build();
+
+ assertFalse(appsFilter.shouldFilterApplication(DUMMY_CALLING_UID, calling, target, 0));
+ }
+
+ @Test
+ public void testSystemUid_DoesntFilter() {
+ final AppsFilter appsFilter =
+ new AppsFilter(mConfigProviderMock, mPermissionManagerMock, mAppOpsManager,
+ new String[]{}, false);
+
+ PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package")).build();
+
+ assertFalse(appsFilter.shouldFilterApplication(0, null, target, 0));
+ assertFalse(appsFilter.shouldFilterApplication(
+ Process.FIRST_APPLICATION_UID - 1, null, target, 0));
+ }
+
+ @Test
+ public void testNonSystemUid_NoCallingSetting_Filters() {
+ final AppsFilter appsFilter =
+ new AppsFilter(mConfigProviderMock, mPermissionManagerMock, mAppOpsManager,
+ new String[]{}, false);
+
+ PackageSetting target = simulateAddPackage(appsFilter, pkg("com.some.package")).build();
+
+ assertTrue(appsFilter.shouldFilterApplication(DUMMY_CALLING_UID, null, target, 0));
+ }
+
+ private PackageSettingBuilder simulateAddPackage(AppsFilter filter,
+ PackageBuilder newPkgBuilder) {
+ PackageParser.Package newPkg = newPkgBuilder.build();
+ filter.addPackage(newPkg, mExisting);
+ mExisting.put(newPkg.packageName, newPkg);
+ return new PackageSettingBuilder()
+ .setPackage(newPkg)
+ .setName(newPkg.packageName)
+ .setCodePath("/")
+ .setResourcePath("/")
+ .setPVersionCode(1L);
+ }
+
+}
+
diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageBuilder.java b/services/tests/servicestests/src/com/android/server/pm/PackageBuilder.java
index 470d4fa..c38672c 100644
--- a/services/tests/servicestests/src/com/android/server/pm/PackageBuilder.java
+++ b/services/tests/servicestests/src/com/android/server/pm/PackageBuilder.java
@@ -16,10 +16,16 @@
package com.android.server.pm;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
import android.content.pm.PackageParser;
import com.android.internal.util.ArrayUtils;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+
class PackageBuilder {
final PackageParser.Package mPkg;
@@ -119,4 +125,52 @@
mPkg.applicationInfo.flags |= flag;
return this;
}
+
+ public PackageBuilder setApplicationInfoTargetSdkVersion(int versionCode) {
+ mPkg.applicationInfo.targetSdkVersion = versionCode;
+ return this;
+ }
+
+ public PackageBuilder setQueriesIntents(Collection<Intent> queriesIntents) {
+ mPkg.mQueriesIntents = new ArrayList<>(queriesIntents);
+ return this;
+ }
+
+ public PackageBuilder setQueriesIntents(Intent... intents) {
+ return setQueriesIntents(Arrays.asList(intents));
+ }
+
+ public PackageBuilder setQueriesPackages(Collection<String> queriesPackages) {
+ mPkg.mQueriesPackages = new ArrayList<>(queriesPackages);
+ return this;
+ }
+
+ public PackageBuilder setQueriesPackages(String... queriesPackages) {
+ return setQueriesPackages(Arrays.asList(queriesPackages));
+ }
+
+ public PackageBuilder setForceQueryable(boolean forceQueryable) {
+ mPkg.mForceQueryable = forceQueryable;
+ return this;
+ }
+
+ public interface ParseComponentArgsCreator {
+ PackageParser.ParseComponentArgs create(PackageParser.Package pkg);
+ }
+
+ public PackageBuilder addActivity(ParseComponentArgsCreator argsCreator, ActivityInfo info) {
+ mPkg.activities.add(new PackageParser.Activity(argsCreator.create(mPkg), info));
+ return this;
+ }
+
+ public interface ActivityIntentInfoCreator {
+ PackageParser.ActivityIntentInfo create(PackageParser.Activity activity);
+ }
+
+ public PackageBuilder addActivityIntentInfo(
+ int activityIndex, ActivityIntentInfoCreator creator) {
+ final PackageParser.Activity activity = mPkg.activities.get(activityIndex);
+ activity.intents.add(creator.create(activity));
+ return this;
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageSettingBuilder.java b/services/tests/servicestests/src/com/android/server/pm/PackageSettingBuilder.java
index b42cfd8..06c6314 100644
--- a/services/tests/servicestests/src/com/android/server/pm/PackageSettingBuilder.java
+++ b/services/tests/servicestests/src/com/android/server/pm/PackageSettingBuilder.java
@@ -16,6 +16,7 @@
package com.android.server.pm;
+import android.content.pm.PackageParser;
import android.content.pm.PackageUserState;
import android.util.SparseArray;
@@ -41,6 +42,12 @@
private long[] mUsesStaticLibrariesVersions;
private String mVolumeUuid;
private SparseArray<PackageUserState> mUserStates = new SparseArray<>();
+ private PackageParser.Package mPkg;
+
+ public PackageSettingBuilder setPackage(PackageParser.Package pkg) {
+ this.mPkg = pkg;
+ return this;
+ }
public PackageSettingBuilder setName(String name) {
this.mName = name;
@@ -144,6 +151,7 @@
mCpuAbiOverrideString, mPVersionCode, mPkgFlags, mPrivateFlags, mParentPackageName,
mChildPackageNames, mSharedUserId, mUsesStaticLibraries,
mUsesStaticLibrariesVersions);
+ packageSetting.pkg = mPkg;
packageSetting.volumeUuid = this.mVolumeUuid;
for (int i = 0; i < mUserStates.size(); i++) {
packageSetting.setUserState(mUserStates.keyAt(i), mUserStates.valueAt(i));