| /* |
| * 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 androidx.annotation.NonNull; |
| import android.text.TextUtils; |
| |
| import com.android.messaging.Factory; |
| import com.android.messaging.datamodel.GalleryBoundCursorLoader; |
| 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<String> SMS_MMS_SCHEMES = new HashSet<String>( |
| Arrays.asList(SCHEME_SMS, SCHEME_MMS, SCHEME_SMSTO, SCHEME_MMSTO)); |
| private static final String SCHEME_HTTP = "http"; |
| private static final String SCHEME_HTTPS = "https"; |
| |
| public static final String SCHEME_BUGLE = "bugle"; |
| public static final HashSet<String> SUPPORTED_SCHEME = new HashSet<String>( |
| 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); |
| } |
| |
| /** Returns whether the given Uri is a file. */ |
| public static boolean isFileUri(final Uri uri) { |
| return uri != null && |
| uri.getScheme() != null && |
| uri.getScheme().trim().toLowerCase().contains(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 content:// style URI for the given MediaStore row Id in the files table on the |
| * external volume. |
| * |
| * @param id the MediaStore row Id to get the URI for |
| * @return the URI to the files table on the external storage. |
| */ |
| public static Uri getContentUriForMediaStoreId(final long id) { |
| return MediaStore.Files.getContentUri( |
| GalleryBoundCursorLoader.MEDIA_SCANNER_VOLUME_EXTERNAL, id); |
| } |
| |
| /** |
| * 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. |
| inputStream = getInputStreamFromRemoteUri(sourceUri); |
| if (inputStream == null) { |
| return null; |
| } |
| } |
| 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); |
| } |
| } |
| } |
| } |
| |
| @DoesNotRunOnMainThread |
| private static InputStream getInputStreamFromRemoteUri(final Uri sourceUri) |
| throws IOException { |
| if (isRemoteUri(sourceUri)) { |
| final URL url = new URL(sourceUri.toString()); |
| final URLConnection ucon = url.openConnection(); |
| return new BufferedInputStream(ucon.getInputStream()); |
| } else { |
| return null; |
| } |
| } |
| |
| private static boolean isRemoteUri(final Uri sourceUri) { |
| return sourceUri.getScheme().equals(SCHEME_HTTP) |
| || sourceUri.getScheme().equals(SCHEME_HTTPS); |
| } |
| |
| /** |
| * 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. |
| inputStream = getInputStreamFromRemoteUri(sourceUri); |
| if (inputStream == null) { |
| return null; |
| } |
| } |
| 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:destination[,destination]?otherstuff |
| * where SCHEME is one of the supported sms/mms schemes. |
| * |
| * @param uri sms/mms uri |
| * @return a comma-separated list of 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(';', ','); |
| } |
| |
| /** |
| * 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); |
| } |
| } |