diff options
| author | 2020-04-01 18:20:51 +0000 | |
|---|---|---|
| committer | 2020-04-01 19:17:53 +0000 | |
| commit | 8e865ecd066d05bdb1ff23f3337b39c8a558555f (patch) | |
| tree | bb0aa7e6909dcd4c7e3126e960b12047b8134012 | |
| parent | 1867bc81e2551f71c6f5e2d335c5edc664a4186c (diff) | |
Revert "Revert "Allow overriding the label and icon of a MainCom..."
Revert submission 10921255-revert-10403399-pkg-override-label-icon-DCRMJNYAKW
Reason for revert: Re-submit broken change with included fix.
Reverted Changes:
Ic5c719cbb:Revert "Allow overriding the label and icon of a M...
Id9d37e661:Revert "Add test constructor to PackageManagerServ...
Bug: 113638339
Change-Id: I4f5247f528f62c175bd34f0e4cb16c9456c8afe4
24 files changed, 862 insertions, 24 deletions
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 5bad055810cc..8bebafff37f0 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -306,6 +306,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}. */ @UnsupportedAppUsage diff --git a/core/java/android/content/pm/PackageUserState.java b/core/java/android/content/pm/PackageUserState.java index 61b1553e28a8..327d1b8beeb1 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<String, String[]> sharedLibraryOverlayPaths; // Lib name to overlay paths private String[] cachedOverlayPaths; + @Nullable + private ArrayMap<ComponentName, Pair<String, Integer>> 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() { @@ -147,6 +159,65 @@ public class PackageUserState { } /** + * 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<String, Integer> 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<String, Integer> getOverrideLabelIconForComponent(ComponentName componentName) { + if (ArrayUtils.isEmpty(componentLabelIconOverrideMap)) { + return null; + } + + return componentLabelIconOverrideMap.get(componentName); + } + + /** * Test if this package is installed. */ public boolean isAvailable(int flags) { diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 4c4b7e6202f9..bba2f1fcbe38 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1910,6 +1910,9 @@ <!-- The name of the package that will hold the system gallery role. --> <string name="config_systemGallery" translatable="false">com.android.gallery</string> + <!-- The name of the package that will be allowed to change its components' label/icon. --> + <string name="config_overrideComponentUiPackage" translatable="false"></string> + <!-- Enable/disable default bluetooth profiles: HSP_AG, ObexObjectPush, Audio, NAP --> <bool name="config_bluetooth_default_profiles">true</bool> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 42718cba0fc9..ec8058235912 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3961,4 +3961,6 @@ <!-- Set to true to make assistant show in front of the dream/screensaver. --> <java-symbol type="bool" name="config_assistantOnTopOfDream"/> + + <java-symbol type="string" name="config_overrideComponentUiPackage" /> </resources> diff --git a/services/core/java/com/android/server/pm/ComponentResolver.java b/services/core/java/com/android/server/pm/ComponentResolver.java index f497f114c05f..f1e14331e33f 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<ResolveInfo> 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 0965878b23dd..8f63131e1327 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -5301,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)) { @@ -15742,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. @@ -20310,6 +20316,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<String> 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) { if (!mUserManager.exists(userId)) return; diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index 9a8692d029e0..8eb391454df8 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 7cb3df5a0350..00a5fe766593 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 ec9746dabceb..3e2ab05e83ec 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 f6ca87df482f..091535dfc792 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<String, PackageSetting> 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 5a1e8e2661b8..137e0aa831d6 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<CharSequence, Integer> 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 000000000000..54466ac8f26b --- /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<CharSequence, Integer> getNonLocalizedLabelAndIcon(ParsedComponent component, + @Nullable PackageSetting pkgSetting, int userId) { + CharSequence label = component.getNonLocalizedLabel(); + int icon = component.getIcon(); + + Pair<String, Integer> 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 000000000000..a2668a184fe0 --- /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 000000000000..c25e1122c730 --- /dev/null +++ b/services/tests/PackageManagerComponentOverrideTests/AndroidManifest.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.pm.test.override"> + + <uses-sdk + android:minSdkVersion="1" + android:targetSdkVersion="28" /> + + <application android:debuggable="true"> + <uses-library android:name="android.test.mock" android:required="true" /> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:label="PackageManagerComponentOverrideTests" + android:targetPackage="com.android.server.pm.test.override" /> + +</manifest> diff --git a/services/tests/PackageManagerComponentOverrideTests/AndroidTest.xml b/services/tests/PackageManagerComponentOverrideTests/AndroidTest.xml new file mode 100644 index 000000000000..b83b1a8fb113 --- /dev/null +++ b/services/tests/PackageManagerComponentOverrideTests/AndroidTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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. + --> + +<configuration description="Test module config for PackageManagerComponentOverrideTests"> + <option name="test-tag" value="PackageManagerComponentOverrideTests" /> + + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="PackageManagerComponentOverrideTests.apk" /> + </target_preparer> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="com.android.server.pm.test.override" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + </test> +</configuration> diff --git a/services/tests/PackageManagerComponentOverrideTests/res/drawable/black16x16.png b/services/tests/PackageManagerComponentOverrideTests/res/drawable/black16x16.png Binary files differnew file mode 100644 index 000000000000..86b12fca81cc --- /dev/null +++ b/services/tests/PackageManagerComponentOverrideTests/res/drawable/black16x16.png diff --git a/services/tests/PackageManagerComponentOverrideTests/res/drawable/white16x16.png b/services/tests/PackageManagerComponentOverrideTests/res/drawable/white16x16.png Binary files differnew file mode 100644 index 000000000000..49dbb4fd7a46 --- /dev/null +++ b/services/tests/PackageManagerComponentOverrideTests/res/drawable/white16x16.png diff --git a/services/tests/PackageManagerComponentOverrideTests/res/values/values.xml b/services/tests/PackageManagerComponentOverrideTests/res/values/values.xml new file mode 100644 index 000000000000..73d11281c96f --- /dev/null +++ b/services/tests/PackageManagerComponentOverrideTests/res/values/values.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<resources> + <public name="black16x16" type="drawable" id="0x7f080001"/> + <public name="white16x16" type="drawable" id="0x7f080002"/> +</resources> 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 000000000000..ecdb30f5e84b --- /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<out java.lang.Exception>) : 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<out Exception>) + : 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<ComponentName>(), 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<PackageSetting>(), + any<PackageSetting>(), 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<String, AndroidPackage>().apply { putAll(mockedPkgs) } + } + + return PackageManagerService(mockInjector, testParams) + } +} diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index e58e91179931..b457856e8630 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 063cd5dacc93..78c708084d38 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 f532dd87909e..d7b02f44ed26 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 index 0f915dbdcf6f..056fa886f640 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 <reified T> mock(block: T.() -> Unit = {}) = Mockito.mock(T::class.java).apply(block) +fun <T> spy(value: T, block: T.() -> Unit = {}) = Mockito.spy(value).apply(block) + fun <Type> Stubber.whenever(mock: Type) = Mockito.`when`(mock) fun <Type : Any?> whenever(mock: Type) = Mockito.`when`(mock) @@ -55,7 +64,7 @@ fun <Type : Any?> whenever(mock: Type, block: InvocationOnMock.() -> Any?) = fun whenever(mock: Unit) = Mockito.`when`(mock).thenAnswer { } -inline fun <reified T> mockThrowOnUnmocked(block: T.() -> Unit): T { +inline fun <reified T> spyThrowOnUnmocked(value: T?, block: T.() -> Unit): T { val swappingAnswer = object : Answer<Any?> { var delegate: Answer<*> = Answers.RETURNS_DEFAULTS @@ -64,9 +73,12 @@ inline fun <reified T> 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 <reified T> mockThrowOnUnmocked(block: T.() -> Unit) = spyThrowOnUnmocked<T>(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 69db38438609..355e7f333bb7 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<MsgInfo> { + public class MsgInfo implements Comparable<MsgInfo> { public final Message message; public final long sendTime; public final RuntimeException postPoint; |