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