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