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 */
16package com.android.messaging.util;
17
18import android.content.ContentResolver;
19import android.content.Context;
20import android.content.res.AssetFileDescriptor;
21import android.media.MediaMetadataRetriever;
22import android.net.Uri;
23import android.os.ParcelFileDescriptor;
24import android.provider.MediaStore;
25import android.support.annotation.NonNull;
26import android.text.TextUtils;
27
28import com.android.messaging.Factory;
29import com.android.messaging.datamodel.MediaScratchFileProvider;
30import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
31import com.google.common.io.ByteStreams;
32
33import java.io.BufferedInputStream;
34import java.io.File;
35import java.io.FileNotFoundException;
36import java.io.IOException;
37import java.io.InputStream;
38import java.io.OutputStream;
39import java.net.URL;
40import java.net.URLConnection;
41import java.util.Arrays;
42import java.util.HashSet;
43
44public 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     */
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     */
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     */
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     */
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
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     */
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     */
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     */
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
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 */
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
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
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
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
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
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
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     */
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     */
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 */
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 */
366    public static Uri uriFromString(final String uriString) {
367        return TextUtils.isEmpty(uriString) ? null : Uri.parse(uriString);
368     }
369}
370