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 * @param context Application context. 42 * @param file The file to be translated. 43 */ 44 Uri getContentUriFromFile(Context context, File file); 45 } 46 47 // Prevent instantiation. 48 private ContentUriUtils() {} 49 50 public static void setFileProviderUtil(FileProviderUtil util) { 51 synchronized (sLock) { 52 sFileProviderUtil = util; 53 } 54 } 55 56 public static Uri getContentUriFromFile(Context context, File file) { 57 synchronized (sLock) { 58 if (sFileProviderUtil != null) { 59 return sFileProviderUtil.getContentUriFromFile(context, 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 desciptor. 68 * 69 * @param context {@link Context} in interest 70 * @param uriString the content URI to open 71 * @return file desciptor upon success, or -1 otherwise. 72 */ 73 @CalledByNative 74 public static int openContentUriForRead(Context context, String uriString) { 75 AssetFileDescriptor afd = getAssetFileDescriptor(context, uriString); 76 if (afd != null) { 77 return afd.getParcelFileDescriptor().detachFd(); 78 } 79 return -1; 80 } 81 82 /** 83 * Check whether a content URI exists. 84 * 85 * @param context {@link Context} in interest. 86 * @param uriString the content URI to query. 87 * @return true if the URI exists, or false otherwise. 88 */ 89 @CalledByNative 90 public static boolean contentUriExists(Context context, String uriString) { 91 AssetFileDescriptor asf = null; 92 try { 93 asf = getAssetFileDescriptor(context, uriString); 94 return asf != null; 95 } finally { 96 // Do not use StreamUtil.closeQuietly here, as AssetFileDescriptor 97 // does not implement Closeable until KitKat. 98 if (asf != null) { 99 try { 100 asf.close(); 101 } catch (IOException e) { 102 // Closing quietly. 103 } 104 } 105 } 106 } 107 108 /** 109 * Retrieve the MIME type for the content URI. 110 * 111 * @param context {@link Context} in interest. 112 * @param uriString the content URI to look up. 113 * @return MIME type or null if the input params are empty or invalid. 114 */ 115 @CalledByNative 116 public static String getMimeType(Context context, String uriString) { 117 ContentResolver resolver = context.getContentResolver(); 118 Uri uri = Uri.parse(uriString); 119 if (isVirtualDocument(uri, context)) { 120 String[] streamTypes = resolver.getStreamTypes(uri, "*/*"); 121 return (streamTypes != null && streamTypes.length > 0) ? streamTypes[0] : null; 122 } 123 return resolver.getType(uri); 124 } 125 126 /** 127 * Helper method to open a content URI and returns the ParcelFileDescriptor. 128 * 129 * @param context {@link Context} in interest. 130 * @param uriString the content URI to open. 131 * @return AssetFileDescriptor of the content URI, or NULL if the file does not exist. 132 */ 133 private static AssetFileDescriptor getAssetFileDescriptor(Context context, String uriString) { 134 ContentResolver resolver = context.getContentResolver(); 135 Uri uri = Uri.parse(uriString); 136 137 try { 138 if (isVirtualDocument(uri, context)) { 139 String[] streamTypes = resolver.getStreamTypes(uri, "*/*"); 140 if (streamTypes != null && streamTypes.length > 0) { 141 AssetFileDescriptor afd = 142 resolver.openTypedAssetFileDescriptor(uri, streamTypes[0], null); 143 if (afd.getStartOffset() != 0) { 144 // Do not use StreamUtil.closeQuietly here, as AssetFileDescriptor 145 // does not implement Closeable until KitKat. 146 try { 147 afd.close(); 148 } catch (IOException e) { 149 // Closing quietly. 150 } 151 throw new SecurityException("Cannot open files with non-zero offset type."); 152 } 153 return afd; 154 } 155 } else { 156 ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r"); 157 if (pfd != null) { 158 return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH); 159 } 160 } 161 } catch (FileNotFoundException e) { 162 Log.w(TAG, "Cannot find content uri: " + uriString, e); 163 } catch (SecurityException e) { 164 Log.w(TAG, "Cannot open content uri: " + uriString, e); 165 } catch (IllegalArgumentException e) { 166 Log.w(TAG, "Unknown content uri: " + uriString, e); 167 } catch (IllegalStateException e) { 168 Log.w(TAG, "Unknown content uri: " + uriString, e); 169 } 170 171 return null; 172 } 173 174 /** 175 * Method to resolve the display name of a content URI. 176 * 177 * @param uri the content URI to be resolved. 178 * @param context {@link Context} in interest. 179 * @param columnField the column field to query. 180 * @return the display name of the @code uri if present in the database 181 * or an empty string otherwise. 182 */ 183 public static String getDisplayName(Uri uri, Context context, String columnField) { 184 if (uri == null) return ""; 185 ContentResolver contentResolver = context.getContentResolver(); 186 Cursor cursor = null; 187 try { 188 cursor = contentResolver.query(uri, null, null, null, null); 189 190 if (cursor != null && cursor.getCount() >= 1) { 191 cursor.moveToFirst(); 192 int displayNameIndex = cursor.getColumnIndex(columnField); 193 if (displayNameIndex == -1) { 194 return ""; 195 } 196 String displayName = cursor.getString(displayNameIndex); 197 // For Virtual documents, try to modify the file extension so it's compatible 198 // with the alternative MIME type. 199 if (hasVirtualFlag(cursor)) { 200 String[] mimeTypes = contentResolver.getStreamTypes(uri, "*/*"); 201 if (mimeTypes != null && mimeTypes.length > 0) { 202 String ext = 203 MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeTypes[0]); 204 if (ext != null) { 205 // Just append, it's simpler and more secure than altering an 206 // existing extension. 207 displayName += "." + ext; 208 } 209 } 210 } 211 return displayName; 212 } 213 } catch (NullPointerException e) { 214 // Some android models don't handle the provider call correctly. 215 // see crbug.com/345393 216 return ""; 217 } finally { 218 StreamUtil.closeQuietly(cursor); 219 } 220 return ""; 221 } 222 223 /** 224 * Checks whether the passed Uri represents a virtual document. 225 * 226 * @param uri the content URI to be resolved. 227 * @param contentResolver the content resolver to query. 228 * @return True for virtual file, false for any other file. 229 */ 230 private static boolean isVirtualDocument(Uri uri, Context context) { 231 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return false; 232 if (uri == null) return false; 233 if (!DocumentsContract.isDocumentUri(context, uri)) return false; 234 ContentResolver contentResolver = context.getContentResolver(); 235 Cursor cursor = null; 236 try { 237 cursor = contentResolver.query(uri, null, null, null, null); 238 239 if (cursor != null && cursor.getCount() >= 1) { 240 cursor.moveToFirst(); 241 return hasVirtualFlag(cursor); 242 } 243 } catch (NullPointerException e) { 244 // Some android models don't handle the provider call correctly. 245 // see crbug.com/345393 246 return false; 247 } finally { 248 StreamUtil.closeQuietly(cursor); 249 } 250 return false; 251 } 252 253 /** 254 * Checks whether the passed cursor for a document has a virtual document flag. 255 * 256 * The called must close the passed cursor. 257 * 258 * @param cursor Cursor with COLUMN_FLAGS. 259 * @return True for virtual file, false for any other file. 260 */ 261 private static boolean hasVirtualFlag(Cursor cursor) { 262 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return false; 263 int index = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS); 264 if (index > -1) { 265 return (cursor.getLong(index) & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) != 0; 266 } 267 return false; 268 } 269 } 270