diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 5bad055810ccc..8bebafff37f02 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -305,6 +305,35 @@ interface IPackageManager { void setHomeActivity(in ComponentName className, int userId); + /** + * Overrides the label and icon of the component specified by the component name. The component + * must belong to the calling app. + * + * These changes will be reset on the next boot and whenever the package is updated. + * + * Only the app defined as com.android.internal.R.config_overrideComponentUiPackage is allowed + * to call this. + * + * @param componentName The component name to override the label/icon of. + * @param nonLocalizedLabel The label to be displayed. + * @param icon The icon to be displayed. + * @param userId The user id. + */ + void overrideLabelAndIcon(in ComponentName componentName, String nonLocalizedLabel, + int icon, int userId); + + /** + * Restores the label and icon of the activity specified by the component name if either has + * been overridden. The component must belong to the calling app. + * + * Only the app defined as com.android.internal.R.config_overrideComponentUiPackage is allowed + * to call this. + * + * @param componentName The component name. + * @param userId The user id. + */ + void restoreLabelAndIcon(in ComponentName componentName, int userId); + /** * As per {@link android.content.pm.PackageManager#setComponentEnabledSetting}. */ diff --git a/core/java/android/content/pm/PackageUserState.java b/core/java/android/content/pm/PackageUserState.java index 61b1553e28a8a..327d1b8beeb12 100644 --- a/core/java/android/content/pm/PackageUserState.java +++ b/core/java/android/content/pm/PackageUserState.java @@ -27,18 +27,24 @@ import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS; import static android.content.pm.PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS; import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; +import android.content.ComponentName; import android.content.pm.parsing.component.ParsedMainComponent; import android.os.BaseBundle; import android.os.Debug; import android.os.PersistableBundle; +import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.DebugUtils; +import android.util.Pair; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.CollectionUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -84,6 +90,9 @@ public class PackageUserState { private ArrayMap sharedLibraryOverlayPaths; // Lib name to overlay paths private String[] cachedOverlayPaths; + @Nullable + private ArrayMap> componentLabelIconOverrideMap; + @UnsupportedAppUsage public PackageUserState() { installed = true; @@ -123,6 +132,9 @@ public class PackageUserState { sharedLibraryOverlayPaths = new ArrayMap<>(o.sharedLibraryOverlayPaths); } harmfulAppWarning = o.harmfulAppWarning; + if (o.componentLabelIconOverrideMap != null) { + this.componentLabelIconOverrideMap = new ArrayMap<>(o.componentLabelIconOverrideMap); + } } public String[] getOverlayPaths() { @@ -146,6 +158,65 @@ public class PackageUserState { cachedOverlayPaths = null; } + /** + * Overrides the non-localized label and icon of a component. + * + * @return true if the label or icon was changed. + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public boolean overrideLabelAndIcon(@NonNull ComponentName component, + @Nullable String nonLocalizedLabel, @Nullable Integer icon) { + String existingLabel = null; + Integer existingIcon = null; + + if (componentLabelIconOverrideMap != null) { + Pair pair = componentLabelIconOverrideMap.get(component); + if (pair != null) { + existingLabel = pair.first; + existingIcon = pair.second; + } + } + + boolean changed = !TextUtils.equals(existingLabel, nonLocalizedLabel) + || !Objects.equals(existingIcon, icon); + + if (changed) { + if (nonLocalizedLabel == null && icon == null) { + componentLabelIconOverrideMap.remove(component); + if (componentLabelIconOverrideMap.isEmpty()) { + componentLabelIconOverrideMap = null; + } + } else { + if (componentLabelIconOverrideMap == null) { + componentLabelIconOverrideMap = new ArrayMap<>(1); + } + + componentLabelIconOverrideMap.put(component, Pair.create(nonLocalizedLabel, icon)); + } + } + + return changed; + } + + /** + * Clears all values previously set by {@link #overrideLabelAndIcon(ComponentName, + * String, Integer)}. + * + * This is done when the package is updated as the components and resource IDs may have changed. + */ + public void resetOverrideComponentLabelIcon() { + componentLabelIconOverrideMap = null; + } + + @Nullable + public Pair getOverrideLabelIconForComponent(ComponentName componentName) { + if (ArrayUtils.isEmpty(componentLabelIconOverrideMap)) { + return null; + } + + return componentLabelIconOverrideMap.get(componentName); + } + /** * Test if this package is installed. */ diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 4c4b7e6202f98..bba2f1fcbe38b 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1910,6 +1910,9 @@ com.android.gallery + + + true diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 0fea372ea5803..31b9f0a4354f0 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3959,4 +3959,6 @@ + + diff --git a/services/core/java/com/android/server/pm/ComponentResolver.java b/services/core/java/com/android/server/pm/ComponentResolver.java index f497f114c05fa..f1e14331e33f8 100644 --- a/services/core/java/com/android/server/pm/ComponentResolver.java +++ b/services/core/java/com/android/server/pm/ComponentResolver.java @@ -54,6 +54,7 @@ import android.util.Pair; import android.util.Slog; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.server.IntentResolver; import com.android.server.pm.parsing.PackageInfoUtils; @@ -207,33 +208,57 @@ public class ComponentResolver { } /** Returns the given activity */ - ParsedActivity getActivity(ComponentName component) { + @Nullable + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public ParsedActivity getActivity(@NonNull ComponentName component) { synchronized (mLock) { return mActivities.mActivities.get(component); } } /** Returns the given provider */ - ParsedProvider getProvider(ComponentName component) { + @Nullable + ParsedProvider getProvider(@NonNull ComponentName component) { synchronized (mLock) { return mProviders.mProviders.get(component); } } /** Returns the given receiver */ - ParsedActivity getReceiver(ComponentName component) { + @Nullable + ParsedActivity getReceiver(@NonNull ComponentName component) { synchronized (mLock) { return mReceivers.mActivities.get(component); } } /** Returns the given service */ - ParsedService getService(ComponentName component) { + @Nullable + ParsedService getService(@NonNull ComponentName component) { synchronized (mLock) { return mServices.mServices.get(component); } } + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public boolean componentExists(@NonNull ComponentName componentName) { + synchronized (mLock) { + ParsedMainComponent component = mActivities.mActivities.get(componentName); + if (component != null) { + return true; + } + component = mReceivers.mActivities.get(componentName); + if (component != null) { + return true; + } + component = mServices.mServices.get(componentName); + if (component != null) { + return true; + } + return mProviders.mProviders.get(componentName) != null; + } + } + @Nullable List queryActivities(Intent intent, String resolvedType, int flags, @PrivateResolveFlags int privateResolveFlags, int userId) { diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 8850f29e9bc26..8f63131e13276 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -674,7 +674,7 @@ public class PackageManagerService extends IPackageManager.Stub final ServiceThread mHandlerThread; - final PackageHandler mHandler; + final Handler mHandler; private final ProcessLoggingHandler mProcessLoggingHandler; @@ -1033,6 +1033,61 @@ public class PackageManagerService extends IPackageManager.Stub } } + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static class TestParams { + public ApexManager apexManager; + public @Nullable String appPredictionServicePackage; + public ArtManagerService artManagerService; + public @Nullable String configuratorPackage; + public int defParseFlags; + public DexManager dexManager; + public List dirsToScanAsSystem; + public @Nullable String documenterPackage; + public boolean factoryTest; + public ArrayMap availableFeatures; + public Handler handler; + public ServiceThread handlerThread; + public @Nullable String incidentReportApproverPackage; + public IncrementalManager incrementalManager; + public PackageInstallerService installerService; + public InstantAppRegistry instantAppRegistry; + public InstantAppResolverConnection instantAppResolverConnection; + public ComponentName instantAppResolverSettingsComponent; + public @Nullable IntentFilterVerifier intentFilterVerifier; + public @Nullable ComponentName intentFilterVerifierComponent; + public boolean isPreNmr1Upgrade; + public boolean isPreNupgrade; + public boolean isPreQupgrade; + public boolean isUpgrade; + public DisplayMetrics Metrics; + public ModuleInfoProvider moduleInfoProvider; + public MoveCallbacks moveCallbacks; + public boolean onlyCore; + public OverlayConfig overlayConfig; + public PackageDexOptimizer packageDexOptimizer; + public IPermissionManager permissionManagerService; + public PendingPackageBroadcasts pendingPackageBroadcasts; + public PackageManagerInternal pmInternal; + public ProcessLoggingHandler processLoggingHandler; + public ProtectedPackages protectedPackages; + public @NonNull String requiredInstallerPackage; + public @NonNull String requiredPermissionControllerPackage; + public @NonNull String requiredUninstallerPackage; + public @Nullable String requiredVerifierPackage; + public String[] separateProcesses; + public @NonNull String servicesExtensionPackageName; + public @Nullable String setupWizardPackage; + public @NonNull String sharedSystemSharedLibraryPackageName; + public @Nullable String storageManagerPackage; + public @Nullable String defaultTextClassifierPackage; + public @Nullable String systemTextClassifierPackage; + public ViewCompiler viewCompiler; + public @Nullable String wellbeingPackage; + public @Nullable String retailDemoPackage; + public ComponentName resolveComponentName; + public ArrayMap packages; + } + private final AppsFilter mAppsFilter; final PackageParser2.Callback mPackageParserCallback; @@ -1396,7 +1451,8 @@ public class PackageManagerService extends IPackageManager.Stub } // Set of pending broadcasts for aggregating enable/disable of components. - static class PendingPackageBroadcasts { + @VisibleForTesting(visibility = Visibility.PACKAGE) + public static class PendingPackageBroadcasts { // for each user id, a map of components within that package> final SparseArray>> mUidMap; @@ -1459,7 +1515,7 @@ public class PackageManagerService extends IPackageManager.Stub return map; } } - final PendingPackageBroadcasts mPendingBroadcasts = new PendingPackageBroadcasts(); + final PendingPackageBroadcasts mPendingBroadcasts; static final int SEND_PENDING_BROADCAST = 1; static final int INIT_COPY = 5; @@ -2676,6 +2732,81 @@ public class PackageManagerService extends IPackageManager.Stub } } + /** + * A extremely minimal constructor designed to start up a PackageManagerService instance for + * testing. + * + * It is assumed that all methods under test will mock the internal fields and thus + * none of the initialization is needed. + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + public PackageManagerService(@NonNull Injector injector, @NonNull TestParams testParams) { + mInjector = injector; + mInjector.bootstrap(this); + mAppsFilter = injector.getAppsFilter(); + mComponentResolver = injector.getComponentResolver(); + mContext = injector.getContext(); + mInstaller = injector.getInstaller(); + mInstallLock = injector.getInstallLock(); + mLock = injector.getLock(); + mPermissionManager = injector.getPermissionManagerServiceInternal(); + mSettings = injector.getSettings(); + mUserManager = injector.getUserManagerService(); + + mApexManager = testParams.apexManager; + mArtManagerService = testParams.artManagerService; + mAvailableFeatures = testParams.availableFeatures; + mDefParseFlags = testParams.defParseFlags; + mDexManager = testParams.dexManager; + mDirsToScanAsSystem = testParams.dirsToScanAsSystem; + mFactoryTest = testParams.factoryTest; + mHandler = testParams.handler; + mHandlerThread = testParams.handlerThread; + mIncrementalManager = testParams.incrementalManager; + mInstallerService = testParams.installerService; + mInstantAppRegistry = testParams.instantAppRegistry; + mInstantAppResolverConnection = testParams.instantAppResolverConnection; + mInstantAppResolverSettingsComponent = testParams.instantAppResolverSettingsComponent; + mIntentFilterVerifier = testParams.intentFilterVerifier; + mIntentFilterVerifierComponent = testParams.intentFilterVerifierComponent; + mIsPreNMR1Upgrade = testParams.isPreNmr1Upgrade; + mIsPreNUpgrade = testParams.isPreNupgrade; + mIsPreQUpgrade = testParams.isPreQupgrade; + mIsUpgrade = testParams.isUpgrade; + mMetrics = testParams.Metrics; + mModuleInfoProvider = testParams.moduleInfoProvider; + mMoveCallbacks = testParams.moveCallbacks; + mOnlyCore = testParams.onlyCore; + mOverlayConfig = testParams.overlayConfig; + mPackageDexOptimizer = testParams.packageDexOptimizer; + mPendingBroadcasts = testParams.pendingPackageBroadcasts; + mPermissionManagerService = testParams.permissionManagerService; + mPmInternal = testParams.pmInternal; + mProcessLoggingHandler = testParams.processLoggingHandler; + mProtectedPackages = testParams.protectedPackages; + mSeparateProcesses = testParams.separateProcesses; + mViewCompiler = testParams.viewCompiler; + mRequiredVerifierPackage = testParams.requiredVerifierPackage; + mRequiredInstallerPackage = testParams.requiredInstallerPackage; + mRequiredUninstallerPackage = testParams.requiredUninstallerPackage; + mRequiredPermissionControllerPackage = testParams.requiredPermissionControllerPackage; + mSetupWizardPackage = testParams.setupWizardPackage; + mStorageManagerPackage = testParams.storageManagerPackage; + mDefaultTextClassifierPackage = testParams.defaultTextClassifierPackage; + mSystemTextClassifierPackageName = testParams.systemTextClassifierPackage; + mWellbeingPackage = testParams.wellbeingPackage; + mRetailDemoPackage = testParams.retailDemoPackage; + mDocumenterPackage = testParams.documenterPackage; + mConfiguratorPackage = testParams.configuratorPackage; + mAppPredictionServicePackage = testParams.appPredictionServicePackage; + mIncidentReportApproverPackage = testParams.incidentReportApproverPackage; + mServicesExtensionPackageName = testParams.servicesExtensionPackageName; + mSharedSystemSharedLibraryPackageName = testParams.sharedSystemSharedLibraryPackageName; + + mResolveComponentName = testParams.resolveComponentName; + mPackages.putAll(testParams.packages); + } + public PackageManagerService(Injector injector, boolean onlyCore, boolean factoryTest) { PackageManager.invalidatePackageInfoCache(); PackageManager.disableApplicationInfoCache(); @@ -2683,6 +2814,8 @@ public class PackageManagerService extends IPackageManager.Stub final TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG + "Timing", Trace.TRACE_TAG_PACKAGE_MANAGER); + mPendingBroadcasts = new PendingPackageBroadcasts(); + mInjector = injector; mInjector.bootstrap(this); mLock = injector.getLock(); @@ -5168,7 +5301,7 @@ public class PackageManagerService extends IPackageManager.Stub AndroidPackage pkg = a == null ? null : mPackages.get(a.getPackageName()); if (pkg != null && mSettings.isEnabledAndMatchLPr(pkg, a, flags, userId)) { - PackageSetting ps = mSettings.mPackages.get(component.getPackageName()); + PackageSetting ps = mSettings.getPackageLPr(component.getPackageName()); if (ps == null) return null; if (shouldFilterApplicationLocked( ps, filterCallingUid, component, TYPE_ACTIVITY, userId)) { @@ -15609,6 +15742,12 @@ public class PackageManagerService extends IPackageManager.Stub // these install state changes will be persisted in the // upcoming call to mSettings.writeLPr(). } + + if (allUsers != null) { + for (int currentUserId : allUsers) { + ps.resetOverrideComponentLabelIcon(currentUserId); + } + } } // Retrieve the overlays for shared libraries of the package. @@ -20176,6 +20315,86 @@ public class PackageManagerService extends IPackageManager.Stub } } + @Override + public void overrideLabelAndIcon(@NonNull ComponentName componentName, + @NonNull String nonLocalizedLabel, int icon, int userId) { + if (TextUtils.isEmpty(nonLocalizedLabel)) { + throw new IllegalArgumentException("Override label should be a valid String"); + } + updateComponentLabelIcon(componentName, nonLocalizedLabel, icon, userId); + } + + @Override + public void restoreLabelAndIcon(@NonNull ComponentName componentName, int userId) { + updateComponentLabelIcon(componentName, null, null, userId); + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public void updateComponentLabelIcon(/*@NonNull*/ ComponentName componentName, + @Nullable String nonLocalizedLabel, @Nullable Integer icon, int userId) { + if (componentName == null) { + throw new IllegalArgumentException("Must specify a component"); + } + + boolean componentExists = mComponentResolver.componentExists(componentName); + if (!componentExists) { + throw new IllegalArgumentException("Component " + componentName + " not found"); + } + + int callingUid = Binder.getCallingUid(); + + String componentPkgName = componentName.getPackageName(); + int componentUid = getPackageUid(componentPkgName, 0, userId); + if (!UserHandle.isSameApp(callingUid, componentUid)) { + throw new SecurityException("The calling UID (" + callingUid + ")" + + " does not match the target UID"); + } + + String allowedCallerPkg = mContext.getString(R.string.config_overrideComponentUiPackage); + if (TextUtils.isEmpty(allowedCallerPkg)) { + throw new SecurityException( + "There is no package defined as allowed to change a component's label or icon"); + } + + int allowedCallerUid = getPackageUid(allowedCallerPkg, PackageManager.MATCH_SYSTEM_ONLY, + userId); + if (allowedCallerUid == -1 || !UserHandle.isSameApp(callingUid, allowedCallerUid)) { + throw new SecurityException("The calling UID (" + callingUid + ")" + + " is not allowed to change a component's label or icon"); + } + + synchronized (mLock) { + AndroidPackage pkg = mPackages.get(componentPkgName); + PackageSetting pkgSetting = getPackageSetting(componentPkgName); + if (pkg == null || pkgSetting == null + || (!pkg.isSystem() && !pkgSetting.getPkgState().isUpdatedSystemApp())) { + throw new SecurityException( + "Changing the label is not allowed for " + componentName); + } + + if (!pkgSetting.overrideNonLocalizedLabelAndIcon(componentName, nonLocalizedLabel, + icon, userId)) { + // Nothing changed + return; + } + } + + ArrayList components = mPendingBroadcasts.get(userId, componentPkgName); + if (components == null) { + components = new ArrayList<>(); + mPendingBroadcasts.put(userId, componentPkgName, components); + } + + String className = componentName.getClassName(); + if (!components.contains(className)) { + components.add(className); + } + + if (!mHandler.hasMessages(SEND_PENDING_BROADCAST)) { + mHandler.sendEmptyMessageDelayed(SEND_PENDING_BROADCAST, BROADCAST_DELAY); + } + } + @Override public void setComponentEnabledSetting(ComponentName componentName, int newState, int flags, int userId) { diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index 9a8692d029e04..8eb391454df8d 100644 --- a/services/core/java/com/android/server/pm/PackageSetting.java +++ b/services/core/java/com/android/server/pm/PackageSetting.java @@ -26,6 +26,7 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.proto.ProtoOutputStream; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.pm.parsing.pkg.AndroidPackage; import com.android.server.pm.permission.PermissionsState; import com.android.server.pm.pkg.PackageStateUnserialized; @@ -68,7 +69,8 @@ public class PackageSetting extends PackageSettingBase { @NonNull private PackageStateUnserialized pkgState = new PackageStateUnserialized(); - PackageSetting(String name, String realName, File codePath, File resourcePath, + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public PackageSetting(String name, String realName, File codePath, File resourcePath, String legacyNativeLibraryPathString, String primaryCpuAbiString, String secondaryCpuAbiString, String cpuAbiOverrideString, long pVersionCode, int pkgFlags, int privateFlags, diff --git a/services/core/java/com/android/server/pm/PackageSettingBase.java b/services/core/java/com/android/server/pm/PackageSettingBase.java index 7cb3df5a03504..00a5fe7665937 100644 --- a/services/core/java/com/android/server/pm/PackageSettingBase.java +++ b/services/core/java/com/android/server/pm/PackageSettingBase.java @@ -21,6 +21,9 @@ import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.ComponentName; import android.content.pm.ApplicationInfo; import android.content.pm.IntentFilterVerificationInfo; import android.content.pm.PackageManager; @@ -697,6 +700,26 @@ public abstract class PackageSettingBase extends SettingBase { return userState.harmfulAppWarning; } + /** + * @see PackageUserState#overrideLabelAndIcon(ComponentName, String, Integer) + * + * @param userId the specific user to change the label/icon for + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public boolean overrideNonLocalizedLabelAndIcon(@NonNull ComponentName component, + @Nullable String label, @Nullable Integer icon, @UserIdInt int userId) { + return modifyUserState(userId).overrideLabelAndIcon(component, label, icon); + } + + /** + * @see PackageUserState#resetOverrideComponentLabelIcon() + * + * @param userId the specific user to reset + */ + public void resetOverrideComponentLabelIcon(@UserIdInt int userId) { + modifyUserState(userId).resetOverrideComponentLabelIcon(); + } + protected PackageSettingBase updateFrom(PackageSettingBase other) { super.copyFrom(other); this.codePath = other.codePath; diff --git a/services/core/java/com/android/server/pm/SettingBase.java b/services/core/java/com/android/server/pm/SettingBase.java index ec9746dabcebb..3e2ab05e83ecf 100644 --- a/services/core/java/com/android/server/pm/SettingBase.java +++ b/services/core/java/com/android/server/pm/SettingBase.java @@ -18,9 +18,11 @@ package com.android.server.pm; import android.content.pm.ApplicationInfo; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.pm.permission.PermissionsState; -abstract class SettingBase { +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +public abstract class SettingBase { int pkgFlags; int pkgPrivateFlags; diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index f6ca87df482ff..091535dfc792a 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -91,6 +91,7 @@ import android.util.Xml; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BackgroundThread; import com.android.internal.util.ArrayUtils; import com.android.internal.util.CollectionUtils; @@ -418,6 +419,21 @@ public final class Settings { /** Settings and other information about permissions */ final PermissionSettings mPermissions; + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + public Settings(Map pkgSettings) { + mLock = new Object(); + mPackages.putAll(pkgSettings); + mSystemDir = null; + mPermissions = null; + mRuntimePermissionsPersistence = null; + mSettingsFilename = null; + mBackupSettingsFilename = null; + mPackageListFilename = null; + mStoppedPackagesFilename = null; + mBackupStoppedPackagesFilename = null; + mKernelMappingFilename = null; + } + Settings(File dataDir, PermissionSettings permission, Object lock) { mLock = lock; @@ -4328,8 +4344,9 @@ public final class Settings { return userState.isMatch(componentInfo, flags); } - boolean isEnabledAndMatchLPr(AndroidPackage pkg, ParsedMainComponent component, int flags, - int userId) { + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public boolean isEnabledAndMatchLPr(AndroidPackage pkg, ParsedMainComponent component, + int flags, int userId) { final PackageSetting ps = mPackages.get(component.getPackageName()); if (ps == null) return false; diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java index 5a1e8e2661b8d..137e0aa831d66 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java +++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java @@ -48,6 +48,7 @@ import android.content.pm.parsing.component.ParsedService; import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.Pair; import android.util.Slog; import com.android.internal.util.ArrayUtils; @@ -271,7 +272,7 @@ public class PackageInfoUtils { ActivityInfo info = PackageInfoWithoutStateUtils.generateActivityInfoUnchecked(a, applicationInfo); - assignSharedFieldsForComponentInfo(info, a, pkgSetting); + assignSharedFieldsForComponentInfo(info, a, pkgSetting, userId); return info; } @@ -306,7 +307,7 @@ public class PackageInfoUtils { ServiceInfo info = PackageInfoWithoutStateUtils.generateServiceInfoUnchecked(s, applicationInfo); - assignSharedFieldsForComponentInfo(info, s, pkgSetting); + assignSharedFieldsForComponentInfo(info, s, pkgSetting, userId); return info; } @@ -333,7 +334,7 @@ public class PackageInfoUtils { } ProviderInfo info = PackageInfoWithoutStateUtils.generateProviderInfoUnchecked(p, flags, applicationInfo); - assignSharedFieldsForComponentInfo(info, p, pkgSetting); + assignSharedFieldsForComponentInfo(info, p, pkgSetting, userId); return info; } @@ -358,7 +359,7 @@ public class PackageInfoUtils { info.nativeLibraryDir = pkg.getNativeLibraryDir(); info.secondaryNativeLibraryDir = pkg.getSecondaryNativeLibraryDir(); - assignStateFieldsForPackageItemInfo(info, i, pkgSetting); + assignStateFieldsForPackageItemInfo(info, i, pkgSetting, userId); return info; } @@ -426,8 +427,9 @@ public class PackageInfoUtils { } private static void assignSharedFieldsForComponentInfo(@NonNull ComponentInfo componentInfo, - @NonNull ParsedMainComponent mainComponent, @Nullable PackageSetting pkgSetting) { - assignStateFieldsForPackageItemInfo(componentInfo, mainComponent, pkgSetting); + @NonNull ParsedMainComponent mainComponent, @Nullable PackageSetting pkgSetting, + int userId) { + assignStateFieldsForPackageItemInfo(componentInfo, mainComponent, pkgSetting, userId); componentInfo.descriptionRes = mainComponent.getDescriptionRes(); componentInfo.directBootAware = mainComponent.isDirectBootAware(); componentInfo.enabled = mainComponent.isEnabled(); @@ -436,8 +438,12 @@ public class PackageInfoUtils { private static void assignStateFieldsForPackageItemInfo( @NonNull PackageItemInfo packageItemInfo, @NonNull ParsedComponent component, - @Nullable PackageSetting pkgSetting) { - // TODO(b/135203078): Add setting related state + @Nullable PackageSetting pkgSetting, int userId) { + Pair labelAndIcon = + ParsedComponentStateUtils.getNonLocalizedLabelAndIcon(component, pkgSetting, + userId); + packageItemInfo.nonLocalizedLabel = labelAndIcon.first; + packageItemInfo.icon = labelAndIcon.second; } @CheckResult diff --git a/services/core/java/com/android/server/pm/parsing/ParsedComponentStateUtils.java b/services/core/java/com/android/server/pm/parsing/ParsedComponentStateUtils.java new file mode 100644 index 0000000000000..54466ac8f26b2 --- /dev/null +++ b/services/core/java/com/android/server/pm/parsing/ParsedComponentStateUtils.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2020 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.parsing; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.pm.parsing.component.ParsedComponent; +import android.util.Pair; + +import com.android.server.pm.PackageSetting; + +/** + * For exposing internal fields to the rest of the server, enforcing that any overridden state from + * a {@link com.android.server.pm.PackageSetting} is applied. + * + * TODO(chiuwinson): The fields on ParsedComponent are not actually hidden. Will need to find a + * way to enforce the mechanism now that they exist in core instead of server. Can't rely on + * package-private. + * + * @hide + */ +public class ParsedComponentStateUtils { + + @NonNull + public static Pair getNonLocalizedLabelAndIcon(ParsedComponent component, + @Nullable PackageSetting pkgSetting, int userId) { + CharSequence label = component.getNonLocalizedLabel(); + int icon = component.getIcon(); + + Pair overrideLabelIcon = pkgSetting == null ? null : + pkgSetting.readUserState(userId) + .getOverrideLabelIconForComponent(component.getComponentName()); + if (overrideLabelIcon != null) { + if (overrideLabelIcon.first != null) { + label = overrideLabelIcon.first; + } + if (overrideLabelIcon.second != null) { + icon = overrideLabelIcon.second; + } + } + + return Pair.create(label, icon); + } +} diff --git a/services/tests/PackageManagerComponentOverrideTests/Android.bp b/services/tests/PackageManagerComponentOverrideTests/Android.bp new file mode 100644 index 0000000000000..a2668a184fe06 --- /dev/null +++ b/services/tests/PackageManagerComponentOverrideTests/Android.bp @@ -0,0 +1,40 @@ +// +// 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. +// + +// NOTE: This test is separate from service tests since it relies on same vs different calling UID, +// and this is more representative of a real caller. It also uses Mockito extended, and this +// prevents converting the entire services test module. +android_test { + name: "PackageManagerComponentOverrideTests", + srcs: [ + "src/**/*.kt" + ], + static_libs: [ + "androidx.test.runner", + "mockito-target-extended-minus-junit4", + "services.core", + "servicestests-utils-mockito-extended", + "testng", // TODO: remove once Android migrates to JUnit 4.12, which provides assertThrows + "truth-prebuilt", + ], + + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + test_suites: ["device-tests"], + platform_apis: true, +} diff --git a/services/tests/PackageManagerComponentOverrideTests/AndroidManifest.xml b/services/tests/PackageManagerComponentOverrideTests/AndroidManifest.xml new file mode 100644 index 0000000000000..c25e1122c730d --- /dev/null +++ b/services/tests/PackageManagerComponentOverrideTests/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/services/tests/PackageManagerComponentOverrideTests/AndroidTest.xml b/services/tests/PackageManagerComponentOverrideTests/AndroidTest.xml new file mode 100644 index 0000000000000..b83b1a8fb1133 --- /dev/null +++ b/services/tests/PackageManagerComponentOverrideTests/AndroidTest.xml @@ -0,0 +1,29 @@ + + + + diff --git a/services/tests/PackageManagerComponentOverrideTests/res/drawable/black16x16.png b/services/tests/PackageManagerComponentOverrideTests/res/drawable/black16x16.png new file mode 100644 index 0000000000000..86b12fca81cc6 Binary files /dev/null and b/services/tests/PackageManagerComponentOverrideTests/res/drawable/black16x16.png differ diff --git a/services/tests/PackageManagerComponentOverrideTests/res/drawable/white16x16.png b/services/tests/PackageManagerComponentOverrideTests/res/drawable/white16x16.png new file mode 100644 index 0000000000000..49dbb4fd7a463 Binary files /dev/null and b/services/tests/PackageManagerComponentOverrideTests/res/drawable/white16x16.png differ diff --git a/services/tests/PackageManagerComponentOverrideTests/res/values/values.xml b/services/tests/PackageManagerComponentOverrideTests/res/values/values.xml new file mode 100644 index 0000000000000..73d11281c96f0 --- /dev/null +++ b/services/tests/PackageManagerComponentOverrideTests/res/values/values.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt new file mode 100644 index 0000000000000..ecdb30f5e84b6 --- /dev/null +++ b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2020 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.override + +import android.content.ComponentName +import android.content.Context +import android.content.pm.parsing.component.ParsedActivity +import android.os.Binder +import android.os.UserHandle +import android.util.ArrayMap +import com.android.server.pm.AppsFilter +import com.android.server.pm.ComponentResolver +import com.android.server.pm.PackageManagerService +import com.android.server.pm.PackageSetting +import com.android.server.pm.Settings +import com.android.server.pm.UserManagerService +import com.android.server.pm.parsing.pkg.AndroidPackage +import com.android.server.pm.parsing.pkg.PackageImpl +import com.android.server.pm.parsing.pkg.ParsedPackage +import com.android.server.pm.permission.PermissionManagerServiceInternal +import com.android.server.pm.test.override.PackageManagerComponentLabelIconOverrideTest.Companion.Params.AppType +import com.android.server.pm.test.override.R +import com.android.server.testutils.TestHandler +import com.android.server.testutils.mock +import com.android.server.testutils.mockThrowOnUnmocked +import com.android.server.testutils.spy +import com.android.server.testutils.whenever +import com.android.server.wm.ActivityTaskManagerInternal +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.mockito.Mockito.any +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.anyString +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.intThat +import org.mockito.Mockito.never +import org.mockito.Mockito.same +import org.mockito.Mockito.verify +import org.testng.Assert.assertThrows +import java.io.File + +@RunWith(Parameterized::class) +class PackageManagerComponentLabelIconOverrideTest { + + companion object { + private const val VALID_PKG = "com.android.server.pm.test.override" + private const val SHARED_PKG = "com.android.server.pm.test.override.shared" + private const val INVALID_PKG = "com.android.server.pm.test.override.invalid" + + private const val SEND_PENDING_BROADCAST = 1 // PackageManagerService.SEND_PENDING_BROADCAST + + private const val DEFAULT_LABEL = "DefaultLabel" + private const val TEST_LABEL = "TestLabel" + + private const val DEFAULT_ICON = R.drawable.black16x16 + private const val TEST_ICON = R.drawable.white16x16 + + private const val COMPONENT_CLASS_NAME = ".TestComponent" + + sealed class Result { + // Component label/icon changed, message sent to send broadcast + object Changed : Result() + + // Component label/icon changed, message was pending, not re-sent + object ChangedWithoutNotify : Result() + + // Component label/icon did not changed, was already equivalent + object NotChanged : Result() + + // Updating label/icon encountered a specific exception + data class Exception(val type: Class) : Result() + } + + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun parameters() = arrayOf( + // Start with an array of the simplest known inputs and expected outputs + Params(VALID_PKG, AppType.SYSTEM_APP, Result.Changed), + Params(SHARED_PKG, AppType.SYSTEM_APP, Result.Changed), + Params(INVALID_PKG, AppType.SYSTEM_APP, SecurityException::class.java) + ) + .flatMap { param -> + mutableListOf(param).apply { + if (param.result is Result.Changed) { + // For each param that would've succeeded, also verify that if a change + // happened, but a message was pending, another is not re-queued/reset + this += param.copy(result = Result.ChangedWithoutNotify) + // Also verify that when the component is already configured, no change + // is propagated + this += param.copy(result = Result.NotChanged) + } + // For all params, verify that an invalid component will cause an + // IllegalArgumentException, instead of result initially specified + this += param.copy(componentName = null, + result = Result.Exception(IllegalArgumentException::class.java)) + // Also verify an updated system app variant, which should have the same + // result as a vanilla system app + this += param.copy(appType = AppType.UPDATED_SYSTEM_APP) + // Also verify a non-system app will cause a failure, since normal apps + // are not allowed to edit their label/icon + this += param.copy(appType = AppType.NORMAL_APP, + result = Result.Exception(SecurityException::class.java)) + } + } + + data class Params( + val pkgName: String, + private val appType: AppType, + val result: Result, + val componentName: ComponentName? = ComponentName(pkgName, COMPONENT_CLASS_NAME) + ) { + constructor(pkgName: String, appType: AppType, exception: Class) + : this(pkgName, appType, Result.Exception(exception)) + + val expectedLabel = when (result) { + Result.Changed, Result.ChangedWithoutNotify, Result.NotChanged -> TEST_LABEL + is Result.Exception -> DEFAULT_LABEL + } + + val expectedIcon = when (result) { + Result.Changed, Result.ChangedWithoutNotify, Result.NotChanged -> TEST_ICON + is Result.Exception -> DEFAULT_ICON + } + + val isUpdatedSystemApp = appType == AppType.UPDATED_SYSTEM_APP + val isSystem = appType == AppType.SYSTEM_APP || isUpdatedSystemApp + + override fun toString(): String { + val resultString = when (result) { + Result.Changed -> "Changed" + Result.ChangedWithoutNotify -> "ChangedWithoutNotify" + Result.NotChanged -> "NotChanged" + is Result.Exception -> result.type.simpleName + } + + // Nicer formatting for the test method suffix + return "pkg=$pkgName, type=$appType, component=$componentName, result=$resultString" + } + + enum class AppType { SYSTEM_APP, UPDATED_SYSTEM_APP, NORMAL_APP } + } + } + + @Parameterized.Parameter(0) + lateinit var params: Params + + private lateinit var testHandler: TestHandler + private lateinit var mockPendingBroadcasts: PackageManagerService.PendingPackageBroadcasts + private lateinit var mockPkg: AndroidPackage + private lateinit var mockPkgSetting: PackageSetting + private lateinit var service: PackageManagerService + + private val userId = UserHandle.getCallingUserId() + private val userIdDifferent = userId + 1 + + @Before + fun setUpMocks() { + makeTestData() + + testHandler = TestHandler(null) + if (params.result is Result.ChangedWithoutNotify) { + // Case where the handler already has a message and so another should not be sent. + // This case will verify that only 1 message exists, which is the one added here. + testHandler.sendEmptyMessage(SEND_PENDING_BROADCAST) + } + + mockPendingBroadcasts = PackageManagerService.PendingPackageBroadcasts() + + service = mockService() + } + + @Test + fun updateComponentLabelIcon() { + fun runUpdate() { + service.updateComponentLabelIcon(params.componentName, TEST_LABEL, TEST_ICON, userId) + } + + when (val result = params.result) { + Result.Changed, Result.ChangedWithoutNotify, Result.NotChanged -> { + runUpdate() + verify(mockPkgSetting).overrideNonLocalizedLabelAndIcon(params.componentName!!, + TEST_LABEL, TEST_ICON, userId) + } + is Result.Exception -> { + assertThrows(result.type) { runUpdate() } + verify(mockPkgSetting, never()).overrideNonLocalizedLabelAndIcon( + any(), any(), anyInt(), anyInt()) + } + } + } + + @After + fun verifyExpectedResult() { + if (params.componentName != null) { + val activityInfo = service.getActivityInfo(params.componentName, 0, userId) + assertThat(activityInfo.nonLocalizedLabel).isEqualTo(params.expectedLabel) + assertThat(activityInfo.icon).isEqualTo(params.expectedIcon) + } + } + + @After + fun verifyDifferentUserUnchanged() { + when (params.result) { + Result.Changed, Result.ChangedWithoutNotify -> { + val activityInfo = service.getActivityInfo(params.componentName, 0, userIdDifferent) + assertThat(activityInfo.nonLocalizedLabel).isEqualTo(DEFAULT_LABEL) + assertThat(activityInfo.icon).isEqualTo(DEFAULT_ICON) + } + Result.NotChanged, is Result.Exception -> {} + }.run { /*exhaust*/ } + } + + @After + fun verifyHandlerHasMessage() { + when (params.result) { + is Result.Changed, is Result.ChangedWithoutNotify -> { + assertThat(testHandler.pendingMessages).hasSize(1) + assertThat(testHandler.pendingMessages.first().message.what) + .isEqualTo(SEND_PENDING_BROADCAST) + } + is Result.NotChanged, is Result.Exception -> { + assertThat(testHandler.pendingMessages).hasSize(0) + } + }.run { /*exhaust*/ } + } + + @After + fun verifyPendingBroadcast() { + when (params.result) { + is Result.Changed, Result.ChangedWithoutNotify -> { + assertThat(mockPendingBroadcasts.get(userId, params.pkgName)) + .containsExactly(params.componentName!!.className) + .inOrder() + } + is Result.NotChanged, is Result.Exception -> { + assertThat(mockPendingBroadcasts.get(userId, params.pkgName)).isNull() + } + }.run { /*exhaust*/ } + } + + private fun makePkg(pkgName: String, block: ParsedPackage.() -> Unit = {}) = + PackageImpl.forTesting(pkgName) + .setEnabled(true) + .let { it.hideAsParsed() as ParsedPackage } + .setSystem(params.isSystem) + .apply(block) + .hideAsFinal() + + private fun makePkgSetting(pkgName: String) = spy(PackageSetting(pkgName, null, File("/test"), + File("/test"), null, null, null, null, 0, 0, 0, 0, null, null, null)) { + this.pkgState.isUpdatedSystemApp = params.isUpdatedSystemApp + } + + private fun makeTestData() { + mockPkg = makePkg(params.pkgName) + mockPkgSetting = makePkgSetting(params.pkgName) + + if (params.result is Result.NotChanged) { + // If verifying no-op behavior, set the current setting to the test values + mockPkgSetting.overrideNonLocalizedLabelAndIcon(params.componentName!!, TEST_LABEL, + TEST_ICON, userId) + // Then clear the mock because the line above just incremented it + clearInvocations(mockPkgSetting) + } + } + + private fun mockService(): PackageManagerService { + val mockedPkgs = mapOf( + // Must use the test app's UID so that PMS can match them when querying, since + // the static Binder.getCallingUid can't mocked as it's marked final + VALID_PKG to makePkg(VALID_PKG) { uid = Binder.getCallingUid() }, + SHARED_PKG to makePkg(SHARED_PKG) { uid = Binder.getCallingUid() }, + INVALID_PKG to makePkg(INVALID_PKG) { uid = Binder.getCallingUid() + 1 } + ) + val mockedPkgSettings = mapOf( + VALID_PKG to makePkgSetting(VALID_PKG), + SHARED_PKG to makePkgSetting(SHARED_PKG), + INVALID_PKG to makePkgSetting(INVALID_PKG) + ) + // Add pkgSetting under test so its attributes override the defaults added above + .plus(params.pkgName to mockPkgSetting) + + val mockActivity: ParsedActivity = mock { + whenever(this.packageName) { params.pkgName } + whenever(this.nonLocalizedLabel) { DEFAULT_LABEL } + whenever(this.icon) { DEFAULT_ICON } + whenever(this.componentName) { params.componentName } + whenever(this.name) { params.componentName?.className } + whenever(this.isEnabled) { true } + whenever(this.isDirectBootAware) { params.isSystem } + } + + val mockSettings = Settings(mockedPkgSettings) + val mockComponentResolver: ComponentResolver = mockThrowOnUnmocked { + params.componentName?.let { + whenever(this.componentExists(same(it))) { true } + whenever(this.getActivity(same(it))) { mockActivity } + } + } + val mockUserManagerService: UserManagerService = mockThrowOnUnmocked { + val matcher: (Int) -> Boolean = { it == userId || it == userIdDifferent } + whenever(this.exists(intThat(matcher))) { true } + whenever(this.isUserUnlockingOrUnlocked(intThat(matcher))) { true } + } + val mockPermissionManagerService: PermissionManagerServiceInternal = mockThrowOnUnmocked { + whenever(this.enforceCrossUserPermission(anyInt(), anyInt(), anyBoolean(), anyBoolean(), + anyString())) { } + } + val mockActivityTaskManager: ActivityTaskManagerInternal = mockThrowOnUnmocked { + whenever(this.isCallerRecents(anyInt())) { false } + } + val mockAppsFilter: AppsFilter = mockThrowOnUnmocked { + whenever(this.shouldFilterApplication(anyInt(), any(), + any(), anyInt())) { false } + } + val mockContext: Context = mockThrowOnUnmocked { + whenever(this.getString( + com.android.internal.R.string.config_overrideComponentUiPackage)) { VALID_PKG } + } + val mockInjector: PackageManagerService.Injector = mock { + whenever(this.lock) { Object() } + whenever(this.componentResolver) { mockComponentResolver } + whenever(this.userManagerService) { mockUserManagerService } + whenever(this.permissionManagerServiceInternal) { mockPermissionManagerService } + whenever(this.settings) { mockSettings } + whenever(this.activityTaskManagerInternal) { mockActivityTaskManager } + whenever(this.appsFilter) { mockAppsFilter } + whenever(this.context) { mockContext } + } + val testParams = PackageManagerService.TestParams().apply { + this.handler = testHandler + this.pendingPackageBroadcasts = mockPendingBroadcasts + this.resolveComponentName = ComponentName("android", ".Test") + this.packages = ArrayMap().apply { putAll(mockedPkgs) } + } + + return PackageManagerService(mockInjector, testParams) + } +} diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index e58e911799313..b457856e86308 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -107,6 +107,8 @@ java_library { name: "servicestests-utils", srcs: [ "utils/**/*.java", + "utils/**/*.kt", + "utils-mockito/**/*.kt", ], static_libs: [ "junit", @@ -117,6 +119,22 @@ java_library { ], } +java_library { + name: "servicestests-utils-mockito-extended", + srcs: [ + "utils/**/*.java", + "utils/**/*.kt", + "utils-mockito/**/*.kt", + ], + static_libs: [ + "junit", + "mockito-target-extended-minus-junit4", + ], + libs: [ + "android.test.runner", + ], +} + filegroup { name: "servicestests-SuspendTestApp-files", srcs: [ diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayReferenceMapperTests.kt b/services/tests/servicestests/src/com/android/server/om/OverlayReferenceMapperTests.kt index 063cd5dacc93d..78c708084d382 100644 --- a/services/tests/servicestests/src/com/android/server/om/OverlayReferenceMapperTests.kt +++ b/services/tests/servicestests/src/com/android/server/om/OverlayReferenceMapperTests.kt @@ -18,6 +18,8 @@ package com.android.server.om import android.net.Uri import com.android.server.pm.parsing.pkg.AndroidPackage +import com.android.server.testutils.mockThrowOnUnmocked +import com.android.server.testutils.whenever import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingTestBase.kt b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingTestBase.kt index f532dd87909e1..d7b02f44ed26a 100644 --- a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingTestBase.kt +++ b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingTestBase.kt @@ -31,12 +31,12 @@ import android.os.Debug import android.os.Environment import android.util.SparseArray import androidx.test.platform.app.InstrumentationRegistry -import com.android.server.om.mockThrowOnUnmocked -import com.android.server.om.whenever import com.android.server.pm.PackageManagerService import com.android.server.pm.PackageSetting import com.android.server.pm.parsing.pkg.AndroidPackage import com.android.server.pm.pkg.PackageStateUnserialized +import com.android.server.testutils.mockThrowOnUnmocked +import com.android.server.testutils.whenever import org.junit.BeforeClass import org.mockito.Mockito import org.mockito.Mockito.anyInt diff --git a/services/tests/servicestests/src/com/android/server/om/MockitoUtils.kt b/services/tests/servicestests/utils-mockito/com/android/server/testutils/MockitoUtils.kt similarity index 75% rename from services/tests/servicestests/src/com/android/server/om/MockitoUtils.kt rename to services/tests/servicestests/utils-mockito/com/android/server/testutils/MockitoUtils.kt index 0f915dbdcf6fd..056fa886f6402 100644 --- a/services/tests/servicestests/src/com/android/server/om/MockitoUtils.kt +++ b/services/tests/servicestests/utils-mockito/com/android/server/testutils/MockitoUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * Copyright (C) 2020 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.om +package com.android.server.testutils import org.mockito.Answers import org.mockito.Mockito @@ -31,6 +31,13 @@ object MockitoUtils { else -> { val arguments = it.arguments ?.takeUnless { it.isEmpty() } + ?.mapIndexed { index, arg -> + try { + arg?.toString() + } catch (e: Exception) { + "toString[$index] threw ${e.message}" + } + } ?.joinToString() ?.let { "with $it" @@ -46,6 +53,8 @@ object MockitoUtils { inline fun mock(block: T.() -> Unit = {}) = Mockito.mock(T::class.java).apply(block) +fun spy(value: T, block: T.() -> Unit = {}) = Mockito.spy(value).apply(block) + fun Stubber.whenever(mock: Type) = Mockito.`when`(mock) fun whenever(mock: Type) = Mockito.`when`(mock) @@ -55,7 +64,7 @@ fun whenever(mock: Type, block: InvocationOnMock.() -> Any?) = fun whenever(mock: Unit) = Mockito.`when`(mock).thenAnswer { } -inline fun mockThrowOnUnmocked(block: T.() -> Unit): T { +inline fun spyThrowOnUnmocked(value: T?, block: T.() -> Unit): T { val swappingAnswer = object : Answer { var delegate: Answer<*> = Answers.RETURNS_DEFAULTS @@ -64,9 +73,12 @@ inline fun mockThrowOnUnmocked(block: T.() -> Unit): T { } } - return Mockito.mock(T::class.java, swappingAnswer).apply(block) + return Mockito.mock(T::class.java, Mockito.withSettings().spiedInstance(value) + .defaultAnswer(swappingAnswer)).apply(block) .also { // To allow when() usage inside block, only swap to throwing afterwards swappingAnswer.delegate = MockitoUtils.ANSWER_THROWS } } + +inline fun mockThrowOnUnmocked(block: T.() -> Unit) = spyThrowOnUnmocked(null, block) diff --git a/services/tests/servicestests/utils/com/android/server/testutils/TestHandler.java b/services/tests/servicestests/utils/com/android/server/testutils/TestHandler.java index 69db38438609c..355e7f333bb77 100644 --- a/services/tests/servicestests/utils/com/android/server/testutils/TestHandler.java +++ b/services/tests/servicestests/utils/com/android/server/testutils/TestHandler.java @@ -139,7 +139,7 @@ public class TestHandler extends Handler { } } - private class MsgInfo implements Comparable { + public class MsgInfo implements Comparable { public final Message message; public final long sendTime; public final RuntimeException postPoint;