/* * 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.safetycenter.resources; import static java.util.Objects.requireNonNull; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.util.Log; import androidx.annotation.ColorInt; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.VisibleForTesting; import java.io.File; import java.io.InputStream; import java.util.List; /** * A class to access Safety Center resources that need to be fetched from a dedicated APK. * *
You must check whether Safety Center is enabled or the value returned by {@link #init()} prior * to interacting with this class. Failure to do so may cause an {@link IllegalStateException} if * the resources APK cannot be accessed. * *
This class isn't thread safe. Thread safety must be handled by the caller, or this may cause * the resources APK {@link Context} to be initialized multiple times. */ public final class SafetyCenterResourcesApk { private static final String TAG = "SafetyCenterResApk"; /** Intent action that is used to identify the Safety Center resources APK */ private static final String RESOURCES_APK_ACTION = "com.android.safetycenter.intent.action.SAFETY_CENTER_RESOURCES_APK"; /** Permission APEX name */ private static final String APEX_MODULE_NAME = "com.android.permission"; /** * The path where the Permission apex is mounted. Current value = "/apex/com.android.permission" */ private static final String APEX_MODULE_PATH = new File("/apex", APEX_MODULE_NAME).getAbsolutePath(); /** Raw XML config resource name */ private static final String CONFIG_NAME = "safety_center_config"; private final Context mContext; /** Intent action that is used to identify the Safety Center resources APK */ private final String mResourcesApkAction; /** The path where the Safety Center resources APK is expected to be installed */ private final String mResourcesApkPath; /** Specific flags used for retrieving resolve info. */ private final int mFlags; /** * Whether we should fallback with an empty string / null values when calling the methods of * this class for a resource that does not exist. */ private final boolean mShouldFallbackIfNamedResourceNotFound; // Cached context from the resources APK. @Nullable private Context mResourcesApkContext; public SafetyCenterResourcesApk(Context context) { this(context, /* shouldFallbackIfNamedResourceNotFound */ true); } private SafetyCenterResourcesApk( Context context, boolean shouldFallbackIfNamedResourceNotFound) { this( context, RESOURCES_APK_ACTION, APEX_MODULE_PATH, PackageManager.MATCH_SYSTEM_ONLY, shouldFallbackIfNamedResourceNotFound); } @VisibleForTesting SafetyCenterResourcesApk( Context context, String resourcesApkAction, String resourcesApkPath, int flags, boolean shouldFallbackIfNamedResourceNotFound) { mContext = requireNonNull(context); mResourcesApkAction = requireNonNull(resourcesApkAction); mResourcesApkPath = requireNonNull(resourcesApkPath); mFlags = flags; mShouldFallbackIfNamedResourceNotFound = shouldFallbackIfNamedResourceNotFound; } /** Creates a new {@link SafetyCenterResourcesApk} for testing. */ @VisibleForTesting public static SafetyCenterResourcesApk forTests(Context context) { return new SafetyCenterResourcesApk( context, /* shouldFallbackIfNamedResourceNotFound */ false); } /** * Initializes the resources APK {@link Context}, and returns whether this was successful. * *
This call is optional as this can also be lazily instantiated. It can be used to ensure * that the resources APK context is loaded prior to interacting with this class. This * initialization code needs to run in the same user as the provided base {@link Context}. This * may not be the case with a binder call, which is why it can be more appropriate to do this * explicitly. */ public boolean init() { mResourcesApkContext = loadResourcesApkContext(); return mResourcesApkContext != null; } /** * Returns the {@link Context} of the Safety Center resources APK. * *
Throws an {@link IllegalStateException} if the resources APK is not available
*/
public Context getContext() {
if (mResourcesApkContext != null) {
return mResourcesApkContext;
}
mResourcesApkContext = loadResourcesApkContext();
if (mResourcesApkContext == null) {
throw new IllegalStateException("Resources APK context not found");
}
return mResourcesApkContext;
}
@Nullable
private Context loadResourcesApkContext() {
List If the {@link String} cannot be accessed, returns {@code ""} or throws {@link
* Resources.NotFoundException} depending on {@link #mShouldFallbackIfNamedResourceNotFound}.
*/
public String getStringByName(String name) {
int resId = getResIdAndMaybeThrowIfNull(name, "string");
if (resId == Resources.ID_NULL) {
return "";
}
return getString(resId);
}
/** Same as {@link #getStringByName(String)} but with the given {@code formatArgs}. */
public String getStringByName(String name, Object... formatArgs) {
int resId = getResIdAndMaybeThrowIfNull(name, "string");
if (resId == Resources.ID_NULL) {
return "";
}
return getString(resId, formatArgs);
}
/**
* Returns an optional {@link String} resource with the given {@code stringId}.
*
* Returns {@code null} if {@code stringId} is equal to {@link Resources#ID_NULL}. Otherwise,
* throws a {@link Resources.NotFoundException}.
*/
@Nullable
public String getOptionalString(@StringRes int stringId) {
if (stringId == Resources.ID_NULL) {
return null;
}
return getString(stringId);
}
/** Same as {@link #getOptionalString(int)} but with the given resource name rather than ID. */
@Nullable
public String getOptionalStringByName(String name) {
return getOptionalString(getResId(name, "string"));
}
/**
* Returns the {@link Drawable} with the given resource name.
*
* If the {@link Drawable} cannot be accessed, returns {@code null} or throws {@link
* Resources.NotFoundException} depending on {@link #mShouldFallbackIfNamedResourceNotFound}.
*
* @param theme the theme used to style the drawable attributes, may be {@code null}
*/
@Nullable
public Drawable getDrawableByName(String name, @Nullable Resources.Theme theme) {
int resId = getResIdAndMaybeThrowIfNull(name, "drawable");
if (resId == Resources.ID_NULL) {
return null;
}
return getResources().getDrawable(resId, theme);
}
/**
* Returns an {@link Icon} containing the {@link Drawable} with the given resource name.
*
* If the {@link Drawable} cannot be accessed, returns {@code null} or throws {@link
* Resources.NotFoundException} depending on {@link #mShouldFallbackIfNamedResourceNotFound}.
*/
@Nullable
public Icon getIconByDrawableName(String name) {
int resId = getResIdAndMaybeThrowIfNull(name, "drawable");
if (resId == Resources.ID_NULL) {
return null;
}
return Icon.createWithResource(getContext().getPackageName(), resId);
}
/**
* Returns the {@link ColorInt} with the given resource name.
*
* If the {@link ColorInt} cannot be accessed, returns {@code null} or throws {@link
* Resources.NotFoundException} depending on {@link #mShouldFallbackIfNamedResourceNotFound}.
*/
@ColorInt
@Nullable
public Integer getColorByName(String name) {
int resId = getResIdAndMaybeThrowIfNull(name, "color");
if (resId == Resources.ID_NULL) {
return null;
}
return getResources().getColor(resId, getContext().getTheme());
}
private int getResIdAndMaybeThrowIfNull(String name, String type) {
int resId = getResId(name, type);
if (resId != Resources.ID_NULL) {
return resId;
}
if (!mShouldFallbackIfNamedResourceNotFound) {
throw new Resources.NotFoundException();
}
Log.w(TAG, "Named " + type + " resource: " + name + " not found");
return resId;
}
private int getResId(String name, String type) {
// TODO(b/227738283): profile the performance of this operation and consider adding caching
// or finding some alternative solution.
return getResources().getIdentifier(name, type, getContext().getPackageName());
}
}