1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.safetycenter.resources; 18 19 import static java.util.Objects.requireNonNull; 20 21 import android.content.Context; 22 import android.content.ContextWrapper; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.content.pm.ResolveInfo; 26 import android.content.res.AssetManager; 27 import android.content.res.Resources; 28 import android.graphics.drawable.Drawable; 29 import android.graphics.drawable.Icon; 30 import android.util.Log; 31 32 import androidx.annotation.ColorInt; 33 import androidx.annotation.Nullable; 34 import androidx.annotation.StringRes; 35 import androidx.annotation.VisibleForTesting; 36 37 import java.io.File; 38 import java.io.InputStream; 39 import java.util.List; 40 41 /** 42 * Wrapper for context to override getResources method. Resources for the Safety Center that need to 43 * be fetched from the dedicated resources APK. 44 */ 45 public class SafetyCenterResourcesContext extends ContextWrapper { 46 private static final String TAG = "SafetyCenterResContext"; 47 48 /** Intent action that is used to identify the Safety Center resources APK */ 49 private static final String RESOURCES_APK_ACTION = 50 "com.android.safetycenter.intent.action.SAFETY_CENTER_RESOURCES_APK"; 51 52 /** Permission APEX name */ 53 private static final String APEX_MODULE_NAME = "com.android.permission"; 54 55 /** 56 * The path where the Permission apex is mounted. Current value = "/apex/com.android.permission" 57 */ 58 private static final String APEX_MODULE_PATH = 59 new File("/apex", APEX_MODULE_NAME).getAbsolutePath(); 60 61 /** Raw XML config resource name */ 62 private static final String CONFIG_NAME = "safety_center_config"; 63 64 /** Intent action that is used to identify the Safety Center resources APK */ 65 private final String mResourcesApkAction; 66 67 /** The path where the Safety Center resources APK is expected to be installed */ 68 @Nullable private final String mResourcesApkPath; 69 70 /** Raw XML config resource name */ 71 private final String mConfigName; 72 73 /** Specific flags used for retrieving resolve info */ 74 private final int mFlags; 75 76 /** 77 * Whether we should fallback with an empty string when calling {@link #getStringByName} for a 78 * string resource that does not exist. 79 */ 80 private final boolean mShouldFallbackIfNamedResourceNotFound; 81 82 // Cached package name and resources from the resources APK 83 @Nullable private String mResourcesApkPkgName; 84 @Nullable private AssetManager mAssetsFromApk; 85 @Nullable private Resources mResourcesFromApk; 86 @Nullable private Resources.Theme mThemeFromApk; 87 SafetyCenterResourcesContext(Context contextBase)88 public SafetyCenterResourcesContext(Context contextBase) { 89 this(contextBase, /* shouldFallbackIfNamedResourceNotFound */ true); 90 } 91 SafetyCenterResourcesContext( Context contextBase, boolean shouldFallbackIfNamedResourceNotFound)92 private SafetyCenterResourcesContext( 93 Context contextBase, boolean shouldFallbackIfNamedResourceNotFound) { 94 this( 95 contextBase, 96 RESOURCES_APK_ACTION, 97 APEX_MODULE_PATH, 98 CONFIG_NAME, 99 PackageManager.MATCH_SYSTEM_ONLY, 100 shouldFallbackIfNamedResourceNotFound); 101 } 102 103 @VisibleForTesting SafetyCenterResourcesContext( Context contextBase, String resourcesApkAction, @Nullable String resourcesApkPath, String configName, int flags, boolean shouldFallbackIfNamedResourceNotFound)104 SafetyCenterResourcesContext( 105 Context contextBase, 106 String resourcesApkAction, 107 @Nullable String resourcesApkPath, 108 String configName, 109 int flags, 110 boolean shouldFallbackIfNamedResourceNotFound) { 111 super(contextBase); 112 mResourcesApkAction = requireNonNull(resourcesApkAction); 113 mResourcesApkPath = resourcesApkPath; 114 mConfigName = requireNonNull(configName); 115 mFlags = flags; 116 mShouldFallbackIfNamedResourceNotFound = shouldFallbackIfNamedResourceNotFound; 117 } 118 119 /** Creates a new {@link SafetyCenterResourcesContext} for testing. */ 120 @VisibleForTesting forTests(Context contextBase)121 public static SafetyCenterResourcesContext forTests(Context contextBase) { 122 return new SafetyCenterResourcesContext( 123 contextBase, /* shouldFallbackIfNamedResourceNotFound */ false); 124 } 125 126 /** 127 * Initializes the {@link Context}'s {@link AssetManager}, {@link Resources} and {@link 128 * Resources.Theme}. 129 * 130 * <p>This call is optional as this can also be lazily instantiated. This is useful to ensure 131 * that resources are loaded prior to interacting with the {@link SafetyCenterResourcesContext}, 132 * as this code needs to run for the same user as the provided base {@link Context}; which may 133 * not be the case with a binder call. 134 */ init()135 public void init() { 136 mAssetsFromApk = getAssets(); 137 mResourcesFromApk = getResources(); 138 mThemeFromApk = getTheme(); 139 } 140 141 /** Get the package name of the Safety Center resources APK. */ 142 @VisibleForTesting 143 @Nullable getResourcesApkPkgName()144 String getResourcesApkPkgName() { 145 if (mResourcesApkPkgName != null) { 146 return mResourcesApkPkgName; 147 } 148 149 List<ResolveInfo> resolveInfos = 150 getPackageManager().queryIntentActivities(new Intent(mResourcesApkAction), mFlags); 151 152 if (resolveInfos.size() > 1) { 153 // multiple apps found, log a warning, but continue 154 Log.w(TAG, "Found > 1 APK that can resolve Safety Center resources APK intent:"); 155 final int resolveInfosSize = resolveInfos.size(); 156 for (int i = 0; i < resolveInfosSize; i++) { 157 ResolveInfo resolveInfo = resolveInfos.get(i); 158 Log.w( 159 TAG, 160 String.format( 161 "- pkg:%s at:%s", 162 resolveInfo.activityInfo.applicationInfo.packageName, 163 resolveInfo.activityInfo.applicationInfo.sourceDir)); 164 } 165 } 166 167 ResolveInfo info = null; 168 // Assume the first good ResolveInfo is the one we're looking for 169 final int resolveInfosSize = resolveInfos.size(); 170 for (int i = 0; i < resolveInfosSize; i++) { 171 ResolveInfo resolveInfo = resolveInfos.get(i); 172 if (mResourcesApkPath != null 173 && !resolveInfo.activityInfo.applicationInfo.sourceDir.startsWith( 174 mResourcesApkPath)) { 175 // skip apps that don't live in the Permission apex 176 continue; 177 } 178 info = resolveInfo; 179 break; 180 } 181 182 if (info == null) { 183 // Resource APK not loaded yet, print a stack trace to see where this is called from 184 Log.e( 185 TAG, 186 "Attempted to fetch resources before Safety Center resources APK is loaded!", 187 new IllegalStateException()); 188 return null; 189 } 190 191 mResourcesApkPkgName = info.activityInfo.applicationInfo.packageName; 192 Log.i(TAG, "Found Safety Center resources APK at: " + mResourcesApkPkgName); 193 return mResourcesApkPkgName; 194 } 195 196 /** 197 * Gets the raw XML resource representing the Safety Center configuration from the Safety Center 198 * resources APK. 199 */ 200 @Nullable getSafetyCenterConfig()201 public InputStream getSafetyCenterConfig() { 202 String resourcePkgName = getResourcesApkPkgName(); 203 if (resourcePkgName == null) { 204 return null; 205 } 206 Resources resources = getResources(); 207 if (resources == null) { 208 return null; 209 } 210 int id = resources.getIdentifier(mConfigName, "raw", resourcePkgName); 211 if (id == Resources.ID_NULL) { 212 return null; 213 } 214 return resources.openRawResource(id); 215 } 216 217 /** 218 * Returns an optional {@link String} resource from the given {@code stringId}. 219 * 220 * <p>Returns {@code null} if {@code stringId} is equal to {@link Resources#ID_NULL}. Otherwise, 221 * throws a {@link Resources.NotFoundException} if the resource cannot be accessed. 222 */ 223 @Nullable getOptionalString(@tringRes int stringId)224 public String getOptionalString(@StringRes int stringId) { 225 if (stringId == Resources.ID_NULL) { 226 return null; 227 } 228 return getString(stringId); 229 } 230 231 /** Same as {@link #getOptionalString(int)} but with the given {@code formatArgs}. */ 232 @Nullable getOptionalString(@tringRes int stringId, Object... formatArgs)233 public String getOptionalString(@StringRes int stringId, Object... formatArgs) { 234 if (stringId == Resources.ID_NULL) { 235 return null; 236 } 237 return getString(stringId, formatArgs); 238 } 239 240 /** Same as {@link #getOptionalString(int)} but using the string name rather than ID. */ 241 @Nullable getOptionalStringByName(String name)242 public String getOptionalStringByName(String name) { 243 return getOptionalString(getStringRes(name)); 244 } 245 246 /** 247 * Gets a string resource by name from the Safety Center resources APK, and returns an empty 248 * string if the resource does not exist (or throws a {@link Resources.NotFoundException} if 249 * {@link #mShouldFallbackIfNamedResourceNotFound} is {@code false}). 250 */ getStringByName(String name)251 public String getStringByName(String name) { 252 int id = getStringRes(name); 253 return maybeFallbackIfNamedResourceIsNull(name, getOptionalString(id)); 254 } 255 256 /** Same as {@link #getStringByName(String)} but with the given {@code formatArgs}. */ getStringByName(String name, Object... formatArgs)257 public String getStringByName(String name, Object... formatArgs) { 258 int id = getStringRes(name); 259 return maybeFallbackIfNamedResourceIsNull(name, getOptionalString(id, formatArgs)); 260 } 261 maybeFallbackIfNamedResourceIsNull(String name, @Nullable String value)262 private String maybeFallbackIfNamedResourceIsNull(String name, @Nullable String value) { 263 if (value != null) { 264 return value; 265 } 266 if (!mShouldFallbackIfNamedResourceNotFound) { 267 throw new Resources.NotFoundException(); 268 } 269 Log.w(TAG, "String resource " + name + " not found"); 270 return ""; 271 } 272 273 @StringRes getStringRes(String name)274 private int getStringRes(String name) { 275 return getResId(name, "string"); 276 } 277 getResId(String name, String type)278 private int getResId(String name, String type) { 279 String resourcePkgName = getResourcesApkPkgName(); 280 if (resourcePkgName == null) { 281 return Resources.ID_NULL; 282 } 283 Resources resources = getResources(); 284 if (resources == null) { 285 return Resources.ID_NULL; 286 } 287 // TODO(b/227738283): profile the performance of this operation and consider adding caching 288 // or finding some alternative solution. 289 return resources.getIdentifier(name, type, resourcePkgName); 290 } 291 292 @Nullable getResourcesApkContext()293 private Context getResourcesApkContext() { 294 String name = getResourcesApkPkgName(); 295 if (name == null) { 296 return null; 297 } 298 try { 299 return createPackageContext(name, 0); 300 } catch (PackageManager.NameNotFoundException e) { 301 Log.wtf(TAG, "Failed to load resources", e); 302 } 303 return null; 304 } 305 306 /** Retrieve assets held in the Safety Center resources APK. */ 307 @Override 308 @Nullable getAssets()309 public AssetManager getAssets() { 310 if (mAssetsFromApk == null) { 311 Context resourcesApkContext = getResourcesApkContext(); 312 if (resourcesApkContext != null) { 313 mAssetsFromApk = resourcesApkContext.getAssets(); 314 } 315 } 316 return mAssetsFromApk; 317 } 318 319 /** Retrieve resources held in the Safety Center resources APK. */ 320 @Override 321 @Nullable getResources()322 public Resources getResources() { 323 if (mResourcesFromApk == null) { 324 Context resourcesApkContext = getResourcesApkContext(); 325 if (resourcesApkContext != null) { 326 mResourcesFromApk = resourcesApkContext.getResources(); 327 } 328 } 329 return mResourcesFromApk; 330 } 331 332 /** Retrieve theme held in the Safety Center resources APK. */ 333 @Override 334 @Nullable getTheme()335 public Resources.Theme getTheme() { 336 if (mThemeFromApk == null) { 337 Context resourcesApkContext = getResourcesApkContext(); 338 if (resourcesApkContext != null) { 339 mThemeFromApk = resourcesApkContext.getTheme(); 340 } 341 } 342 return mThemeFromApk; 343 } 344 345 /** 346 * Gets a drawable resource by name from the Safety Center resources APK. Returns a null 347 * drawable if the resource does not exist (or throws a {@link Resources.NotFoundException} if 348 * {@link #mShouldFallbackIfNamedResourceNotFound} is {@code false}). 349 * 350 * @param name the identifier for this drawable resource 351 * @param theme the theme used to style the drawable attributes, may be {@code null} 352 */ 353 @Nullable getDrawableByName(String name, @Nullable Resources.Theme theme)354 public Drawable getDrawableByName(String name, @Nullable Resources.Theme theme) { 355 int resId = getResId(name, "drawable"); 356 if (resId != Resources.ID_NULL) { 357 return getResources().getDrawable(resId, theme); 358 } 359 360 if (!mShouldFallbackIfNamedResourceNotFound) { 361 throw new Resources.NotFoundException(); 362 } 363 364 Log.w(TAG, "Drawable resource " + name + " not found"); 365 return null; 366 } 367 368 /** 369 * Returns an {@link Icon} instance containing a drawable with the given name. If no such 370 * drawable exists, returns {@code null} or throws {@link Resources.NotFoundException}. 371 */ 372 @Nullable getIconByDrawableName(String drawableResName)373 public Icon getIconByDrawableName(String drawableResName) { 374 int resId = getResId(drawableResName, "drawable"); 375 if (resId != Resources.ID_NULL) { 376 return Icon.createWithResource(getResourcesApkPkgName(), resId); 377 } 378 379 if (!mShouldFallbackIfNamedResourceNotFound) { 380 throw new Resources.NotFoundException(); 381 } 382 383 Log.w(TAG, "Drawable resource " + drawableResName + " not found"); 384 return null; 385 } 386 387 /** Gets a color by resource name */ 388 @ColorInt 389 @Nullable getColorByName(String name)390 public Integer getColorByName(String name) { 391 int resId = getResId(name, "color"); 392 if (resId != Resources.ID_NULL) { 393 return getResources().getColor(resId, getTheme()); 394 } 395 396 if (!mShouldFallbackIfNamedResourceNotFound) { 397 throw new Resources.NotFoundException(); 398 } 399 400 Log.w(TAG, "Color resource " + name + " not found"); 401 return null; 402 } 403 } 404