1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base; 6 7 import android.content.ContentResolver; 8 import android.content.Context; 9 import android.content.res.AssetFileDescriptor; 10 import android.database.Cursor; 11 import android.net.Uri; 12 import android.os.Build; 13 import android.os.ParcelFileDescriptor; 14 import android.provider.DocumentsContract; 15 import android.util.Log; 16 import android.webkit.MimeTypeMap; 17 18 import org.chromium.base.annotations.CalledByNative; 19 20 import java.io.File; 21 import java.io.FileNotFoundException; 22 import java.io.IOException; 23 24 /** 25 * This class provides methods to access content URI schemes. 26 */ 27 public abstract class ContentUriUtils { 28 private static final String TAG = "ContentUriUtils"; 29 private static FileProviderUtil sFileProviderUtil; 30 31 // Guards access to sFileProviderUtil. 32 private static final Object sLock = new Object(); 33 34 /** 35 * Provides functionality to translate a file into a content URI for use 36 * with a content provider. 37 */ 38 public interface FileProviderUtil { 39 /** 40 * Generate a content URI from the given file. 41 * 42 * @param file The file to be translated. 43 */ getContentUriFromFile(File file)44 Uri getContentUriFromFile(File file); 45 } 46 47 // Prevent instantiation. ContentUriUtils()48 private ContentUriUtils() {} 49 setFileProviderUtil(FileProviderUtil util)50 public static void setFileProviderUtil(FileProviderUtil util) { 51 synchronized (sLock) { 52 sFileProviderUtil = util; 53 } 54 } 55 getContentUriFromFile(File file)56 public static Uri getContentUriFromFile(File file) { 57 synchronized (sLock) { 58 if (sFileProviderUtil != null) { 59 return sFileProviderUtil.getContentUriFromFile(file); 60 } 61 } 62 return null; 63 } 64 65 /** 66 * Opens the content URI for reading, and returns the file descriptor to 67 * the caller. The caller is responsible for closing the file descriptor. 68 * 69 * @param uriString the content URI to open 70 * @return file descriptor upon success, or -1 otherwise. 71 */ 72 @CalledByNative openContentUriForRead(String uriString)73 public static int openContentUriForRead(String uriString) { 74 AssetFileDescriptor afd = getAssetFileDescriptor(uriString); 75 if (afd != null) { 76 return afd.getParcelFileDescriptor().detachFd(); 77 } 78 return -1; 79 } 80 81 /** 82 * Check whether a content URI exists. 83 * 84 * @param uriString the content URI to query. 85 * @return true if the URI exists, or false otherwise. 86 */ 87 @CalledByNative contentUriExists(String uriString)88 public static boolean contentUriExists(String uriString) { 89 AssetFileDescriptor asf = null; 90 try { 91 asf = getAssetFileDescriptor(uriString); 92 return asf != null; 93 } finally { 94 // Do not use StreamUtil.closeQuietly here, as AssetFileDescriptor 95 // does not implement Closeable until KitKat. 96 if (asf != null) { 97 try { 98 asf.close(); 99 } catch (IOException e) { 100 // Closing quietly. 101 } 102 } 103 } 104 } 105 106 /** 107 * Retrieve the MIME type for the content URI. 108 * 109 * @param uriString the content URI to look up. 110 * @return MIME type or null if the input params are empty or invalid. 111 */ 112 @CalledByNative getMimeType(String uriString)113 public static String getMimeType(String uriString) { 114 ContentResolver resolver = ContextUtils.getApplicationContext().getContentResolver(); 115 Uri uri = Uri.parse(uriString); 116 if (isVirtualDocument(uri)) { 117 String[] streamTypes = resolver.getStreamTypes(uri, "*/*"); 118 return (streamTypes != null && streamTypes.length > 0) ? streamTypes[0] : null; 119 } 120 return resolver.getType(uri); 121 } 122 123 /** 124 * Helper method to open a content URI and returns the ParcelFileDescriptor. 125 * 126 * @param uriString the content URI to open. 127 * @return AssetFileDescriptor of the content URI, or NULL if the file does not exist. 128 */ getAssetFileDescriptor(String uriString)129 private static AssetFileDescriptor getAssetFileDescriptor(String uriString) { 130 ContentResolver resolver = ContextUtils.getApplicationContext().getContentResolver(); 131 Uri uri = Uri.parse(uriString); 132 133 try { 134 if (isVirtualDocument(uri)) { 135 String[] streamTypes = resolver.getStreamTypes(uri, "*/*"); 136 if (streamTypes != null && streamTypes.length > 0) { 137 AssetFileDescriptor afd = 138 resolver.openTypedAssetFileDescriptor(uri, streamTypes[0], null); 139 if (afd != null && afd.getStartOffset() != 0) { 140 // Do not use StreamUtil.closeQuietly here, as AssetFileDescriptor 141 // does not implement Closeable until KitKat. 142 try { 143 afd.close(); 144 } catch (IOException e) { 145 // Closing quietly. 146 } 147 throw new SecurityException("Cannot open files with non-zero offset type."); 148 } 149 return afd; 150 } 151 } else { 152 ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r"); 153 if (pfd != null) { 154 return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH); 155 } 156 } 157 } catch (FileNotFoundException e) { 158 Log.w(TAG, "Cannot find content uri: " + uriString, e); 159 } catch (SecurityException e) { 160 Log.w(TAG, "Cannot open content uri: " + uriString, e); 161 } catch (Exception e) { 162 Log.w(TAG, "Unknown content uri: " + uriString, e); 163 } 164 return null; 165 } 166 167 /** 168 * Method to resolve the display name of a content URI. 169 * 170 * @param uri the content URI to be resolved. 171 * @param context {@link Context} in interest. 172 * @param columnField the column field to query. 173 * @return the display name of the @code uri if present in the database 174 * or an empty string otherwise. 175 */ getDisplayName(Uri uri, Context context, String columnField)176 public static String getDisplayName(Uri uri, Context context, String columnField) { 177 if (uri == null) return ""; 178 ContentResolver contentResolver = context.getContentResolver(); 179 try (Cursor cursor = contentResolver.query(uri, null, null, null, null)) { 180 if (cursor != null && cursor.getCount() >= 1) { 181 cursor.moveToFirst(); 182 int displayNameIndex = cursor.getColumnIndex(columnField); 183 if (displayNameIndex == -1) { 184 return ""; 185 } 186 String displayName = cursor.getString(displayNameIndex); 187 // For Virtual documents, try to modify the file extension so it's compatible 188 // with the alternative MIME type. 189 if (hasVirtualFlag(cursor)) { 190 String[] mimeTypes = contentResolver.getStreamTypes(uri, "*/*"); 191 if (mimeTypes != null && mimeTypes.length > 0) { 192 String ext = 193 MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeTypes[0]); 194 if (ext != null) { 195 // Just append, it's simpler and more secure than altering an 196 // existing extension. 197 displayName += "." + ext; 198 } 199 } 200 } 201 return displayName; 202 } 203 } catch (NullPointerException e) { 204 // Some android models don't handle the provider call correctly. 205 // see crbug.com/345393 206 return ""; 207 } 208 return ""; 209 } 210 211 /** 212 * Checks whether the passed Uri represents a virtual document. 213 * 214 * @param uri the content URI to be resolved. 215 * @return True for virtual file, false for any other file. 216 */ isVirtualDocument(Uri uri)217 private static boolean isVirtualDocument(Uri uri) { 218 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return false; 219 if (uri == null) return false; 220 if (!DocumentsContract.isDocumentUri(ContextUtils.getApplicationContext(), uri)) { 221 return false; 222 } 223 ContentResolver contentResolver = ContextUtils.getApplicationContext().getContentResolver(); 224 try (Cursor cursor = contentResolver.query(uri, null, null, null, null)) { 225 if (cursor != null && cursor.getCount() >= 1) { 226 cursor.moveToFirst(); 227 return hasVirtualFlag(cursor); 228 } 229 } catch (NullPointerException e) { 230 // Some android models don't handle the provider call correctly. 231 // see crbug.com/345393 232 return false; 233 } 234 return false; 235 } 236 237 /** 238 * Checks whether the passed cursor for a document has a virtual document flag. 239 * 240 * The called must close the passed cursor. 241 * 242 * @param cursor Cursor with COLUMN_FLAGS. 243 * @return True for virtual file, false for any other file. 244 */ hasVirtualFlag(Cursor cursor)245 private static boolean hasVirtualFlag(Cursor cursor) { 246 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return false; 247 int index = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS); 248 return index > -1 249 && (cursor.getLong(index) & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) != 0; 250 } 251 } 252