/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.messaging.util; import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.provider.MediaStore; import android.support.annotation.NonNull; import android.text.TextUtils; import com.android.messaging.Factory; import com.android.messaging.datamodel.MediaScratchFileProvider; import com.android.messaging.util.Assert.DoesNotRunOnMainThread; import com.google.common.io.ByteStreams; import java.io.BufferedInputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; import java.net.URLConnection; import java.util.Arrays; import java.util.HashSet; public class UriUtil { private static final String SCHEME_SMS = "sms"; private static final String SCHEME_SMSTO = "smsto"; private static final String SCHEME_MMS = "mms"; private static final String SCHEME_MMSTO = "smsto"; public static final HashSet SMS_MMS_SCHEMES = new HashSet( Arrays.asList(SCHEME_SMS, SCHEME_MMS, SCHEME_SMSTO, SCHEME_MMSTO)); public static final String SCHEME_BUGLE = "bugle"; public static final HashSet SUPPORTED_SCHEME = new HashSet( Arrays.asList(ContentResolver.SCHEME_ANDROID_RESOURCE, ContentResolver.SCHEME_CONTENT, ContentResolver.SCHEME_FILE, SCHEME_BUGLE)); public static final String SCHEME_TEL = "tel:"; /** * Get a Uri representation of the file path of a resource file. */ public static Uri getUriForResourceFile(final String path) { return TextUtils.isEmpty(path) ? null : Uri.fromFile(new File(path)); } /** * Extract the path from a file:// Uri, or null if the uri is of other scheme. */ public static String getFilePathFromUri(final Uri uri) { if (!isFileUri(uri)) { return null; } return uri.getPath(); } /** * Returns whether the given Uri is local or remote. */ public static boolean isLocalResourceUri(final Uri uri) { final String scheme = uri.getScheme(); return TextUtils.equals(scheme, ContentResolver.SCHEME_ANDROID_RESOURCE) || TextUtils.equals(scheme, ContentResolver.SCHEME_CONTENT) || TextUtils.equals(scheme, ContentResolver.SCHEME_FILE); } /** * Returns whether the given Uri is part of Bugle's app package */ public static boolean isBugleAppResource(final Uri uri) { final String scheme = uri.getScheme(); return TextUtils.equals(scheme, ContentResolver.SCHEME_ANDROID_RESOURCE); } public static boolean isFileUri(final Uri uri) { return uri != null && TextUtils.equals(uri.getScheme(), ContentResolver.SCHEME_FILE); } /** * Constructs an android.resource:// uri for the given resource id. */ public static Uri getUriForResourceId(final Context context, final int resId) { return new Uri.Builder() .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) .authority(context.getPackageName()) .appendPath(String.valueOf(resId)) .build(); } /** * Returns whether the given Uri string is local. */ public static boolean isLocalUri(@NonNull final Uri uri) { Assert.notNull(uri); return SUPPORTED_SCHEME.contains(uri.getScheme()); } private static final String MEDIA_STORE_URI_KLP = "com.android.providers.media.documents"; /** * Check if a URI is from the MediaStore */ public static boolean isMediaStoreUri(final Uri uri) { final String uriAuthority = uri.getAuthority(); return TextUtils.equals(ContentResolver.SCHEME_CONTENT, uri.getScheme()) && (TextUtils.equals(MediaStore.AUTHORITY, uriAuthority) || // KK changed the media store authority name TextUtils.equals(MEDIA_STORE_URI_KLP, uriAuthority)); } /** * Gets the size in bytes for the content uri. Currently we only support content in the * scratch space. */ @DoesNotRunOnMainThread public static long getContentSize(final Uri uri) { Assert.isNotMainThread(); if (isLocalResourceUri(uri)) { ParcelFileDescriptor pfd = null; try { pfd = Factory.get().getApplicationContext() .getContentResolver().openFileDescriptor(uri, "r"); return Math.max(pfd.getStatSize(), 0); } catch (final FileNotFoundException e) { LogUtil.e(LogUtil.BUGLE_TAG, "Error getting content size", e); } finally { if (pfd != null) { try { pfd.close(); } catch (final IOException e) { // Do nothing. } } } } else { Assert.fail("Unsupported uri type!"); } return 0; } /** @return duration in milliseconds or 0 if not able to determine */ public static int getMediaDurationMs(final Uri uri) { final MediaMetadataRetrieverWrapper retriever = new MediaMetadataRetrieverWrapper(); try { retriever.setDataSource(uri); return retriever.extractInteger(MediaMetadataRetriever.METADATA_KEY_DURATION, 0); } catch (final IOException e) { LogUtil.e(LogUtil.BUGLE_TAG, "Unable extract duration from media file: " + uri, e); return 0; } finally { retriever.release(); } } /** * Persist a piece of content from the given input stream, byte by byte to the scratch * directory. * @return the output Uri if the operation succeeded, or null if failed. */ @DoesNotRunOnMainThread public static Uri persistContentToScratchSpace(final InputStream inputStream) { final Context context = Factory.get().getApplicationContext(); final Uri scratchSpaceUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(null); return copyContent(context, inputStream, scratchSpaceUri); } /** * Persist a piece of content from the given sourceUri, byte by byte to the scratch * directory. * @return the output Uri if the operation succeeded, or null if failed. */ @DoesNotRunOnMainThread public static Uri persistContentToScratchSpace(final Uri sourceUri) { InputStream inputStream = null; final Context context = Factory.get().getApplicationContext(); try { if (UriUtil.isLocalResourceUri(sourceUri)) { inputStream = context.getContentResolver().openInputStream(sourceUri); } else { // The content is remote. Download it. final URL url = new URL(sourceUri.toString()); final URLConnection ucon = url.openConnection(); inputStream = new BufferedInputStream(ucon.getInputStream()); } return persistContentToScratchSpace(inputStream); } catch (final Exception ex) { LogUtil.e(LogUtil.BUGLE_TAG, "Error while retrieving media ", ex); return null; } finally { if (inputStream != null) { try { inputStream.close(); } catch (final IOException e) { LogUtil.e(LogUtil.BUGLE_TAG, "error trying to close the inputStream", e); } } } } /** * Persist a piece of content from the given input stream, byte by byte to the specified * directory. * @return the output Uri if the operation succeeded, or null if failed. */ @DoesNotRunOnMainThread public static Uri persistContent( final InputStream inputStream, final File outputDir, final String contentType) { if (!outputDir.exists() && !outputDir.mkdirs()) { LogUtil.e(LogUtil.BUGLE_TAG, "Error creating " + outputDir.getAbsolutePath()); return null; } final Context context = Factory.get().getApplicationContext(); try { final Uri targetUri = Uri.fromFile(FileUtil.getNewFile(outputDir, contentType)); return copyContent(context, inputStream, targetUri); } catch (final IOException e) { LogUtil.e(LogUtil.BUGLE_TAG, "Error creating file in " + outputDir.getAbsolutePath()); return null; } } /** * Persist a piece of content from the given sourceUri, byte by byte to the * specified output directory. * @return the output Uri if the operation succeeded, or null if failed. */ @DoesNotRunOnMainThread public static Uri persistContent( final Uri sourceUri, final File outputDir, final String contentType) { InputStream inputStream = null; final Context context = Factory.get().getApplicationContext(); try { if (UriUtil.isLocalResourceUri(sourceUri)) { inputStream = context.getContentResolver().openInputStream(sourceUri); } else { // The content is remote. Download it. final URL url = new URL(sourceUri.toString()); final URLConnection ucon = url.openConnection(); inputStream = new BufferedInputStream(ucon.getInputStream()); } return persistContent(inputStream, outputDir, contentType); } catch (final Exception ex) { LogUtil.e(LogUtil.BUGLE_TAG, "Error while retrieving media ", ex); return null; } finally { if (inputStream != null) { try { inputStream.close(); } catch (final IOException e) { LogUtil.e(LogUtil.BUGLE_TAG, "error trying to close the inputStream", e); } } } } /** @return uri of target file, or null on error */ @DoesNotRunOnMainThread private static Uri copyContent( final Context context, final InputStream inputStream, final Uri targetUri) { Assert.isNotMainThread(); OutputStream outputStream = null; try { outputStream = context.getContentResolver().openOutputStream(targetUri); ByteStreams.copy(inputStream, outputStream); } catch (final Exception ex) { LogUtil.e(LogUtil.BUGLE_TAG, "Error while copying content ", ex); return null; } finally { if (outputStream != null) { try { outputStream.flush(); } catch (final IOException e) { LogUtil.e(LogUtil.BUGLE_TAG, "error trying to flush the outputStream", e); return null; } finally { try { outputStream.close(); } catch (final IOException e) { // Do nothing. } } } } return targetUri; } public static boolean isSmsMmsUri(final Uri uri) { return uri != null && SMS_MMS_SCHEMES.contains(uri.getScheme()); } /** * Extract recipient destinations from Uri of form * SCHEME:destionation[,destination]?otherstuff * where SCHEME is one of the supported sms/mms schemes. * * @param uri sms/mms uri * @return recipient destinations or null */ public static String[] parseRecipientsFromSmsMmsUri(final Uri uri) { if (!isSmsMmsUri(uri)) { return null; } final String[] parts = uri.getSchemeSpecificPart().split("\\?"); if (TextUtils.isEmpty(parts[0])) { return null; } // replaceUnicodeDigits will replace digits typed in other languages (i.e. Egyptian) with // the usual ascii equivalents. return TextUtil.replaceUnicodeDigits(parts[0]).replace(';', ',').split(","); } /** * Return the length of the file to which contentUri refers * * @param contentUri URI for the file of which we want the length * @return Length of the file or AssetFileDescriptor.UNKNOWN_LENGTH */ public static long getUriContentLength(final Uri contentUri) { final Context context = Factory.get().getApplicationContext(); AssetFileDescriptor afd = null; try { afd = context.getContentResolver().openAssetFileDescriptor(contentUri, "r"); return afd.getLength(); } catch (final FileNotFoundException e) { LogUtil.w(LogUtil.BUGLE_TAG, "Failed to query length of " + contentUri); } finally { if (afd != null) { try { afd.close(); } catch (final IOException e) { LogUtil.w(LogUtil.BUGLE_TAG, "Failed to close afd for " + contentUri); } } } return AssetFileDescriptor.UNKNOWN_LENGTH; } /** @return string representation of URI or null if URI was null */ public static String stringFromUri(final Uri uri) { return uri == null ? null : uri.toString(); } /** @return URI created from string or null if string was null or empty */ public static Uri uriFromString(final String uriString) { return TextUtils.isEmpty(uriString) ? null : Uri.parse(uriString); } }