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