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 android.app.admin;
18 
19 import static java.util.Objects.requireNonNull;
20 
21 import android.annotation.AnyRes;
22 import android.annotation.IntDef;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.content.Context;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageManager;
28 import android.content.res.Resources;
29 import android.graphics.drawable.Drawable;
30 import android.os.Parcel;
31 import android.os.Parcelable;
32 import android.util.Slog;
33 
34 import com.android.modules.utils.TypedXmlPullParser;
35 import com.android.modules.utils.TypedXmlSerializer;
36 
37 import org.xmlpull.v1.XmlPullParserException;
38 
39 import java.io.IOException;
40 import java.lang.annotation.Retention;
41 import java.lang.annotation.RetentionPolicy;
42 import java.util.Objects;
43 import java.util.function.Supplier;
44 
45 /**
46  * Used to store the required information to load a resource that was updated using
47  * {@link DevicePolicyResourcesManager#setDrawables} and
48  * {@link DevicePolicyResourcesManager#setStrings}.
49  *
50  * @hide
51  */
52 public final class ParcelableResource implements Parcelable {
53 
54     private static String TAG = "DevicePolicyManager";
55 
56     private static final String ATTR_RESOURCE_ID = "resource-id";
57     private static final String ATTR_PACKAGE_NAME = "package-name";
58     private static final String ATTR_RESOURCE_NAME = "resource-name";
59     private static final String ATTR_RESOURCE_TYPE = "resource-type";
60 
61     public static final int RESOURCE_TYPE_DRAWABLE = 1;
62     public static final int RESOURCE_TYPE_STRING = 2;
63 
64 
65     @Retention(RetentionPolicy.SOURCE)
66     @IntDef(prefix = { "RESOURCE_TYPE_" }, value = {
67             RESOURCE_TYPE_DRAWABLE,
68             RESOURCE_TYPE_STRING
69     })
70     public @interface ResourceType {}
71 
72     private final int mResourceId;
73     @NonNull private final String mPackageName;
74     @NonNull private final String mResourceName;
75     private final int mResourceType;
76 
77     /**
78      *
79      * Creates a {@code ParcelableDevicePolicyResource} for the given {@code resourceId} and
80      * verifies that it exists in the package of the given {@code context}.
81      *
82      * @param context for the package containing the {@code resourceId} to use as the updated
83      *                resource
84      * @param resourceId of the resource to use as an updated resource
85      * @param resourceType see {@link ResourceType}
86      */
ParcelableResource( @onNull Context context, @AnyRes int resourceId, @ResourceType int resourceType)87     public ParcelableResource(
88             @NonNull Context context, @AnyRes int resourceId, @ResourceType int resourceType)
89             throws IllegalStateException, IllegalArgumentException {
90         Objects.requireNonNull(context, "context must be provided");
91         verifyResourceExistsInCallingPackage(context, resourceId, resourceType);
92 
93         this.mResourceId = resourceId;
94         this.mPackageName = context.getResources().getResourcePackageName(resourceId);
95         this.mResourceName = context.getResources().getResourceName(resourceId);
96         this.mResourceType = resourceType;
97     }
98 
99     /**
100      * Creates a {@code ParcelableDevicePolicyResource} with the given params, this DOES NOT make
101      * any verifications on whether the given {@code resourceId} actually exists.
102      */
ParcelableResource( @nyRes int resourceId, @NonNull String packageName, @NonNull String resourceName, @ResourceType int resourceType)103     private ParcelableResource(
104             @AnyRes int resourceId, @NonNull String packageName, @NonNull String resourceName,
105             @ResourceType int resourceType) {
106         this.mResourceId = resourceId;
107         this.mPackageName = requireNonNull(packageName);
108         this.mResourceName = requireNonNull(resourceName);
109         this.mResourceType = resourceType;
110     }
111 
verifyResourceExistsInCallingPackage( Context context, @AnyRes int resourceId, @ResourceType int resourceType)112     private static void verifyResourceExistsInCallingPackage(
113             Context context, @AnyRes int resourceId, @ResourceType int resourceType)
114             throws IllegalStateException, IllegalArgumentException {
115         switch (resourceType) {
116             case RESOURCE_TYPE_DRAWABLE:
117                 if (!hasDrawableInCallingPackage(context, resourceId)) {
118                     throw new IllegalStateException(String.format(
119                             "Drawable with id %d doesn't exist in the calling package %s",
120                             resourceId,
121                             context.getPackageName()));
122                 }
123                 break;
124             case RESOURCE_TYPE_STRING:
125                 if (!hasStringInCallingPackage(context, resourceId)) {
126                     throw new IllegalStateException(String.format(
127                             "String with id %d doesn't exist in the calling package %s",
128                             resourceId,
129                             context.getPackageName()));
130                 }
131                 break;
132             default:
133                 throw new IllegalArgumentException(
134                         "Unknown ResourceType: " + resourceType);
135         }
136     }
137 
hasDrawableInCallingPackage(Context context, @AnyRes int resourceId)138     private static boolean hasDrawableInCallingPackage(Context context, @AnyRes int resourceId) {
139         try {
140             return "drawable".equals(context.getResources().getResourceTypeName(resourceId));
141         } catch (Resources.NotFoundException e) {
142             return false;
143         }
144     }
145 
hasStringInCallingPackage(Context context, @AnyRes int resourceId)146     private static boolean hasStringInCallingPackage(Context context, @AnyRes int resourceId) {
147         try {
148             return "string".equals(context.getResources().getResourceTypeName(resourceId));
149         } catch (Resources.NotFoundException e) {
150             return false;
151         }
152     }
153 
getResourceId()154     public @AnyRes int getResourceId() {
155         return mResourceId;
156     }
157 
158     @NonNull
getPackageName()159     public String getPackageName() {
160         return mPackageName;
161     }
162 
163     @NonNull
getResourceName()164     public String getResourceName() {
165         return mResourceName;
166     }
167 
getResourceType()168     public int getResourceType() {
169         return mResourceType;
170     }
171 
172     /**
173      * Loads the drawable with id {@code mResourceId} from {@code mPackageName} using the provided
174      * {@code density} and {@link Resources.Theme} and {@link Resources#getConfiguration} of the
175      * provided {@code context}.
176      *
177      * <p>Returns the default drawable by calling the {@code defaultDrawableLoader} if the updated
178      * drawable was not found or could not be loaded.</p>
179      */
180     @Nullable
getDrawable( Context context, int density, @NonNull Supplier<Drawable> defaultDrawableLoader)181     public Drawable getDrawable(
182             Context context,
183             int density,
184             @NonNull Supplier<Drawable> defaultDrawableLoader) {
185         // TODO(b/203548565): properly handle edge case when the device manager role holder is
186         //  unavailable because it's being updated.
187         try {
188             Resources resources = getAppResourcesWithCallersConfiguration(context);
189             verifyResourceName(resources);
190             return resources.getDrawableForDensity(mResourceId, density, context.getTheme());
191         } catch (PackageManager.NameNotFoundException | RuntimeException e) {
192             Slog.e(TAG, "Unable to load drawable resource " + mResourceName, e);
193             return loadDefaultDrawable(defaultDrawableLoader);
194         }
195     }
196 
197     /**
198      * Loads the string with id {@code mResourceId} from {@code mPackageName} using the
199      * configuration returned from {@link Resources#getConfiguration} of the provided
200      * {@code context}.
201      *
202      * <p>Returns the default string by calling  {@code defaultStringLoader} if the updated
203      * string was not found or could not be loaded.</p>
204      */
205     @Nullable
getString( Context context, @NonNull Supplier<String> defaultStringLoader)206     public String getString(
207             Context context,
208             @NonNull Supplier<String> defaultStringLoader) {
209         // TODO(b/203548565): properly handle edge case when the device manager role holder is
210         //  unavailable because it's being updated.
211         try {
212             Resources resources = getAppResourcesWithCallersConfiguration(context);
213             verifyResourceName(resources);
214             return resources.getString(mResourceId);
215         } catch (PackageManager.NameNotFoundException | RuntimeException e) {
216             Slog.e(TAG, "Unable to load string resource " + mResourceName, e);
217             return loadDefaultString(defaultStringLoader);
218         }
219     }
220 
221     /**
222      * Loads the string with id {@code mResourceId} from {@code mPackageName} using the
223      * configuration returned from {@link Resources#getConfiguration} of the provided
224      * {@code context}.
225      *
226      * <p>Returns the default string by calling  {@code defaultStringLoader} if the updated
227      * string was not found or could not be loaded.</p>
228      */
229     @Nullable
getString( Context context, @NonNull Supplier<String> defaultStringLoader, @NonNull Object... formatArgs)230     public String getString(
231             Context context,
232             @NonNull Supplier<String> defaultStringLoader,
233             @NonNull Object... formatArgs) {
234         // TODO(b/203548565): properly handle edge case when the device manager role holder is
235         //  unavailable because it's being updated.
236         try {
237             Resources resources = getAppResourcesWithCallersConfiguration(context);
238             verifyResourceName(resources);
239             String rawString = resources.getString(mResourceId);
240             return String.format(
241                     context.getResources().getConfiguration().getLocales().get(0),
242                     rawString,
243                     formatArgs);
244         } catch (PackageManager.NameNotFoundException | RuntimeException e) {
245             Slog.e(TAG, "Unable to load string resource " + mResourceName, e);
246             return loadDefaultString(defaultStringLoader);
247         }
248     }
249 
getAppResourcesWithCallersConfiguration(Context context)250     private Resources getAppResourcesWithCallersConfiguration(Context context)
251             throws PackageManager.NameNotFoundException {
252         PackageManager pm = context.getPackageManager();
253         ApplicationInfo ai = pm.getApplicationInfo(
254                 mPackageName,
255                 PackageManager.MATCH_UNINSTALLED_PACKAGES
256                         | PackageManager.GET_SHARED_LIBRARY_FILES);
257         return pm.getResourcesForApplication(ai, context.getResources().getConfiguration());
258     }
259 
verifyResourceName(Resources resources)260     private void verifyResourceName(Resources resources) throws IllegalStateException {
261         String name = resources.getResourceName(mResourceId);
262         if (!mResourceName.equals(name)) {
263             throw new IllegalStateException(String.format("Current resource name %s for resource id"
264                             + " %d has changed from the previously stored resource name %s.",
265                     name, mResourceId, mResourceName));
266         }
267     }
268 
269     /**
270      * returns the {@link Drawable} loaded from calling {@code defaultDrawableLoader}.
271      */
272     @Nullable
loadDefaultDrawable(@onNull Supplier<Drawable> defaultDrawableLoader)273     public static Drawable loadDefaultDrawable(@NonNull Supplier<Drawable> defaultDrawableLoader) {
274         Objects.requireNonNull(defaultDrawableLoader, "defaultDrawableLoader can't be null");
275         return defaultDrawableLoader.get();
276     }
277 
278     /**
279      * returns the {@link String} loaded from calling {@code defaultStringLoader}.
280      */
281     @Nullable
loadDefaultString(@onNull Supplier<String> defaultStringLoader)282     public static String loadDefaultString(@NonNull Supplier<String> defaultStringLoader) {
283         Objects.requireNonNull(defaultStringLoader, "defaultStringLoader can't be null");
284         return defaultStringLoader.get();
285     }
286 
287     /**
288      * Writes the content of the current {@code ParcelableDevicePolicyResource} to the xml file
289      * specified by {@code xmlSerializer}.
290      */
writeToXmlFile(TypedXmlSerializer xmlSerializer)291     public void writeToXmlFile(TypedXmlSerializer xmlSerializer) throws IOException {
292         xmlSerializer.attributeInt(/* namespace= */ null, ATTR_RESOURCE_ID, mResourceId);
293         xmlSerializer.attribute(/* namespace= */ null, ATTR_PACKAGE_NAME, mPackageName);
294         xmlSerializer.attribute(/* namespace= */ null, ATTR_RESOURCE_NAME, mResourceName);
295         xmlSerializer.attributeInt(/* namespace= */ null, ATTR_RESOURCE_TYPE, mResourceType);
296     }
297 
298     /**
299      * Creates a new {@code ParcelableDevicePolicyResource} using the content of
300      * {@code xmlPullParser}.
301      */
createFromXml(TypedXmlPullParser xmlPullParser)302     public static ParcelableResource createFromXml(TypedXmlPullParser xmlPullParser)
303             throws XmlPullParserException, IOException {
304         int resourceId = xmlPullParser.getAttributeInt(/* namespace= */ null, ATTR_RESOURCE_ID);
305         String packageName = xmlPullParser.getAttributeValue(
306                 /* namespace= */ null, ATTR_PACKAGE_NAME);
307         String resourceName = xmlPullParser.getAttributeValue(
308                 /* namespace= */ null, ATTR_RESOURCE_NAME);
309         int resourceType = xmlPullParser.getAttributeInt(
310                 /* namespace= */ null, ATTR_RESOURCE_TYPE);
311 
312         return new ParcelableResource(
313                 resourceId, packageName, resourceName, resourceType);
314     }
315 
316     @Override
equals(@ullable Object o)317     public boolean equals(@Nullable Object o) {
318         if (this == o) return true;
319         if (o == null || getClass() != o.getClass()) return false;
320         ParcelableResource other = (ParcelableResource) o;
321         return mResourceId == other.mResourceId
322                 && mPackageName.equals(other.mPackageName)
323                 && mResourceName.equals(other.mResourceName)
324                 && mResourceType == other.mResourceType;
325     }
326 
327     @Override
hashCode()328     public int hashCode() {
329         return Objects.hash(mResourceId, mPackageName, mResourceName, mResourceType);
330     }
331 
332     @Override
describeContents()333     public int describeContents() {
334         return 0;
335     }
336 
337     @Override
writeToParcel(Parcel dest, int flags)338     public void writeToParcel(Parcel dest, int flags) {
339         dest.writeInt(mResourceId);
340         dest.writeString(mPackageName);
341         dest.writeString(mResourceName);
342         dest.writeInt(mResourceType);
343     }
344 
345     public static final @NonNull Creator<ParcelableResource> CREATOR =
346             new Creator<ParcelableResource>() {
347                 @Override
348                 public ParcelableResource createFromParcel(Parcel in) {
349                     int resourceId = in.readInt();
350                     String packageName = in.readString();
351                     String resourceName = in.readString();
352                     int resourceType = in.readInt();
353 
354                     return new ParcelableResource(
355                             resourceId, packageName, resourceName, resourceType);
356                 }
357 
358                 @Override
359                 public ParcelableResource[] newArray(int size) {
360                     return new ParcelableResource[size];
361                 }
362             };
363 }
364