1 /* 2 * Copyright (C) 2020 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 package com.android.server.textclassifier; 17 18 import android.annotation.Nullable; 19 import android.net.Uri; 20 import android.util.ArrayMap; 21 import android.util.Log; 22 23 import com.android.internal.annotations.GuardedBy; 24 import com.android.internal.annotations.VisibleForTesting; 25 import com.android.internal.annotations.VisibleForTesting.Visibility; 26 27 import java.util.List; 28 import java.util.Map; 29 import java.util.Objects; 30 import java.util.UUID; 31 import java.util.function.Supplier; 32 33 /** 34 * A helper for mapping an icon resource to a content uri. 35 * 36 * <p>NOTE: Care must be taken to avoid passing resource uris to non-permitted apps via this helper. 37 */ 38 @VisibleForTesting(visibility = Visibility.PACKAGE) 39 public final class IconsUriHelper { 40 41 public static final String AUTHORITY = "com.android.textclassifier.icons"; 42 43 private static final String TAG = "IconsUriHelper"; 44 private static final Supplier<String> DEFAULT_ID_SUPPLIER = () -> UUID.randomUUID().toString(); 45 46 // TODO: Consider using an LRU cache to limit resource usage. 47 // This may depend on the expected number of packages that a device typically has. 48 @GuardedBy("mPackageIds") 49 private final Map<String, String> mPackageIds = new ArrayMap<>(); 50 51 private final Supplier<String> mIdSupplier; 52 53 private static final IconsUriHelper sSingleton = new IconsUriHelper(null); 54 IconsUriHelper(@ullable Supplier<String> idSupplier)55 private IconsUriHelper(@Nullable Supplier<String> idSupplier) { 56 mIdSupplier = (idSupplier != null) ? idSupplier : DEFAULT_ID_SUPPLIER; 57 58 // Useful for testing: 59 // Magic id for the android package so it is the same across classloaders. 60 // This is okay as this package does not have access restrictions, and 61 // the TextClassifierService hardly returns icons from this package. 62 mPackageIds.put("android", "android"); 63 } 64 65 /** 66 * Returns a new instance of this object for testing purposes. 67 */ newInstanceForTesting(@ullable Supplier<String> idSupplier)68 public static IconsUriHelper newInstanceForTesting(@Nullable Supplier<String> idSupplier) { 69 return new IconsUriHelper(idSupplier); 70 } 71 getInstance()72 static IconsUriHelper getInstance() { 73 return sSingleton; 74 } 75 76 /** 77 * Returns a Uri for the specified icon resource. 78 * 79 * @param packageName the resource's package name 80 * @param resId the resource id 81 * @see #getResourceInfo(Uri) 82 */ getContentUri(String packageName, int resId)83 public Uri getContentUri(String packageName, int resId) { 84 Objects.requireNonNull(packageName); 85 synchronized (mPackageIds) { 86 if (!mPackageIds.containsKey(packageName)) { 87 // TODO: Ignore packages that don't actually exist on the device. 88 mPackageIds.put(packageName, mIdSupplier.get()); 89 } 90 return new Uri.Builder() 91 .scheme("content") 92 .authority(AUTHORITY) 93 .path(mPackageIds.get(packageName)) 94 .appendPath(Integer.toString(resId)) 95 .build(); 96 } 97 } 98 99 /** 100 * Returns a valid {@link ResourceInfo} for the specified uri. Returns {@code null} if a valid 101 * {@link ResourceInfo} cannot be returned for the specified uri. 102 * 103 * @see #getContentUri(String, int); 104 */ 105 @Nullable getResourceInfo(Uri uri)106 public ResourceInfo getResourceInfo(Uri uri) { 107 if (!"content".equals(uri.getScheme())) { 108 return null; 109 } 110 if (!AUTHORITY.equals(uri.getAuthority())) { 111 return null; 112 } 113 114 final List<String> pathItems = uri.getPathSegments(); 115 try { 116 synchronized (mPackageIds) { 117 final String packageId = pathItems.get(0); 118 final int resId = Integer.parseInt(pathItems.get(1)); 119 for (String packageName : mPackageIds.keySet()) { 120 if (packageId.equals(mPackageIds.get(packageName))) { 121 return new ResourceInfo(packageName, resId); 122 } 123 } 124 } 125 } catch (Exception e) { 126 Log.v(TAG, "Could not get resource info. Reason: " + e.getMessage()); 127 } 128 return null; 129 } 130 131 /** 132 * A holder for a resource's package name and id. 133 */ 134 public static final class ResourceInfo { 135 136 public final String packageName; 137 public final int id; 138 ResourceInfo(String packageName, int id)139 private ResourceInfo(String packageName, int id) { 140 this.packageName = Objects.requireNonNull(packageName); 141 this.id = id; 142 } 143 } 144 } 145