1 /*
2  * Copyright (C) 2016 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.car.apps.common;
17 
18 import static android.content.pm.PackageManager.MATCH_ALL;
19 
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.content.Intent.ShortcutIconResource;
23 import android.content.pm.PackageManager;
24 import android.content.pm.PackageManager.NameNotFoundException;
25 import android.content.pm.ProviderInfo;
26 import android.content.res.Resources;
27 import android.graphics.drawable.Drawable;
28 import android.net.Uri;
29 import android.text.TextUtils;
30 
31 import androidx.annotation.Nullable;
32 
33 /**
34  * Utilities for working with URIs.
35  */
36 public final class UriUtils {
37 
38     private static final String SCHEME_SHORTCUT_ICON_RESOURCE = "shortcut.icon.resource";
39     private static final String SCHEME_DELIMITER = "://";
40     private static final String URI_PATH_DELIMITER = "/";
41     private static final String URI_PACKAGE_DELIMITER = ":";
42     private static final String HTTP_PREFIX = "http";
43     private static final String HTTPS_PREFIX = "https";
44     private static final String SCHEME_ACCOUNT_IMAGE = "image.account";
45     private static final String ACCOUNT_IMAGE_CHANGE_NOTIFY_URI = "change_notify_uri";
46     private static final String DETAIL_DIALOG_URI_DIALOG_TITLE = "detail_dialog_title";
47     private static final String DETAIL_DIALOG_URI_DIALOG_DESCRIPTION = "detail_dialog_description";
48     private static final String DETAIL_DIALOG_URI_DIALOG_ACTION_START_INDEX =
49             "detail_dialog_action_start_index";
50     private static final String DETAIL_DIALOG_URI_DIALOG_ACTION_START_NAME =
51             "detail_dialog_action_start_name";
52 
53     /**
54      * Non instantiable.
55      */
UriUtils()56     private UriUtils() {}
57 
58     /** Returns true if the uri is null or empty. */
isEmpty(@ullable Uri uri)59     public static boolean isEmpty(@Nullable Uri uri) {
60         return (uri == null || TextUtils.isEmpty(uri.toString()));
61     }
62 
63     /**
64      * Gets resource uri representation for a resource of a package
65      */
getAndroidResourceUri(Context context, int resourceId)66     public static String getAndroidResourceUri(Context context, int resourceId) {
67         return getAndroidResourceUri(context.getResources(), resourceId);
68     }
69 
70     /**
71      * Gets resource uri representation for a resource
72      */
getAndroidResourceUri(Resources resources, int resourceId)73     public static String getAndroidResourceUri(Resources resources, int resourceId) {
74         return ContentResolver.SCHEME_ANDROID_RESOURCE
75                 + SCHEME_DELIMITER + resources.getResourceName(resourceId)
76                         .replace(URI_PACKAGE_DELIMITER, URI_PATH_DELIMITER);
77     }
78 
79     /**
80      * Loads drawable from resource
81      */
82     @Nullable
getDrawable(Context context, ShortcutIconResource r)83     public static Drawable getDrawable(Context context, ShortcutIconResource r) {
84         Resources resources = null;
85         try {
86             resources = context.getPackageManager().getResourcesForApplication(r.packageName);
87         } catch (NameNotFoundException e) {
88             // Return null below.
89         }
90         if (resources == null) {
91             return null;
92         }
93         final int id = resources.getIdentifier(r.resourceName, null, null);
94         return resources.getDrawable(id, null);
95     }
96 
97     /**
98      * Gets a URI with short cut icon scheme.
99      */
getShortcutIconResourceUri(ShortcutIconResource iconResource)100     public static Uri getShortcutIconResourceUri(ShortcutIconResource iconResource) {
101         return Uri.parse(SCHEME_SHORTCUT_ICON_RESOURCE + SCHEME_DELIMITER + iconResource.packageName
102                 + URI_PATH_DELIMITER
103                 + iconResource.resourceName.replace(URI_PACKAGE_DELIMITER, URI_PATH_DELIMITER));
104     }
105 
106     /**
107      * Gets a URI with scheme = {@link ContentResolver#SCHEME_ANDROID_RESOURCE} for
108      * a full resource name. This name is a single string of the form "package:type/entry".
109      */
getAndroidResourceUri(String resourceName)110     public static Uri getAndroidResourceUri(String resourceName) {
111         Uri uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + SCHEME_DELIMITER
112                 + resourceName.replace(URI_PACKAGE_DELIMITER, URI_PATH_DELIMITER));
113         return uri;
114     }
115 
116     /**
117      * Checks if the URI refers to an Android resource.
118      */
isAndroidResourceUri(Uri uri)119     public static boolean isAndroidResourceUri(Uri uri) {
120         return ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme());
121     }
122 
123     /**
124      * Gets a URI with the account image scheme.
125      * @hide
126      */
getAccountImageUri(String accountName)127     public static Uri getAccountImageUri(String accountName) {
128         Uri uri = Uri.parse(SCHEME_ACCOUNT_IMAGE + SCHEME_DELIMITER + accountName);
129         return uri;
130     }
131 
132     /**
133      * Gets a URI with the account image scheme, and specifying an URI to be
134      * used in notifyChange() when the image pointed to by the returned URI is
135      * updated.
136      * @hide
137      */
getAccountImageUri(String accountName, Uri changeNotifyUri)138     public static Uri getAccountImageUri(String accountName, Uri changeNotifyUri) {
139         Uri uri = Uri.parse(SCHEME_ACCOUNT_IMAGE + SCHEME_DELIMITER + accountName);
140         if (changeNotifyUri != null) {
141             uri = uri.buildUpon().appendQueryParameter(ACCOUNT_IMAGE_CHANGE_NOTIFY_URI,
142                     changeNotifyUri.toString()).build();
143         }
144         return uri;
145     }
146 
147     /**
148      * Checks if the URI refers to an account image.
149      * @hide
150      */
isAccountImageUri(Uri uri)151     public static boolean isAccountImageUri(Uri uri) {
152         return uri == null ? false : SCHEME_ACCOUNT_IMAGE.equals(uri.getScheme());
153     }
154 
155     /**
156      * @hide
157      */
getAccountName(Uri uri)158     public static String getAccountName(Uri uri) {
159         if (isAccountImageUri(uri)) {
160             String accountName = uri.getAuthority() + uri.getPath();
161             return accountName;
162         } else {
163             throw new IllegalArgumentException("Invalid account image URI. " + uri);
164         }
165     }
166 
167     /**
168      * @hide
169      */
getAccountImageChangeNotifyUri(Uri uri)170     public static Uri getAccountImageChangeNotifyUri(Uri uri) {
171         if (isAccountImageUri(uri)) {
172             String notifyUri = uri.getQueryParameter(ACCOUNT_IMAGE_CHANGE_NOTIFY_URI);
173             if (notifyUri == null) {
174                 return null;
175             } else {
176                 return Uri.parse(notifyUri);
177             }
178         } else {
179             throw new IllegalArgumentException("Invalid account image URI. " + uri);
180         }
181     }
182 
183     /**
184      * Finds the packageName of the application to which the content authority of the given uri
185      * belongs to.
186      */
187     @Nullable
getPackageName(Context context, Uri uri)188     public static String getPackageName(Context context, Uri uri) {
189         PackageManager pm = context.getPackageManager();
190         ProviderInfo info = pm.resolveContentProvider(uri.getAuthority(), MATCH_ALL);
191         // Info can be null when the app doesn't define a provider.
192         return (info != null) ? info.packageName : uri.getAuthority();
193     }
194 
195     /**
196      * Returns {@code true} if the URI refers to a content URI which can be opened via
197      * {@link ContentResolver#openInputStream(Uri)}.
198      */
isContentUri(Uri uri)199     public static boolean isContentUri(Uri uri) {
200         return ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) ||
201                 ContentResolver.SCHEME_FILE.equals(uri.getScheme());
202     }
203 
204     /**
205      * Checks if the URI refers to an shortcut icon resource.
206      */
isShortcutIconResourceUri(Uri uri)207     public static boolean isShortcutIconResourceUri(Uri uri) {
208         return SCHEME_SHORTCUT_ICON_RESOURCE.equals(uri.getScheme());
209     }
210 
211     /**
212      * Creates a shortcut icon resource object from an Android resource URI.
213      */
getIconResource(Context context, Uri uri)214     public static ShortcutIconResource getIconResource(Context context, Uri uri) {
215         if(isAndroidResourceUri(uri)) {
216             ShortcutIconResource iconResource = new ShortcutIconResource();
217             iconResource.packageName = getPackageName(context, uri);
218             // Trim off the scheme + 3 extra for "://" + authority, then replace the first "/"
219             // with a ":" and add to packageName.
220             int resStart = ContentResolver.SCHEME_ANDROID_RESOURCE.length()
221                     + SCHEME_DELIMITER.length() + uri.getAuthority().length();
222             iconResource.resourceName = iconResource.packageName
223                     + uri.toString().substring(resStart)
224                             .replaceFirst(URI_PATH_DELIMITER, URI_PACKAGE_DELIMITER);
225             return iconResource;
226         } else if(isShortcutIconResourceUri(uri)) {
227             ShortcutIconResource iconResource = new ShortcutIconResource();
228             iconResource.packageName = getPackageName(context, uri);
229             iconResource.resourceName = uri.toString().substring(
230                     SCHEME_SHORTCUT_ICON_RESOURCE.length() + SCHEME_DELIMITER.length()
231                     + uri.getAuthority().length() + URI_PATH_DELIMITER.length())
232                     .replaceFirst(URI_PATH_DELIMITER, URI_PACKAGE_DELIMITER);
233             return iconResource;
234         } else {
235             throw new IllegalArgumentException("Invalid resource URI. " + uri);
236         }
237     }
238 
239     /**
240      * Returns {@code true} if this is a web URI.
241      */
isWebUri(Uri resourceUri)242     public static boolean isWebUri(Uri resourceUri) {
243         String scheme = resourceUri.getScheme() == null ? null
244                 : resourceUri.getScheme().toLowerCase();
245         return HTTP_PREFIX.equals(scheme) || HTTPS_PREFIX.equals(scheme);
246     }
247 
248     /**
249      * Build a Uri for canvas details subactions dialog given content uri and optional parameters.
250      * @param uri the subactions ContentUri
251      * @param dialogTitle the custom subactions dialog title. If the value is null, canvas will
252      *        fall back to use previous action's name as the subactions dialog title.
253      * @param dialogDescription the custom subactions dialog description. If the value is null,
254      *        canvas will fall back to use previous action's subname as the subactions dialog
255      *        description.
256      * @hide
257      */
getSubactionDialogUri(Uri uri, String dialogTitle, String dialogDescription)258     public static Uri getSubactionDialogUri(Uri uri, String dialogTitle, String dialogDescription) {
259         return getSubactionDialogUri(uri, dialogTitle, dialogDescription, null, -1);
260     }
261 
262     /**
263      * Build a Uri for canvas details subactions dialog given content uri and optional parameters.
264      * @param uri the subactions ContentUri
265      * @param dialogTitle the custom subactions dialog title. If the value is null, canvas will
266      *        fall back to use previous action's name as the subactions dialog title.
267      * @param dialogDescription the custom subactions dialog description. If the value is null,
268      *        canvas will fall back to use previous action's subname as the subactions dialog
269      *        description.
270      * @param startIndex the focused action in actions list when started.
271      * @hide
272      */
getSubactionDialogUri(Uri uri, String dialogTitle, String dialogDescription, int startIndex)273     public static Uri getSubactionDialogUri(Uri uri, String dialogTitle, String dialogDescription,
274             int startIndex) {
275         return getSubactionDialogUri(uri, dialogTitle, dialogDescription, null, startIndex);
276     }
277 
278     /**
279      * Build a Uri for canvas details subactions dialog given content uri and optional parameters.
280      * @param uri the subactions ContentUri
281      * @param dialogTitle the custom subactions dialog title. If the value is null, canvas will
282      *        fall back to use previous action's name as the subactions dialog title.
283      * @param dialogDescription the custom subactions dialog description. If the value is null,
284      *        canvas will fall back to use previous action's subname as the subactions dialog
285      *        description.
286      * @param startName the name of action that is focused in actions list when started.
287      * @hide
288      */
getSubactionDialogUri(Uri uri, String dialogTitle, String dialogDescription, String startName)289     public static Uri getSubactionDialogUri(Uri uri, String dialogTitle, String dialogDescription,
290             String startName) {
291         return getSubactionDialogUri(uri, dialogTitle, dialogDescription, startName, -1);
292     }
293 
294     /**
295      * Build a Uri for canvas details subactions dialog given content uri and optional parameters.
296      * @param uri the subactions ContentUri
297      * @param dialogTitle the custom subactions dialog title. If the value is null, canvas will
298      *        fall back to use previous action's name as the subactions dialog title.
299      * @param dialogDescription the custom subactions dialog description. If the value is null,
300      *        canvas will fall back to use previous action's subname as the subactions dialog
301      *        description.
302      * @param startIndex the focused action in actions list when started.
303      * @param startName the name of action that is focused in actions list when started. startName
304      *        takes priority over start index.
305      * @hide
306      */
getSubactionDialogUri(Uri uri, String dialogTitle, String dialogDescription, String startName, int startIndex)307     public static Uri getSubactionDialogUri(Uri uri, String dialogTitle, String dialogDescription,
308             String startName, int startIndex) {
309         if (uri == null || !isContentUri(uri)) {
310             // If given uri is null, or it is not of contentUri type, return null.
311             return null;
312         }
313 
314         Uri.Builder builder = uri.buildUpon();
315         if (!TextUtils.isEmpty(dialogTitle)) {
316             builder.appendQueryParameter(DETAIL_DIALOG_URI_DIALOG_TITLE, dialogTitle);
317         }
318 
319         if (!TextUtils.isEmpty(DETAIL_DIALOG_URI_DIALOG_DESCRIPTION)) {
320             builder.appendQueryParameter(DETAIL_DIALOG_URI_DIALOG_DESCRIPTION, dialogDescription);
321         }
322 
323         if (startIndex != -1) {
324             builder.appendQueryParameter(DETAIL_DIALOG_URI_DIALOG_ACTION_START_INDEX,
325                     Integer.toString(startIndex));
326         }
327 
328         if (!TextUtils.isEmpty(startName)) {
329             builder.appendQueryParameter(DETAIL_DIALOG_URI_DIALOG_ACTION_START_NAME, startName);
330         }
331 
332         return builder.build();
333     }
334 
335     /**
336      * Get subaction dialog title parameter from URI
337      * @param uri ContentUri for canvas details subactions
338      * @return custom dialog title if this parameter is available in URI. Otherwise, return null.
339      * @hide
340      */
getSubactionDialogTitle(Uri uri)341     public static String getSubactionDialogTitle(Uri uri) {
342         if (uri == null || !isContentUri(uri)) {
343             return null;
344         }
345 
346         return uri.getQueryParameter(DETAIL_DIALOG_URI_DIALOG_TITLE);
347     }
348 
349     /**
350      * Get subaction dialog description parameter from URI
351      * @param uri ContentUri for canvas details subactions
352      * @return custom dialog description if this parameter is available in URI.
353      * Otherwise, return null.
354      * @hide
355      */
getSubactionDialogDescription(Uri uri)356     public static String getSubactionDialogDescription(Uri uri) {
357         if (uri == null || !isContentUri(uri)) {
358             return null;
359         }
360 
361         return uri.getQueryParameter(DETAIL_DIALOG_URI_DIALOG_DESCRIPTION);
362     }
363 
364     /**
365      * Get subaction dialog action list focused index when started from URI
366      * @param uri ContentUri for canvas details subactions
367      * @return action starting index if this parameter is available in URI. Otherwise, return -1.
368      * @hide
369      */
getSubactionDialogActionStartIndex(Uri uri)370     public static int getSubactionDialogActionStartIndex(Uri uri) {
371         if (uri == null || !isContentUri(uri)) {
372             return -1;
373         }
374 
375         String startIndexStr = uri.getQueryParameter(DETAIL_DIALOG_URI_DIALOG_ACTION_START_INDEX);
376         if (!TextUtils.isEmpty(startIndexStr) && TextUtils.isDigitsOnly(startIndexStr)) {
377             return Integer.parseInt(startIndexStr);
378         } else {
379             return -1;
380         }
381     }
382 
383     /**
384      * Get subaction dialog action list focused action name when started from URI
385      * @param uri ContentUri for canvas details subactions
386      * @return that name of starting action if this parameter is available in URI.
387      * Otherwise, return null.
388      * @hide
389      */
getSubactionDialogActionStartName(Uri uri)390     public static String getSubactionDialogActionStartName(Uri uri) {
391         if (uri == null || !isContentUri(uri)) {
392             return null;
393         }
394 
395         return uri.getQueryParameter(DETAIL_DIALOG_URI_DIALOG_ACTION_START_NAME);
396     }
397 }
398