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