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