blob: cb7e9ff2ab35d23193ff1d1f64da79c195cb7188 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.art;
import static com.android.server.art.GetDexoptNeededResult.ArtifactsLocation;
import static com.android.server.art.OutputArtifacts.PermissionSettings;
import static com.android.server.art.OutputArtifacts.PermissionSettings.SeContext;
import static com.android.server.art.PrimaryDexUtils.DetailedPrimaryDexInfo;
import static com.android.server.art.ProfilePath.RefProfilePath;
import static com.android.server.art.ProfilePath.TmpRefProfilePath;
import static com.android.server.art.Utils.Abi;
import static com.android.server.art.model.ArtFlags.OptimizeFlags;
import static com.android.server.art.model.OptimizeResult.DexContainerFileOptimizeResult;
import android.R;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.CancellationSignal;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.art.model.ArtFlags;
import com.android.server.art.model.OptimizeParams;
import com.android.server.art.model.OptimizeResult;
import com.android.server.art.wrapper.AndroidPackageApi;
import com.android.server.art.wrapper.PackageState;
import com.android.server.art.wrapper.PackageUserState;
import com.google.auto.value.AutoValue;
import dalvik.system.DexFile;
import java.util.ArrayList;
import java.util.List;
/** @hide */
public class PrimaryDexOptimizer {
private static final String TAG = "PrimaryDexOptimizer";
@NonNull private final Injector mInjector;
public PrimaryDexOptimizer(@NonNull Context context) {
this(new Injector(context));
}
@VisibleForTesting
public PrimaryDexOptimizer(@NonNull Injector injector) {
mInjector = injector;
}
/**
* DO NOT use this method directly. Use {@link
* ArtManagerLocal#optimizePackage(PackageDataSnapshot, String, OptimizeParams)}.
*/
@NonNull
public List<DexContainerFileOptimizeResult> dexopt(@NonNull PackageState pkgState,
@NonNull AndroidPackageApi pkg, @NonNull OptimizeParams params,
@NonNull CancellationSignal cancellationSignal) throws RemoteException {
List<DexContainerFileOptimizeResult> results = new ArrayList<>();
int uid = pkg.getUid();
if (uid < 0) {
throw new IllegalStateException(
"Package '" + pkgState.getPackageName() + "' has invalid app uid");
}
int sharedGid = UserHandle.getSharedAppGid(uid);
if (sharedGid < 0) {
throw new IllegalStateException(
String.format("Unable to get shared gid for package '%s' (uid: %d)",
pkgState.getPackageName(), uid));
}
String targetCompilerFilter =
adjustCompilerFilter(pkgState, pkg, params.getCompilerFilter(), params.getReason());
if (targetCompilerFilter.equals(OptimizeParams.COMPILER_FILTER_NOOP)) {
return results;
}
boolean isInDalvikCache = Utils.isInDalvikCache(pkgState);
for (DetailedPrimaryDexInfo dexInfo : PrimaryDexUtils.getDetailedDexInfo(pkgState, pkg)) {
ProfilePath profile = null;
boolean succeeded = true;
try {
if (!dexInfo.hasCode()) {
continue;
}
// TODO(jiakaiz): Support optimizing a single split.
String compilerFilter = targetCompilerFilter;
boolean needsToBeShared = isSharedLibrary(pkg)
|| mInjector.isUsedByOtherApps(pkgState.getPackageName());
boolean isOtherReadable = true;
// If true, implies that the profile has changed since the last compilation.
boolean profileMerged = false;
if (DexFile.isProfileGuidedCompilerFilter(compilerFilter)) {
if (needsToBeShared) {
profile = initReferenceProfile(pkgState, dexInfo, uid, sharedGid);
} else {
Pair<ProfilePath, Boolean> pair =
getOrInitReferenceProfile(pkgState, dexInfo, uid, sharedGid);
if (pair != null) {
profile = pair.first;
isOtherReadable = pair.second;
}
ProfilePath mergedProfile =
mergeProfiles(pkgState, dexInfo, uid, sharedGid, profile);
if (mergedProfile != null) {
if (profile != null
&& profile.getTag() == ProfilePath.tmpRefProfilePath) {
mInjector.getArtd().deleteProfile(profile);
}
profile = mergedProfile;
isOtherReadable = false;
profileMerged = true;
}
}
if (profile == null) {
// A profile guided optimization with no profile is essentially 'verify',
// and dex2oat already makes this transformation. However, we need to
// explicitly make this transformation here to guide the later decisions
// such as whether the artifacts can be public and whether dexopt is needed.
compilerFilter = needsToBeShared
? ReasonMapping.getCompilerFilterForShared()
: "verify";
}
}
boolean isProfileGuidedCompilerFilter =
DexFile.isProfileGuidedCompilerFilter(compilerFilter);
Utils.check(isProfileGuidedCompilerFilter == (profile != null));
boolean canBePublic = !isProfileGuidedCompilerFilter || isOtherReadable;
Utils.check(Utils.implies(needsToBeShared, canBePublic));
PermissionSettings permissionSettings =
getPermissionSettings(sharedGid, canBePublic);
DexoptOptions dexoptOptions =
getDexoptOptions(pkgState, pkg, params, isProfileGuidedCompilerFilter);
for (Abi abi : Utils.getAllAbis(pkgState)) {
@OptimizeResult.OptimizeStatus int status = OptimizeResult.OPTIMIZE_SKIPPED;
long wallTimeMs = 0;
long cpuTimeMs = 0;
try {
DexoptTarget target = DexoptTarget.builder()
.setDexInfo(dexInfo)
.setIsa(abi.isa())
.setIsInDalvikCache(isInDalvikCache)
.setCompilerFilter(compilerFilter)
.build();
GetDexoptNeededOptions options =
GetDexoptNeededOptions.builder()
.setProfileMerged(profileMerged)
.setFlags(params.getFlags())
.setNeedsToBePublic(needsToBeShared)
.build();
GetDexoptNeededResult getDexoptNeededResult =
getDexoptNeeded(target, options);
if (!getDexoptNeededResult.isDexoptNeeded) {
continue;
}
IArtdCancellationSignal artdCancellationSignal =
mInjector.getArtd().createCancellationSignal();
cancellationSignal.setOnCancelListener(() -> {
try {
artdCancellationSignal.cancel();
} catch (RemoteException e) {
Log.e(TAG, "An error occurred when sending a cancellation signal",
e);
}
});
DexoptResult dexoptResult = dexoptFile(target, profile,
getDexoptNeededResult, permissionSettings,
params.getPriorityClass(), dexoptOptions, artdCancellationSignal);
status = dexoptResult.cancelled ? OptimizeResult.OPTIMIZE_CANCELLED
: OptimizeResult.OPTIMIZE_PERFORMED;
wallTimeMs = dexoptResult.wallTimeMs;
cpuTimeMs = dexoptResult.cpuTimeMs;
if (status == OptimizeResult.OPTIMIZE_CANCELLED) {
return results;
}
} catch (ServiceSpecificException e) {
// Log the error and continue.
Log.e(TAG,
String.format("Failed to dexopt [packageName = %s, dexPath = %s, "
+ "isa = %s, classLoaderContext = %s]",
pkgState.getPackageName(), dexInfo.dexPath(), abi.isa(),
dexInfo.classLoaderContext()),
e);
status = OptimizeResult.OPTIMIZE_FAILED;
} finally {
results.add(new DexContainerFileOptimizeResult(dexInfo.dexPath(),
abi.isPrimaryAbi(), abi.name(), compilerFilter, status, wallTimeMs,
cpuTimeMs));
if (status != OptimizeResult.OPTIMIZE_SKIPPED
&& status != OptimizeResult.OPTIMIZE_PERFORMED) {
succeeded = false;
}
// Make sure artd does not leak even if the caller holds
// `cancellationSignal` forever.
cancellationSignal.setOnCancelListener(null);
}
}
if (profile != null && succeeded) {
if (profile.getTag() == ProfilePath.tmpRefProfilePath) {
// Commit the profile only if dexopt succeeds.
if (commitProfileChanges(profile.getTmpRefProfilePath())) {
profile = null;
}
}
if (profileMerged) {
// Note that this is just an optimization, to reduce the amount of data that
// the runtime writes on every profile save. The profile merge result on the
// next run won't change regardless of whether the cleanup is done or not
// because profman only looks at the diff.
// A caveat is that it may delete more than what has been merged, if the
// runtime writes additional entries between the merge and the cleanup, but
// this is fine because the runtime writes all JITed classes and methods on
// every save and the additional entries will likely be written back on the
// next save.
cleanupCurProfiles(pkgState, dexInfo);
}
}
} finally {
if (profile != null && profile.getTag() == ProfilePath.tmpRefProfilePath) {
mInjector.getArtd().deleteProfile(profile);
}
}
}
return results;
}
@NonNull
private String adjustCompilerFilter(@NonNull PackageState pkgState,
@NonNull AndroidPackageApi pkg, @NonNull String targetCompilerFilter,
@NonNull String reason) {
if (mInjector.isSystemUiPackage(pkgState.getPackageName())) {
String systemUiCompilerFilter = getSystemUiCompilerFilter();
if (!systemUiCompilerFilter.isEmpty()) {
return systemUiCompilerFilter;
}
}
// We force vmSafeMode on debuggable apps as well:
// - the runtime ignores their compiled code
// - they generally have lots of methods that could make the compiler used run out of
// memory (b/130828957)
// Note that forcing the compiler filter here applies to all compilations (even if they
// are done via adb shell commands). This is okay because the runtime will ignore the
// compiled code anyway.
if (pkg.isVmSafeMode() || pkg.isDebuggable()) {
return DexFile.getSafeModeCompilerFilter(targetCompilerFilter);
}
return targetCompilerFilter;
}
@NonNull
private String getSystemUiCompilerFilter() {
String compilerFilter = SystemProperties.get("dalvik.vm.systemuicompilerfilter");
if (!compilerFilter.isEmpty() && !Utils.isValidArtServiceCompilerFilter(compilerFilter)) {
throw new IllegalStateException(
"Got invalid compiler filter '" + compilerFilter + "' for System UI");
}
return compilerFilter;
}
boolean isSharedLibrary(@NonNull AndroidPackageApi pkg) {
// TODO(b/242688548): Package manager should provide a better API for this.
return !TextUtils.isEmpty(pkg.getSdkLibName())
|| !TextUtils.isEmpty(pkg.getStaticSharedLibName())
|| !pkg.getLibraryNames().isEmpty();
}
/**
* Returns a reference profile initialized from a prebuilt profile or a DM profile if exists, or
* null otherwise.
*/
@Nullable
private ProfilePath initReferenceProfile(@NonNull PackageState pkgState,
@NonNull DetailedPrimaryDexInfo dexInfo, int uid, int gid) throws RemoteException {
String profileName = getProfileName(dexInfo.splitName());
OutputProfile output = AidlUtils.buildOutputProfile(
pkgState.getPackageName(), profileName, uid, gid, true /* isPublic */);
ProfilePath prebuiltProfile = AidlUtils.buildProfilePathForPrebuilt(dexInfo.dexPath());
try {
// If the APK is really a prebuilt one, rewriting the profile is unnecessary because the
// dex location is known at build time and is correctly set in the profile header.
// However, the APK can also be an installed one, in which case partners may place a
// profile file next to the APK at install time. Rewriting the profile in the latter
// case is necessary.
if (mInjector.getArtd().copyAndRewriteProfile(
prebuiltProfile, output, dexInfo.dexPath())) {
return ProfilePath.tmpRefProfilePath(output.profilePath);
}
} catch (ServiceSpecificException e) {
Log.e(TAG,
String.format(
"Failed to use prebuilt profile [packageName = %s, profileName = %s]",
pkgState.getPackageName(), profileName),
e);
}
ProfilePath dmProfile = AidlUtils.buildProfilePathForDm(dexInfo.dexPath());
try {
if (mInjector.getArtd().copyAndRewriteProfile(dmProfile, output, dexInfo.dexPath())) {
return ProfilePath.tmpRefProfilePath(output.profilePath);
}
} catch (ServiceSpecificException e) {
Log.e(TAG,
String.format("Failed to use profile in dex metadata file "
+ "[packageName = %s, profileName = %s]",
pkgState.getPackageName(), profileName),
e);
}
return null;
}
/**
* Gets the existing reference profile if exists, or initializes a reference profile from an
* external profile.
*
* @return A pair where the first element is the found or initialized profile, and the second
* element is true if the profile is readable by others. Or null if there is no
* reference profile or external profile to use.
*/
@Nullable
private Pair<ProfilePath, Boolean> getOrInitReferenceProfile(@NonNull PackageState pkgState,
@NonNull DetailedPrimaryDexInfo dexInfo, int uid, int gid) throws RemoteException {
String profileName = getProfileName(dexInfo.splitName());
ProfilePath refProfile =
AidlUtils.buildProfilePathForRef(pkgState.getPackageName(), profileName);
try {
if (mInjector.getArtd().isProfileUsable(refProfile, dexInfo.dexPath())) {
boolean isOtherReadable = mInjector.getArtd().getProfileVisibility(refProfile)
== FileVisibility.OTHER_READABLE;
return Pair.create(refProfile, isOtherReadable);
}
} catch (ServiceSpecificException e) {
Log.e(TAG,
String.format("Failed to use the existing reference profile "
+ "[packageName = %s, profileName = %s]",
pkgState.getPackageName(), profileName),
e);
}
ProfilePath initializedProfile = initReferenceProfile(pkgState, dexInfo, uid, gid);
return initializedProfile != null ? Pair.create(initializedProfile, true) : null;
}
@NonNull
public String getProfileName(@Nullable String splitName) {
return splitName == null ? "primary" : splitName + ".split";
}
@NonNull
PermissionSettings getPermissionSettings(int sharedGid, boolean canBePublic) {
// The files and directories should belong to the system so that Package Manager can manage
// them (e.g., move them around).
// We don't need the "read" bit for "others" on the directories because others only need to
// access the files in the directories, but they don't need to "ls" the directories.
FsPermission dirFsPermission = AidlUtils.buildFsPermission(Process.SYSTEM_UID,
Process.SYSTEM_UID, false /* isOtherReadable */, true /* isOtherExecutable */);
FsPermission fileFsPermission =
AidlUtils.buildFsPermission(Process.SYSTEM_UID, sharedGid, canBePublic);
// For primary dex, we can use the default SELinux context.
SeContext seContext = null;
return AidlUtils.buildPermissionSettings(dirFsPermission, fileFsPermission, seContext);
}
@NonNull
private DexoptOptions getDexoptOptions(@NonNull PackageState pkgState,
@NonNull AndroidPackageApi pkg, @NonNull OptimizeParams params,
boolean isProfileGuidedFilter) {
DexoptOptions dexoptOptions = new DexoptOptions();
dexoptOptions.compilationReason = params.getReason();
dexoptOptions.targetSdkVersion = pkg.getTargetSdkVersion();
dexoptOptions.debuggable = pkg.isDebuggable() || isAlwaysDebuggable();
// Generating a meaningful app image needs a profile to determine what to include in the
// image. Otherwise, the app image will be nearly empty.
// Additionally, disable app images if the app requests for the splits to be loaded in
// isolation because app images are unsupported for multiple class loaders (b/72696798).
dexoptOptions.generateAppImage = isProfileGuidedFilter
&& !PrimaryDexUtils.isIsolatedSplitLoading(pkg) && isAppImageEnabled();
dexoptOptions.hiddenApiPolicyEnabled = isHiddenApiPolicyEnabled(pkgState, pkg);
return dexoptOptions;
}
private boolean isAlwaysDebuggable() {
return SystemProperties.getBoolean("dalvik.vm.always_debuggable", false /* def */);
}
private boolean isAppImageEnabled() {
return !SystemProperties.get("dalvik.vm.appimageformat").isEmpty();
}
private boolean isHiddenApiPolicyEnabled(
@NonNull PackageState pkgState, @NonNull AndroidPackageApi pkg) {
if (pkg.isSignedWithPlatformKey()) {
return false;
}
if (pkgState.isSystem() || pkgState.isUpdatedSystemApp()) {
// TODO(b/236389629): Check whether the app is in hidden api whitelist.
return !pkg.isUsesNonSdkApi();
}
return true;
}
@NonNull
GetDexoptNeededResult getDexoptNeeded(@NonNull DexoptTarget target,
@NonNull GetDexoptNeededOptions options) throws RemoteException {
int dexoptTrigger = getDexoptTrigger(target, options);
// The result should come from artd even if all the bits of `dexoptTrigger` are set
// because the result also contains information about the usable VDEX file.
GetDexoptNeededResult result = mInjector.getArtd().getDexoptNeeded(
target.dexInfo().dexPath(), target.isa(), target.dexInfo().classLoaderContext(),
target.compilerFilter(), dexoptTrigger);
return result;
}
int getDexoptTrigger(@NonNull DexoptTarget target, @NonNull GetDexoptNeededOptions options)
throws RemoteException {
if ((options.flags() & ArtFlags.FLAG_FORCE) != 0) {
return DexoptTrigger.COMPILER_FILTER_IS_BETTER | DexoptTrigger.COMPILER_FILTER_IS_SAME
| DexoptTrigger.COMPILER_FILTER_IS_WORSE
| DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE;
}
if ((options.flags() & ArtFlags.FLAG_SHOULD_DOWNGRADE) != 0) {
return DexoptTrigger.COMPILER_FILTER_IS_WORSE;
}
int dexoptTrigger = DexoptTrigger.COMPILER_FILTER_IS_BETTER
| DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE;
if (options.profileMerged()) {
dexoptTrigger |= DexoptTrigger.COMPILER_FILTER_IS_SAME;
}
ArtifactsPath existingArtifactsPath = AidlUtils.buildArtifactsPath(
target.dexInfo().dexPath(), target.isa(), target.isInDalvikCache());
if (options.needsToBePublic()
&& mInjector.getArtd().getArtifactsVisibility(existingArtifactsPath)
== FileVisibility.NOT_OTHER_READABLE) {
// Typically, this happens after an app starts being used by other apps.
// This case should be the same as force as we have no choice but to trigger a new
// dexopt.
dexoptTrigger |=
DexoptTrigger.COMPILER_FILTER_IS_SAME | DexoptTrigger.COMPILER_FILTER_IS_WORSE;
}
return dexoptTrigger;
}
private DexoptResult dexoptFile(@NonNull DexoptTarget target, @Nullable ProfilePath profile,
@NonNull GetDexoptNeededResult getDexoptNeededResult,
@NonNull PermissionSettings permissionSettings, @PriorityClass int priorityClass,
@NonNull DexoptOptions dexoptOptions, IArtdCancellationSignal artdCancellationSignal)
throws RemoteException {
OutputArtifacts outputArtifacts = AidlUtils.buildOutputArtifacts(target.dexInfo().dexPath(),
target.isa(), target.isInDalvikCache(), permissionSettings);
VdexPath inputVdex =
getInputVdex(getDexoptNeededResult, target.dexInfo().dexPath(), target.isa());
return mInjector.getArtd().dexopt(outputArtifacts, target.dexInfo().dexPath(), target.isa(),
target.dexInfo().classLoaderContext(), target.compilerFilter(), profile, inputVdex,
priorityClass, dexoptOptions, artdCancellationSignal);
}
@Nullable
private VdexPath getInputVdex(@NonNull GetDexoptNeededResult getDexoptNeededResult,
@NonNull String dexPath, @NonNull String isa) {
if (!getDexoptNeededResult.isVdexUsable) {
return null;
}
switch (getDexoptNeededResult.artifactsLocation) {
case ArtifactsLocation.DALVIK_CACHE:
return VdexPath.artifactsPath(
AidlUtils.buildArtifactsPath(dexPath, isa, true /* isInDalvikCache */));
case ArtifactsLocation.NEXT_TO_DEX:
return VdexPath.artifactsPath(
AidlUtils.buildArtifactsPath(dexPath, isa, false /* isInDalvikCache */));
case ArtifactsLocation.DM:
return VdexPath.dexMetadataPath(AidlUtils.buildDexMetadataPath(dexPath));
default:
// This should never happen as the value is got from artd.
throw new IllegalStateException(
"Unknown artifacts location " + getDexoptNeededResult.artifactsLocation);
}
}
private boolean commitProfileChanges(@NonNull TmpRefProfilePath profile)
throws RemoteException {
try {
mInjector.getArtd().commitTmpProfile(profile);
return true;
} catch (ServiceSpecificException e) {
RefProfilePath refProfilePath = profile.refProfilePath;
Log.e(TAG,
String.format(
"Failed to commit profile changes [packageName = %s, profileName = %s]",
refProfilePath.packageName, refProfilePath.profileName),
e);
return false;
}
}
@Nullable
private ProfilePath mergeProfiles(@NonNull PackageState pkgState,
@NonNull DetailedPrimaryDexInfo dexInfo, int uid, int gid,
@Nullable ProfilePath referenceProfile) throws RemoteException {
String profileName = getProfileName(dexInfo.splitName());
OutputProfile output = AidlUtils.buildOutputProfile(
pkgState.getPackageName(), profileName, uid, gid, false /* isPublic */);
try {
if (mInjector.getArtd().mergeProfiles(getCurProfiles(pkgState, dexInfo),
referenceProfile, output, dexInfo.dexPath())) {
return ProfilePath.tmpRefProfilePath(output.profilePath);
}
} catch (ServiceSpecificException e) {
Log.e(TAG,
String.format("Failed to merge profiles [packageName = %s, profileName = %s]",
pkgState.getPackageName(), getProfileName(dexInfo.splitName())),
e);
}
return null;
}
private void cleanupCurProfiles(@NonNull PackageState pkgState,
@NonNull DetailedPrimaryDexInfo dexInfo) throws RemoteException {
for (ProfilePath profile : getCurProfiles(pkgState, dexInfo)) {
mInjector.getArtd().deleteProfile(profile);
}
}
@NonNull
private List<ProfilePath> getCurProfiles(
@NonNull PackageState pkgState, @NonNull DetailedPrimaryDexInfo dexInfo) {
List<ProfilePath> profiles = new ArrayList<>();
for (UserHandle handle :
mInjector.getUserManager().getUserHandles(true /* excludeDying */)) {
int userId = handle.getIdentifier();
PackageUserState userState = pkgState.getUserStateOrDefault(userId);
if (userState.isInstalled()) {
profiles.add(AidlUtils.buildProfilePathForCur(
userId, pkgState.getPackageName(), getProfileName(dexInfo.splitName())));
}
}
return profiles;
}
@AutoValue
abstract static class DexoptTarget {
abstract @NonNull DetailedPrimaryDexInfo dexInfo();
abstract @NonNull String isa();
abstract boolean isInDalvikCache();
abstract @NonNull String compilerFilter();
static Builder builder() {
return new AutoValue_PrimaryDexOptimizer_DexoptTarget.Builder();
}
@AutoValue.Builder
abstract static class Builder {
abstract Builder setDexInfo(@NonNull DetailedPrimaryDexInfo value);
abstract Builder setIsa(@NonNull String value);
abstract Builder setIsInDalvikCache(boolean value);
abstract Builder setCompilerFilter(@NonNull String value);
abstract DexoptTarget build();
}
}
@AutoValue
abstract static class GetDexoptNeededOptions {
abstract @OptimizeFlags int flags();
abstract boolean profileMerged();
abstract boolean needsToBePublic();
static Builder builder() {
return new AutoValue_PrimaryDexOptimizer_GetDexoptNeededOptions.Builder();
}
@AutoValue.Builder
abstract static class Builder {
abstract Builder setFlags(@OptimizeFlags int value);
abstract Builder setProfileMerged(boolean value);
abstract Builder setNeedsToBePublic(boolean value);
abstract GetDexoptNeededOptions build();
}
}
/**
* Injector pattern for testing purpose.
*
* @hide
*/
@VisibleForTesting
public static class Injector {
@NonNull private final Context mContext;
Injector(@NonNull Context context) {
mContext = context;
}
boolean isSystemUiPackage(@NonNull String packageName) {
return packageName.equals(mContext.getString(R.string.config_systemUi));
}
boolean isUsedByOtherApps(@NonNull String packageName) {
// TODO(jiakaiz): Get the real value.
return false;
}
@NonNull
UserManager getUserManager() {
return mContext.getSystemService(UserManager.class);
}
@NonNull
public IArtd getArtd() {
return Utils.getArtd();
}
}
}