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