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