1 /*
2  * Copyright (C) 2015 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.messaging.util;
17 
18 import android.content.ContentResolver;
19 import android.content.Context;
20 import android.content.res.AssetFileDescriptor;
21 import android.media.MediaMetadataRetriever;
22 import android.net.Uri;
23 import android.os.ParcelFileDescriptor;
24 import android.provider.MediaStore;
25 import androidx.annotation.NonNull;
26 import android.text.TextUtils;
27 
28 import com.android.messaging.Factory;
29 import com.android.messaging.datamodel.GalleryBoundCursorLoader;
30 import com.android.messaging.datamodel.MediaScratchFileProvider;
31 import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
32 import com.google.common.io.ByteStreams;
33 
34 import java.io.BufferedInputStream;
35 import java.io.File;
36 import java.io.FileNotFoundException;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.io.OutputStream;
40 import java.net.URL;
41 import java.net.URLConnection;
42 import java.util.Arrays;
43 import java.util.HashSet;
44 
45 public class UriUtil {
46     private static final String SCHEME_SMS = "sms";
47     private static final String SCHEME_SMSTO = "smsto";
48     private static final String SCHEME_MMS = "mms";
49     private static final String SCHEME_MMSTO = "smsto";
50     public static final HashSet<String> SMS_MMS_SCHEMES = new HashSet<String>(
51         Arrays.asList(SCHEME_SMS, SCHEME_MMS, SCHEME_SMSTO, SCHEME_MMSTO));
52 
53     public static final String SCHEME_BUGLE = "bugle";
54     public static final HashSet<String> SUPPORTED_SCHEME = new HashSet<String>(
55         Arrays.asList(ContentResolver.SCHEME_ANDROID_RESOURCE,
56             ContentResolver.SCHEME_CONTENT,
57             ContentResolver.SCHEME_FILE,
58             SCHEME_BUGLE));
59 
60     public static final String SCHEME_TEL = "tel:";
61 
62     /**
63      * Get a Uri representation of the file path of a resource file.
64      */
getUriForResourceFile(final String path)65     public static Uri getUriForResourceFile(final String path) {
66         return TextUtils.isEmpty(path) ? null : Uri.fromFile(new File(path));
67     }
68 
69     /**
70      * Extract the path from a file:// Uri, or null if the uri is of other scheme.
71      */
getFilePathFromUri(final Uri uri)72     public static String getFilePathFromUri(final Uri uri) {
73         if (!isFileUri(uri)) {
74             return null;
75         }
76         return uri.getPath();
77     }
78 
79     /**
80      * Returns whether the given Uri is local or remote.
81      */
isLocalResourceUri(final Uri uri)82     public static boolean isLocalResourceUri(final Uri uri) {
83         final String scheme = uri.getScheme();
84         return TextUtils.equals(scheme, ContentResolver.SCHEME_ANDROID_RESOURCE) ||
85                 TextUtils.equals(scheme, ContentResolver.SCHEME_CONTENT) ||
86                 TextUtils.equals(scheme, ContentResolver.SCHEME_FILE);
87     }
88 
89     /**
90      * Returns whether the given Uri is part of Bugle's app package
91      */
isBugleAppResource(final Uri uri)92     public static boolean isBugleAppResource(final Uri uri) {
93         final String scheme = uri.getScheme();
94         return TextUtils.equals(scheme, ContentResolver.SCHEME_ANDROID_RESOURCE);
95     }
96 
97     /** Returns whether the given Uri is a file. */
isFileUri(final Uri uri)98     public static boolean isFileUri(final Uri uri) {
99         return uri != null &&
100                 uri.getScheme() != null &&
101                 TextUtils.equals(uri.getScheme().trim().toLowerCase(),
102                         ContentResolver.SCHEME_FILE);
103     }
104 
105     /**
106      * Constructs an android.resource:// uri for the given resource id.
107      */
getUriForResourceId(final Context context, final int resId)108     public static Uri getUriForResourceId(final Context context, final int resId) {
109         return new Uri.Builder()
110                 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
111                 .authority(context.getPackageName())
112                 .appendPath(String.valueOf(resId))
113                 .build();
114     }
115 
116     /**
117      * Returns whether the given Uri string is local.
118      */
isLocalUri(@onNull final Uri uri)119     public static boolean isLocalUri(@NonNull final Uri uri) {
120         Assert.notNull(uri);
121         return SUPPORTED_SCHEME.contains(uri.getScheme());
122     }
123 
124     private static final String MEDIA_STORE_URI_KLP = "com.android.providers.media.documents";
125 
126     /**
127      * Check if a URI is from the MediaStore
128      */
isMediaStoreUri(final Uri uri)129     public static boolean isMediaStoreUri(final Uri uri) {
130         final String uriAuthority = uri.getAuthority();
131         return TextUtils.equals(ContentResolver.SCHEME_CONTENT, uri.getScheme())
132                 && (TextUtils.equals(MediaStore.AUTHORITY, uriAuthority) ||
133                 // KK changed the media store authority name
134                 TextUtils.equals(MEDIA_STORE_URI_KLP, uriAuthority));
135     }
136 
137     /**
138      * Gets the content:// style URI for the given MediaStore row Id in the files table on the
139      * external volume.
140      *
141      * @param id the MediaStore row Id to get the URI for
142      * @return the URI to the files table on the external storage.
143      */
getContentUriForMediaStoreId(final long id)144     public static Uri getContentUriForMediaStoreId(final long id) {
145         return MediaStore.Files.getContentUri(
146                 GalleryBoundCursorLoader.MEDIA_SCANNER_VOLUME_EXTERNAL, id);
147     }
148 
149     /**
150      * Gets the size in bytes for the content uri. Currently we only support content in the
151      * scratch space.
152      */
153     @DoesNotRunOnMainThread
getContentSize(final Uri uri)154     public static long getContentSize(final Uri uri) {
155         Assert.isNotMainThread();
156         if (isLocalResourceUri(uri)) {
157             ParcelFileDescriptor pfd = null;
158             try {
159                 pfd = Factory.get().getApplicationContext()
160                         .getContentResolver().openFileDescriptor(uri, "r");
161                 return Math.max(pfd.getStatSize(), 0);
162             } catch (final FileNotFoundException e) {
163                 LogUtil.e(LogUtil.BUGLE_TAG, "Error getting content size", e);
164             } finally {
165                 if (pfd != null) {
166                     try {
167                         pfd.close();
168                     } catch (final IOException e) {
169                         // Do nothing.
170                     }
171                 }
172             }
173         } else {
174             Assert.fail("Unsupported uri type!");
175         }
176         return 0;
177     }
178 
179     /** @return duration in milliseconds or 0 if not able to determine */
getMediaDurationMs(final Uri uri)180     public static int getMediaDurationMs(final Uri uri) {
181         final MediaMetadataRetrieverWrapper retriever = new MediaMetadataRetrieverWrapper();
182         try {
183             retriever.setDataSource(uri);
184             return retriever.extractInteger(MediaMetadataRetriever.METADATA_KEY_DURATION, 0);
185         } catch (final IOException e) {
186             LogUtil.e(LogUtil.BUGLE_TAG, "Unable extract duration from media file: " + uri, e);
187             return 0;
188         } finally {
189             retriever.release();
190         }
191     }
192 
193     /**
194      * Persist a piece of content from the given input stream, byte by byte to the scratch
195      * directory.
196      * @return the output Uri if the operation succeeded, or null if failed.
197      */
198     @DoesNotRunOnMainThread
persistContentToScratchSpace(final InputStream inputStream)199     public static Uri persistContentToScratchSpace(final InputStream inputStream) {
200         final Context context = Factory.get().getApplicationContext();
201         final Uri scratchSpaceUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(null);
202         return copyContent(context, inputStream, scratchSpaceUri);
203     }
204 
205     /**
206      * Persist a piece of content from the given sourceUri, byte by byte to the scratch
207      * directory.
208      * @return the output Uri if the operation succeeded, or null if failed.
209      */
210     @DoesNotRunOnMainThread
persistContentToScratchSpace(final Uri sourceUri)211     public static Uri persistContentToScratchSpace(final Uri sourceUri) {
212         InputStream inputStream = null;
213         final Context context = Factory.get().getApplicationContext();
214         try {
215             if (UriUtil.isLocalResourceUri(sourceUri)) {
216                 inputStream = context.getContentResolver().openInputStream(sourceUri);
217             } else {
218                 // The content is remote. Download it.
219                 final URL url = new URL(sourceUri.toString());
220                 final URLConnection ucon = url.openConnection();
221                 inputStream = new BufferedInputStream(ucon.getInputStream());
222             }
223             return persistContentToScratchSpace(inputStream);
224         } catch (final Exception ex) {
225             LogUtil.e(LogUtil.BUGLE_TAG, "Error while retrieving media ", ex);
226             return null;
227         } finally {
228             if (inputStream != null) {
229                 try {
230                     inputStream.close();
231                 } catch (final IOException e) {
232                     LogUtil.e(LogUtil.BUGLE_TAG, "error trying to close the inputStream", e);
233                 }
234             }
235         }
236     }
237 
238     /**
239      * Persist a piece of content from the given input stream, byte by byte to the specified
240      * directory.
241      * @return the output Uri if the operation succeeded, or null if failed.
242      */
243     @DoesNotRunOnMainThread
persistContent( final InputStream inputStream, final File outputDir, final String contentType)244     public static Uri persistContent(
245             final InputStream inputStream, final File outputDir, final String contentType) {
246         if (!outputDir.exists() && !outputDir.mkdirs()) {
247             LogUtil.e(LogUtil.BUGLE_TAG, "Error creating " + outputDir.getAbsolutePath());
248             return null;
249         }
250 
251         final Context context = Factory.get().getApplicationContext();
252         try {
253             final Uri targetUri = Uri.fromFile(FileUtil.getNewFile(outputDir, contentType));
254             return copyContent(context, inputStream, targetUri);
255         } catch (final IOException e) {
256             LogUtil.e(LogUtil.BUGLE_TAG, "Error creating file in " + outputDir.getAbsolutePath());
257             return null;
258         }
259     }
260 
261     /**
262      * Persist a piece of content from the given sourceUri, byte by byte to the
263      * specified output directory.
264      * @return the output Uri if the operation succeeded, or null if failed.
265      */
266     @DoesNotRunOnMainThread
persistContent( final Uri sourceUri, final File outputDir, final String contentType)267     public static Uri persistContent(
268             final Uri sourceUri, final File outputDir, final String contentType) {
269         InputStream inputStream = null;
270         final Context context = Factory.get().getApplicationContext();
271         try {
272             if (UriUtil.isLocalResourceUri(sourceUri)) {
273                 inputStream = context.getContentResolver().openInputStream(sourceUri);
274             } else {
275                 // The content is remote. Download it.
276                 final URL url = new URL(sourceUri.toString());
277                 final URLConnection ucon = url.openConnection();
278                 inputStream = new BufferedInputStream(ucon.getInputStream());
279             }
280             return persistContent(inputStream, outputDir, contentType);
281         } catch (final Exception ex) {
282             LogUtil.e(LogUtil.BUGLE_TAG, "Error while retrieving media ", ex);
283             return null;
284         } finally {
285             if (inputStream != null) {
286                 try {
287                     inputStream.close();
288                 } catch (final IOException e) {
289                     LogUtil.e(LogUtil.BUGLE_TAG, "error trying to close the inputStream", e);
290                 }
291             }
292         }
293     }
294 
295     /** @return uri of target file, or null on error */
296     @DoesNotRunOnMainThread
copyContent( final Context context, final InputStream inputStream, final Uri targetUri)297     private static Uri copyContent(
298             final Context context, final InputStream inputStream, final Uri targetUri) {
299         Assert.isNotMainThread();
300         OutputStream outputStream = null;
301         try {
302             outputStream = context.getContentResolver().openOutputStream(targetUri);
303             ByteStreams.copy(inputStream, outputStream);
304         } catch (final Exception ex) {
305             LogUtil.e(LogUtil.BUGLE_TAG, "Error while copying content ", ex);
306             return null;
307         } finally {
308             if (outputStream != null) {
309                 try {
310                     outputStream.flush();
311                 } catch (final IOException e) {
312                     LogUtil.e(LogUtil.BUGLE_TAG, "error trying to flush the outputStream", e);
313                     return null;
314                 } finally {
315                     try {
316                         outputStream.close();
317                     } catch (final IOException e) {
318                         // Do nothing.
319                     }
320                 }
321             }
322         }
323         return targetUri;
324     }
325 
isSmsMmsUri(final Uri uri)326     public static boolean isSmsMmsUri(final Uri uri) {
327         return uri != null && SMS_MMS_SCHEMES.contains(uri.getScheme());
328     }
329 
330     /**
331      * Extract recipient destinations from Uri of form SCHEME:destination[,destination]?otherstuff
332      * where SCHEME is one of the supported sms/mms schemes.
333      *
334      * @param uri sms/mms uri
335      * @return a comma-separated list of recipient destinations or null.
336      */
parseRecipientsFromSmsMmsUri(final Uri uri)337     public static String parseRecipientsFromSmsMmsUri(final Uri uri) {
338         if (!isSmsMmsUri(uri)) {
339             return null;
340         }
341         final String[] parts = uri.getSchemeSpecificPart().split("\\?");
342         if (TextUtils.isEmpty(parts[0])) {
343             return null;
344         }
345         // replaceUnicodeDigits will replace digits typed in other languages (i.e. Egyptian) with
346         // the usual ascii equivalents.
347         return TextUtil.replaceUnicodeDigits(parts[0]).replace(';', ',');
348     }
349 
350     /**
351      * Return the length of the file to which contentUri refers
352      *
353      * @param contentUri URI for the file of which we want the length
354      * @return Length of the file or AssetFileDescriptor.UNKNOWN_LENGTH
355      */
getUriContentLength(final Uri contentUri)356     public static long getUriContentLength(final Uri contentUri) {
357         final Context context = Factory.get().getApplicationContext();
358         AssetFileDescriptor afd = null;
359         try {
360             afd = context.getContentResolver().openAssetFileDescriptor(contentUri, "r");
361             return afd.getLength();
362         } catch (final FileNotFoundException e) {
363             LogUtil.w(LogUtil.BUGLE_TAG, "Failed to query length of " + contentUri);
364         } finally {
365             if (afd != null) {
366                 try {
367                     afd.close();
368                 } catch (final IOException e) {
369                     LogUtil.w(LogUtil.BUGLE_TAG, "Failed to close afd for " + contentUri);
370                 }
371             }
372         }
373         return AssetFileDescriptor.UNKNOWN_LENGTH;
374     }
375 
376     /** @return string representation of URI or null if URI was null */
stringFromUri(final Uri uri)377     public static String stringFromUri(final Uri uri) {
378         return uri == null ? null : uri.toString();
379     }
380 
381     /** @return URI created from string or null if string was null or empty */
uriFromString(final String uriString)382     public static Uri uriFromString(final String uriString) {
383         return TextUtils.isEmpty(uriString) ? null : Uri.parse(uriString);
384      }
385 }
386