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