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