How to use the new SD card access API presented for Android 5.0 (Lollipop)?

AndroidAndroid SdcardAndroid 5.0-LollipopDocumentfile

Android Problem Overview


Background

On Android 4.4 (KitKat), Google has made access to the SD card quite restricted.

As of Android Lollipop (5.0), developers can use a new API that asks the user to confirm to allow access to specific folders, as written on the this Google-Groups post .

The problem

The post directs you to visit two websites:

This looks like an inner example (perhaps to be shown on the API demos later), but it's quite hard to understand what's going on.

This is the official documentation of the new API, but it doesn't tell enough details about how to use it.

Here's what it tells you:

> If you really do need full access to an entire subtree of documents, > start by launching ACTION_OPEN_DOCUMENT_TREE to let the user pick a > directory. Then pass the resulting getData() into fromTreeUri(Context, > Uri) to start working with the user selected tree. > > As you navigate the tree of DocumentFile instances, you can always use > getUri() to obtain the Uri representing the underlying document for > that object, for use with openInputStream(Uri), etc. > > To simplify your code on devices running KITKAT or earlier, you can > use fromFile(File) which emulates the behavior of a DocumentsProvider.

The questions

I have a few questions about the new API:

  1. How do you really use it?
  2. According to the post, the OS will remember that the app was given a permission to access the files/folders. How do you check if you can access the files/folders? Is there a function that returns me the list of files/folders that I can access?
  3. How do you handle this problem on Kitkat? Is it a part of the support library?
  4. Is there a settings screen on the OS that shows which apps have access to which files/folders?
  5. What happens if an app is installed for multiple users on the same device?
  6. Is there any other documentation/tutorial about this new API?
  7. Can the permissions be revoked? If so, is there an intent that's being sent to the app?
  8. Would asking for the permission work recursively on a selected folder?
  9. Would using the permission also allow to give the user a chance of multiple selection by user's choice? Or does the app need to specifically tell the intent which files/folders to allow?
  10. Is there a way on the emulator to try the new API ? I mean, it has SD-card partition, but it works as the primary external storage, so all access to it is already given (using a simple permission).
  11. What happens when the user replaces the SD card with another one?

Android Solutions


Solution 1 - Android

Lots of good questions, let's dig in. :)

How do you use it?

Here's a great tutorial for interacting with the Storage Access Framework in KitKat:

https://developer.android.com/guide/topics/providers/document-provider.html#client

Interacting with the new APIs in Lollipop is very similar. To prompt the user to pick a directory tree, you can launch an intent like this:

    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    startActivityForResult(intent, 42);

Then in your onActivityResult(), you can pass the user-picked Uri to the new DocumentFile helper class. Here's a quick example that lists the files in the picked directory, and then creates a new file:

public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
    if (resultCode == RESULT_OK) {
        Uri treeUri = resultData.getData();
        DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);

        // List all existing files inside picked directory
        for (DocumentFile file : pickedDir.listFiles()) {
            Log.d(TAG, "Found file " + file.getName() + " with size " + file.length());
        }

        // Create a new file and write into it
        DocumentFile newFile = pickedDir.createFile("text/plain", "My Novel");
        OutputStream out = getContentResolver().openOutputStream(newFile.getUri());
        out.write("A long time ago...".getBytes());
        out.close();
    }
}

The Uri returned by DocumentFile.getUri() is flexible enough to use with may different platform APIs. For example, you could share it using Intent.setData() with Intent.FLAG_GRANT_READ_URI_PERMISSION.

If you want to access that Uri from native code, you can call ContentResolver.openFileDescriptor() and then use ParcelFileDescriptor.getFd() or detachFd() to obtain a traditional POSIX file descriptor integer.

How do you check if you can access the files/folders?

By default, the Uris returned through Storage Access Frameworks intents are not persisted across reboots. The platform "offers" the ability to persist the permission, but you still need to "take" the permission if you want it. In our example above, you'd call:

    getContentResolver().takePersistableUriPermission(treeUri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION |
            Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

You can always figure out what persisted grants your app has access to through the ContentResolver.getPersistedUriPermissions() API. If you no longer need access to a persisted Uri, you can release it with ContentResolver.releasePersistableUriPermission().

Is this available on KitKat?

No, we can't retroactively add new functionality to older versions of the platform.

Can I see what apps have access to files/folders?

There's currently no UI that shows this, but you can find the details in the "Granted Uri Permissions" section of adb shell dumpsys activity providers output.

What happens if an app is installed for multiple users on the same device?

Uri permission grants are isolated on a per-user basis, just like all other multi-user platform functionality. That is, the same app running under two different users has no overlaping or shared Uri permission grants.

Can the permissions be revoked?

The backing DocumentProvider can revoke permission at any time, such as when a cloud-based document is deleted. The most common way to discover these revoked permissions is when they disappear from ContentResolver.getPersistedUriPermissions() mentioned above.

Permissions are also revoked whenever app data is cleared for either app involved in the grant.

Would asking for the permission work recursively on a selected folder?

Yep, the ACTION_OPEN_DOCUMENT_TREE intent gives you recursive access to both existing and newly created files and directories.

Does this allow multiple selection?

Yep, multiple selection has been supported since KitKat, and you can allow it by setting EXTRA_ALLOW_MULTIPLE when starting your ACTION_OPEN_DOCUMENT intent. You can use Intent.setType() or EXTRA_MIME_TYPES to narrow the types of files that can be picked:

http://developer.android.com/reference/android/content/Intent.html#ACTION_OPEN_DOCUMENT

Is there a way on the emulator to try the new API?

Yep, the primary shared storage device should appear in the picker, even on the emulator. If your app only uses the Storage Access Framework for accessing shared storage, you no longer need the READ/WRITE_EXTERNAL_STORAGE permissions at all and can remove them or use the android:maxSdkVersion feature to only request them on older platform versions.

What happens when the user replaces the SD-card with another one?

When physical media is involved, the UUID (such as FAT serial number) of the underlying media is always burned into the returned Uri. The system uses this to connect you to the media that the user originally selected, even if the user swaps the media around between multiple slots.

If the user swaps in a second card, you'll need to prompt to gain access to the new card. Since the system remembers grants on a per-UUID basis, you'll continue to have previously-granted access to the original card if the user reinserts it later.

http://en.wikipedia.org/wiki/Volume_serial_number

Solution 2 - Android

In my Android project in Github, linked below, you can find working code that allows to write on extSdCard in Android 5. It assumes that the user gives access to the whole SD Card and then lets you write everywhere on this card. (If you want to have access only to single files, things get easier.)

Main Code snipplets

Triggering the Storage Access Framework:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void triggerStorageAccessFramework() {
	Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
	startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
}

Handling the response from the Storage Access Framework:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public final void onActivityResult(final int requestCode, final int resultCode, final Intent resultData) {
	if (requestCode == SettingsFragment.REQUEST_CODE_STORAGE_ACCESS) {
		Uri treeUri = null;
		if (resultCode == Activity.RESULT_OK) {
			// Get Uri from Storage Access Framework.
			treeUri = resultData.getData();

			// Persist URI in shared preference so that you can use it later.
			// Use your own framework here instead of PreferenceUtil.
    		PreferenceUtil.setSharedPreferenceUri(R.string.key_internal_uri_extsdcard, treeUri);
	
			// Persist access permissions.
			final int takeFlags = resultData.getFlags()
				& (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
			getActivity().getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
		}
	}
}

Getting an outputStream for a file via the Storage Access Framework (making use of the stored URL, assuming that this is the URL of the root folder of the external SD card)

DocumentFile targetDocument = getDocumentFile(file, false);
OutputStream outStream = Application.getAppContext().
	getContentResolver().openOutputStream(targetDocument.getUri());

This uses the following helper methods:

public static DocumentFile getDocumentFile(final File file, final boolean isDirectory) {
	String baseFolder = getExtSdCardFolder(file);

	if (baseFolder == null) {
		return null;
	}

	String relativePath = null;
	try {
		String fullPath = file.getCanonicalPath();
		relativePath = fullPath.substring(baseFolder.length() + 1);
	}
	catch (IOException e) {
		return null;
	}

	Uri treeUri = PreferenceUtil.getSharedPreferenceUri(R.string.key_internal_uri_extsdcard);

	if (treeUri == null) {
		return null;
	}

	// start with root of SD card and then parse through document tree.
	DocumentFile document = DocumentFile.fromTreeUri(Application.getAppContext(), treeUri);

	String[] parts = relativePath.split("\\/");
	for (int i = 0; i < parts.length; i++) {
		DocumentFile nextDocument = document.findFile(parts[i]);

		if (nextDocument == null) {
			if ((i < parts.length - 1) || isDirectory) {
				nextDocument = document.createDirectory(parts[i]);
			}
			else {
				nextDocument = document.createFile("image", parts[i]);
			}
		}
		document = nextDocument;
	}

	return document;
}

public static String getExtSdCardFolder(final File file) {
	String[] extSdPaths = getExtSdCardPaths();
	try {
		for (int i = 0; i < extSdPaths.length; i++) {
			if (file.getCanonicalPath().startsWith(extSdPaths[i])) {
				return extSdPaths[i];
			}
		}
	}
	catch (IOException e) {
		return null;
	}
	return null;
}

/**
 * Get a list of external SD card paths. (Kitkat or higher.)
 *
 * @return A list of external SD card paths.
 */
@TargetApi(Build.VERSION_CODES.KITKAT)
private static String[] getExtSdCardPaths() {
	List<String> paths = new ArrayList<>();
	for (File file : Application.getAppContext().getExternalFilesDirs("external")) {
		if (file != null && !file.equals(Application.getAppContext().getExternalFilesDir("external"))) {
			int index = file.getAbsolutePath().lastIndexOf("/Android/data");
			if (index < 0) {
				Log.w(Application.TAG, "Unexpected external file dir: " + file.getAbsolutePath());
			}
			else {
				String path = file.getAbsolutePath().substring(0, index);
				try {
					path = new File(path).getCanonicalPath();
				}
				catch (IOException e) {
					// Keep non-canonical path.
				}
				paths.add(path);
			}
		}
	}
	return paths.toArray(new String[paths.size()]);
}

 /**
 * Retrieve the application context.
 *
 * @return The (statically stored) application context
 */
public static Context getAppContext() {
	return Application.mApplication.getApplicationContext();
}

Reference to the full code

https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/fragments/SettingsFragment.java#L521

and

https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/FileUtil.java

Solution 3 - Android

SimpleStorage helps you by simplifying Storage Access Framework across API levels. It works with scoped storage as well. For example:

val fileFromExternalStorage = DocumentFileCompat.fromSimplePath(context, basePath = "Downloads/MyMovie.mp4")

val fileFromSdCard = DocumentFileCompat.fromSimplePath(context, storageId = "9016-4EF8", basePath = "Downloads/MyMovie.mp4")

Granting SD card's URI permissions, picking files & folders are simpler with this library:

class MainActivity : AppCompatActivity() {

    private lateinit var storageHelper: SimpleStorageHelper

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        storageHelper = SimpleStorageHelper(this, savedInstanceState)
        storageHelper.onFolderSelected = { requestCode, folder ->
            // do stuff
        }
        storageHelper.onFileSelected = { requestCode, file ->
            // do stuff
        }

        btnRequestStorageAccess.setOnClickListener { storageHelper.requestStorageAccess() }
        btnOpenFolderPicker.setOnClickListener { storageHelper.openFolderPicker() }
        btnOpenFilePicker.setOnClickListener { storageHelper.openFilePicker() }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        storageHelper.storage.onActivityResult(requestCode, resultCode, data)
    }

    override fun onSaveInstanceState(outState: Bundle) {
        storageHelper.onSaveInstanceState(outState)
        super.onSaveInstanceState(outState)
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        storageHelper.onRestoreInstanceState(savedInstanceState)
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionandroid developerView Question on Stackoverflow
Solution 1 - AndroidJeff SharkeyView Answer on Stackoverflow
Solution 2 - AndroidJörg EisfeldView Answer on Stackoverflow
Solution 3 - AndroidAnggrayudi HView Answer on Stackoverflow