• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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