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.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.content.Context; 24 import android.content.ContextWrapper; 25 import android.content.Intent; 26 import android.content.pm.PackageManager; 27 import android.content.pm.ResolveInfo; 28 import android.content.res.AssetManager; 29 import android.content.res.Resources; 30 import android.util.Log; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 34 import java.io.File; 35 import java.io.InputStream; 36 import java.util.List; 37 38 /** 39 * Wrapper for context to override getResources method. Resources for the Safety Center that need to 40 * be fetched from the dedicated resources APK. 41 */ 42 public class SafetyCenterResourcesContext extends ContextWrapper { 43 private static final String TAG = "SafetyCenterResContext"; 44 45 /** Intent action that is used to identify the Safety Center resources APK */ 46 private static final String RESOURCES_APK_ACTION = 47 "com.android.safetycenter.intent.action.SAFETY_CENTER_RESOURCES_APK"; 48 49 /** Permission APEX name */ 50 private static final String APEX_MODULE_NAME = "com.android.permission"; 51 52 /** 53 * The path where the Permission apex is mounted. 54 * Current value = "/apex/com.android.permission" 55 */ 56 private static final String APEX_MODULE_PATH = 57 new File("/apex", APEX_MODULE_NAME).getAbsolutePath(); 58 59 /** Raw XML config resource name */ 60 private static final String CONFIG_NAME = "safety_center_config"; 61 62 /** Intent action that is used to identify the Safety Center resources APK */ 63 @NonNull 64 private final String mResourcesApkAction; 65 66 /** The path where the Safety Center resources APK is expected to be installed */ 67 @Nullable 68 private final String mResourcesApkPath; 69 70 /** Raw XML config resource name */ 71 @NonNull 72 private final String mConfigName; 73 74 /** Specific flags used for retrieving resolve info */ 75 private final int mFlags; 76 77 // Cached package name and resources from the resources APK 78 @Nullable 79 private String mResourcesApkPkgName; 80 @Nullable 81 private AssetManager mAssetsFromApk; 82 @Nullable 83 private Resources mResourcesFromApk; 84 @Nullable 85 private Resources.Theme mThemeFromApk; 86 SafetyCenterResourcesContext(@onNull Context contextBase)87 public SafetyCenterResourcesContext(@NonNull Context contextBase) { 88 super(contextBase); 89 mResourcesApkAction = RESOURCES_APK_ACTION; 90 mResourcesApkPath = APEX_MODULE_PATH; 91 mConfigName = CONFIG_NAME; 92 mFlags = PackageManager.MATCH_SYSTEM_ONLY; 93 } 94 SafetyCenterResourcesContext(@onNull Context contextBase, @NonNull String resourcesApkAction, @Nullable String resourcesApkPath, @NonNull String configName, int flags)95 SafetyCenterResourcesContext(@NonNull Context contextBase, @NonNull String resourcesApkAction, 96 @Nullable String resourcesApkPath, @NonNull String configName, int flags) { 97 super(contextBase); 98 mResourcesApkAction = requireNonNull(resourcesApkAction); 99 mResourcesApkPath = resourcesApkPath; 100 mConfigName = requireNonNull(configName); 101 mFlags = flags; 102 } 103 104 /** Get the package name of the Safety Center resources APK. */ 105 @VisibleForTesting 106 @Nullable getResourcesApkPkgName()107 String getResourcesApkPkgName() { 108 if (mResourcesApkPkgName != null) { 109 return mResourcesApkPkgName; 110 } 111 112 List<ResolveInfo> resolveInfos = getPackageManager().queryIntentActivities( 113 new Intent(mResourcesApkAction), mFlags); 114 115 if (resolveInfos.size() > 1) { 116 // multiple apps found, log a warning, but continue 117 Log.w(TAG, "Found > 1 APK that can resolve Safety Center resources APK intent:"); 118 final int resolveInfosSize = resolveInfos.size(); 119 for (int i = 0; i < resolveInfosSize; i++) { 120 ResolveInfo resolveInfo = resolveInfos.get(i); 121 Log.w(TAG, String.format("- pkg:%s at:%s", 122 resolveInfo.activityInfo.applicationInfo.packageName, 123 resolveInfo.activityInfo.applicationInfo.sourceDir)); 124 } 125 } 126 127 ResolveInfo info = null; 128 // Assume the first good ResolveInfo is the one we're looking for 129 final int resolveInfosSize = resolveInfos.size(); 130 for (int i = 0; i < resolveInfosSize; i++) { 131 ResolveInfo resolveInfo = resolveInfos.get(i); 132 if (mResourcesApkPath != null 133 && !resolveInfo.activityInfo.applicationInfo.sourceDir.startsWith( 134 mResourcesApkPath)) { 135 // skip apps that don't live in the Permission apex 136 continue; 137 } 138 info = resolveInfo; 139 break; 140 } 141 142 if (info == null) { 143 // Resource APK not loaded yet, print a stack trace to see where this is called from 144 Log.e(TAG, 145 "Attempted to fetch resources before Safety Center resources APK is loaded!", 146 new IllegalStateException()); 147 return null; 148 } 149 150 mResourcesApkPkgName = info.activityInfo.applicationInfo.packageName; 151 Log.i(TAG, "Found Safety Center resources APK at: " + mResourcesApkPkgName); 152 return mResourcesApkPkgName; 153 } 154 155 /** 156 * Get the raw XML resource representing the Safety Center configuration from the Safety Center 157 * resources APK. 158 */ 159 @Nullable getSafetyCenterConfig()160 public InputStream getSafetyCenterConfig() { 161 String resourcePkgName = getResourcesApkPkgName(); 162 if (resourcePkgName == null) { 163 return null; 164 } 165 Resources resources = getResources(); 166 if (resources == null) { 167 return null; 168 } 169 int id = resources.getIdentifier(mConfigName, "raw", resourcePkgName); 170 if (id == Resources.ID_NULL) { 171 return null; 172 } 173 return resources.openRawResource(id); 174 } 175 176 @Nullable getResourcesApkContext()177 private Context getResourcesApkContext() { 178 String name = getResourcesApkPkgName(); 179 if (name == null) { 180 return null; 181 } 182 try { 183 return createPackageContext(name, 0); 184 } catch (PackageManager.NameNotFoundException e) { 185 Log.wtf(TAG, "Failed to load resources", e); 186 } 187 return null; 188 } 189 190 /** Retrieve assets held in the Safety Center resources APK. */ 191 @Override getAssets()192 public AssetManager getAssets() { 193 if (mAssetsFromApk == null) { 194 Context resourcesApkContext = getResourcesApkContext(); 195 if (resourcesApkContext != null) { 196 mAssetsFromApk = resourcesApkContext.getAssets(); 197 } 198 } 199 return mAssetsFromApk; 200 } 201 202 /** Retrieve resources held in the Safety Center resources APK. */ 203 @Override getResources()204 public Resources getResources() { 205 if (mResourcesFromApk == null) { 206 Context resourcesApkContext = getResourcesApkContext(); 207 if (resourcesApkContext != null) { 208 mResourcesFromApk = resourcesApkContext.getResources(); 209 } 210 } 211 return mResourcesFromApk; 212 } 213 214 /** Retrieve theme held in the Safety Center resources APK. */ 215 @Override getTheme()216 public Resources.Theme getTheme() { 217 if (mThemeFromApk == null) { 218 Context resourcesApkContext = getResourcesApkContext(); 219 if (resourcesApkContext != null) { 220 mThemeFromApk = resourcesApkContext.getTheme(); 221 } 222 } 223 return mThemeFromApk; 224 } 225 } 226