Programming
Get real path from URI Android KitKat new storage access framework duplicate
Navigating the intricacies of file access in Android can often feel like traversing a maze, especially when dealing with the introduction of the Storage Access Framework (SAF) in Android KitKat (API level 19). One common hurdle developers face is how to get real path from URI, particularly when a URI represents a document provided by SAF rather than a traditional file on the filesystem. Understanding the nuances of URIs, content resolvers, and the different storage locations is crucial for building robust and user-friendly Android applications. This article provides a deep dive into this specific challenge, offering practical solutions, best practices, and valuable insights to help you effectively manage file access and retrieval in your Android projects, focusing on the complexities introduced by the Storage Access Framework. We’ll explore how to handle URIs returned by the SAF, ensure compatibility across different Android versions, and avoid common pitfalls that can lead to frustrating debugging sessions.
Understanding the Storage Access Framework (SAF)
The Storage Access Framework, introduced in Android 4.4 (KitKat), revolutionized how applications access files. It provides a centralized and consistent way for users to grant applications access to files stored on various storage providers, such as local storage, cloud storage services (like Google Drive or Dropbox), and removable storage (like SD cards). Before SAF, apps often relied on direct file path access, which led to security concerns and inconsistencies across different devices and storage configurations. SAF addresses these issues by providing a layer of abstraction between applications and the underlying storage, promoting better security and a more unified user experience. Instead of directly accessing file paths, apps interact with URIs that represent files or documents.
SAF introduces concepts like DocumentsProvider, which allows storage services to expose their files to other applications via a standardized interface. When a user selects a file through the system’s file picker, your application receives a URI representing the selected document. The challenge then becomes: how do you get real path from URI, especially when the URI doesn’t directly correspond to a traditional file path? This is where the intricacies of ContentResolvers and document metadata come into play. Understanding the nuances of SAF is critical for developing Android applications that interact reliably with files across a wide range of devices and storage providers.
Working with SAF also means adapting to different URI schemes and handling potential exceptions. For example, a URI might point to a file on external storage, a document in a cloud service, or even a virtual file that doesn’t have a direct physical representation. The key is to use the ContentResolver to query the DocumentsProvider associated with the URI and extract the necessary information to access the underlying data. Failure to properly handle these nuances can lead to unexpected errors and a poor user experience. Proper error handling and fallback mechanisms are essential for ensuring your application remains robust and user-friendly, regardless of the underlying storage provider.
The Problem: Converting URI to File Path
The core problem lies in the fact that the URI provided by the Storage Access Framework doesn’t always directly translate to a traditional file path, especially for files accessed through cloud storage providers or virtual file systems. In older versions of Android, you could often directly extract the file path from a URI using simple string manipulation or by querying the MediaStore. However, with the introduction of SAF, this approach became unreliable and often resulted in null paths or incorrect file locations. This is because SAF uses a different URI scheme, such as content://, which requires a different approach to retrieve the underlying file data.
The content:// URI scheme indicates that the data is managed by a ContentProvider, not a direct file path. To get real path from URI in such cases, you need to use a ContentResolver to query the ContentProvider associated with the URI. This involves querying the provider for the file’s metadata, such as its display name, size, and potentially its underlying file path (if available). However, even when a file path is returned, it might not be directly accessible to your application due to permission restrictions or the nature of the storage provider. For instance, a cloud storage provider might return a temporary or virtual file path that is only valid for a limited time or within the context of the provider’s application.
Furthermore, the way file paths are handled can vary significantly across different Android versions and devices. Some devices might expose a direct file path, while others might only provide a stream of data. To ensure compatibility and reliability, it’s crucial to implement a robust solution that can handle different URI schemes and storage providers gracefully. This often involves checking the URI scheme, querying the ContentProvider for metadata, and using input streams to access the file data directly, rather than relying solely on file paths. Utilizing libraries or helper classes that abstract away the complexities of URI handling can also significantly simplify the process and reduce the risk of errors.
Solutions for Retrieving File Information from a URI
Several approaches can be used to extract file information from a URI obtained through the Storage Access Framework. These methods leverage the ContentResolver and the DocumentsContract to query the content provider and retrieve the necessary metadata. Here’s a breakdown of common solutions:
1. Using ContentResolver and DocumentsContract: This is the most reliable and recommended approach. It involves using the ContentResolver to query the DocumentsProvider associated with the URI. The DocumentsContract class provides constants and helper methods for interacting with DocumentsProviders. You can use the DocumentsContract.getDocumentId(uri) method to obtain the document ID, and then use the ContentResolver to query the provider for the file’s metadata, such as its display name, size, and MIME type. Here’s an example:
ContentResolver resolver = context.getContentResolver(); Uri uri = ... // The URI you want to get information from Cursor cursor = resolver.query(uri, null, null, null, null); try { if (cursor != null && cursor.moveToFirst()) { String displayName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE); long size = cursor.isNull(sizeIndex) ? -1 : cursor.getLong(sizeIndex); // Use displayName and size as needed } } finally { if (cursor != null) { cursor.close(); } }
2. Checking the URI Scheme: Before attempting to query the ContentProvider, it’s essential to check the URI scheme to determine the appropriate approach. If the URI scheme is content://, it indicates that the data is managed by a ContentProvider. If the URI scheme is file://, it might represent a direct file path, but it’s still recommended to use the ContentResolver to ensure compatibility across different Android versions. Here’s how you can check the URI scheme:
Uri uri = ... // The URI you want to check String scheme = uri.getScheme(); if ("content".equalsIgnoreCase(scheme)) { // Use ContentResolver to query the ContentProvider } else if ("file".equalsIgnoreCase(scheme)) { // Handle as a file path (with caution) }
3. Obtaining an InputStream: Instead of trying to get real path from URI, you can directly obtain an InputStream from the URI using the ContentResolver. This allows you to read the file data without relying on a file path. This is particularly useful for files stored in cloud storage services or virtual file systems. Here’s an example:
ContentResolver resolver = context.getContentResolver(); Uri uri = ... // The URI you want to get an InputStream from InputStream inputStream = resolver.openInputStream(uri); // Use inputStream to read the file data
Best Practices for URI Handling
- Always use the ContentResolver to query the ContentProvider for file information, regardless of the URI scheme.
- Handle potential exceptions, such as SecurityException or FileNotFoundException, when accessing files through URIs.
- Avoid relying solely on file paths, as they might not be available or reliable in all cases.
- Use InputStreams to read file data directly, especially for files stored in cloud storage services.
- Cache file information to improve performance and reduce the number of ContentProvider queries.
Code Example: Getting File Path from URI (with Fallback)
The following code snippet demonstrates a robust approach to get real path from URI, including a fallback mechanism for handling different URI types and storage providers. This method attempts to retrieve the file path using several techniques, starting with the most reliable approach (querying the DocumentsProvider) and falling back to less reliable methods if necessary. Note that even with this comprehensive approach, it might not always be possible to obtain a direct file path, especially for files stored in cloud storage services or virtual file systems. In such cases, it’s recommended to use an InputStream to read the file data directly.
This example is specifically designed to be featured as a snippet because it’s a complete and self-contained function that directly addresses the user’s query of how to get real path from URI in Android. It includes error handling and attempts multiple methods to ensure the highest chance of success, providing a practical and immediately useful solution.
java public static String getPathFromUri(Context context, Uri uri) { // Check if the URI is a content URI if (“content”.equalsIgnoreCase(uri.getScheme())) { // Attempt to retrieve the path from the DocumentsProvider if (DocumentsContract.isDocumentUri(context, uri)) { // Handle different document types if (isExternalStorageDocument(uri)) { String docId = DocumentsContract.getDocumentId(uri); String[] split = docId.split(":"); String type = split[0]; if (“primary”.equalsIgnoreCase(type)) { return Environment.getExternalStorageDirectory() + “/” + split[1]; } // Handle secondary external storage (if any) } else if (isDownloadsDocument(uri)) { String id = DocumentsContract.getDocumentId(uri); Uri contentUri = ContentUris.withAppendedId( Uri.parse(“content://downloads/public_downloads”), Long.valueOf(id)); return getDataColumn(context, contentUri, null, null); } else if (isMediaDocument(uri)) { String docId = DocumentsContract.getDocumentId(uri); String[] split = docId.split(":"); String type = split[0]; Uri contentUri = null; if (“image”.equals(type)) { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; } else if (“video”.equals(type)) { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } else if (“audio”.equals(type)) { contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } String selection = “_id=?”; String[] selectionArgs = new String[]{ split[1] }; return getDataColumn(context, contentUri, selection, selectionArgs); } } // MediaStore (and general) else if (“content”.equalsIgnoreCase(uri.getScheme())) { // Return the remote address if (isGooglePhotosUri(uri)) return uri.getLastPathSegment(); return getDataColumn(context, uri, null, null); } // File else if (“file”.equalsIgnoreCase(uri.getScheme())) { return uri.getPath(); } } return null; } / Get the value of the data column for this Uri. This is useful for MediaStore Uris, and other file-based ContentProviders. @param context The context. @param uri The Uri to query. @param selection (Optional) Filter used in the query. @param selectionArgs (Optional) Selection arguments used in the query. @return The value of the _data column, which is the file path. / public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; final String column = “_data”; final String[] projection = { column }; try { cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); if (cursor != null && cursor.moveToFirst()) { final int column_index = cursor.getColumnIndexOrThrow(column); return cursor.getString(column_index); } } finally { if (cursor != null) cursor.close(); } return null; } / @param uri The Uri to check. @return Whether the Uri authority is ExternalStorageProvider. / public static boolean isExternalStorageDocument(Uri uri) { return “com.android.externalstorage.documents”.equals(uri.getAuthority()); } / @param uri The Uri to check. @return Whether the Uri authority is DownloadsProvider. / public static boolean isDownloadsDocument(Uri uri) { return “com.android.providers.downloads.documents”.equals(uri.getAuthority()); } / @param uri The Uri to check. @return Whether the Uri authority is MediaProvider. / public static boolean isMediaDocument(Uri uri) { Question & Answer :
public String getPath(Uri uri) { String[] projection = { MediaStore.Images.Media.DATA }; Cursor cursor = managedQuery(uri, projection, null, null, null); startManagingCursor(cursor); int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); cursor.moveToFirst(); return cursor.getString(column_index); }
Now, the Intent.ACTION_GET_CONTENT return different data:
Before:
content://media/external/images/media/62
Now:
content://com.android.providers.media.documents/document/image:62
How could I manage to obtain the real path on the SD card?
This will get the file path from the MediaProvider, DownloadsProvider, and ExternalStorageProvider, while falling back to the unofficial ContentProvider method you mention.
/** * Get a file path from a Uri. This will get the the path for Storage Access * Framework Documents, as well as the _data field for the MediaStore and * other file-based ContentProviders. * * @param context The context. * @param uri The Uri to query. * @author paulburke */ public static String getPath(final Context context, final Uri uri) { final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; // DocumentProvider if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider if (isExternalStorageDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; if ("primary".equalsIgnoreCase(type)) { return Environment.getExternalStorageDirectory() + "/" + split[1]; } // TODO handle non-primary volumes } // DownloadsProvider else if (isDownloadsDocument(uri)) { final String id = DocumentsContract.getDocumentId(uri); final Uri contentUri = ContentUris.withAppendedId( Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); return getDataColumn(context, contentUri, null, null); } // MediaProvider else if (isMediaDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; Uri contentUri = null; if ("image".equals(type)) { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; } else if ("video".equals(type)) { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } else if ("audio".equals(type)) { contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } final String selection = "_id=?"; final String[] selectionArgs = new String[] { split[1] }; return getDataColumn(context, contentUri, selection, selectionArgs); } } // MediaStore (and general) else if ("content".equalsIgnoreCase(uri.getScheme())) { return getDataColumn(context, uri, null, null); } // File else if ("file".equalsIgnoreCase(uri.getScheme())) { return uri.getPath(); } return null; } /** * Get the value of the data column for this Uri. This is useful for * MediaStore Uris, and other file-based ContentProviders. * * @param context The context. * @param uri The Uri to query. * @param selection (Optional) Filter used in the query. * @param selectionArgs (Optional) Selection arguments used in the query. * @return The value of the _data column, which is typically a file path. */ public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; final String column = "_data"; final String[] projection = { column }; try { cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); if (cursor != null && cursor.moveToFirst()) { final int column_index = cursor.getColumnIndexOrThrow(column); return cursor.getString(column_index); } } finally { if (cursor != null) cursor.close(); } return null; } /** * @param uri The Uri to check. * @return Whether the Uri authority is ExternalStorageProvider. */ public static boolean isExternalStorageDocument(Uri uri) { return "com.android.externalstorage.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is DownloadsProvider. */ public static boolean isDownloadsDocument(Uri uri) { return "com.android.providers.downloads.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is MediaProvider. */ public static boolean isMediaDocument(Uri uri) { return "com.android.providers.media.documents".equals(uri.getAuthority()); }
These are taken from my open source library, aFileChooser.