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