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