package paris.tourolle.darkroom; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; import android.provider.Settings; import android.util.Log; import java.io.File; import java.util.ArrayList; import java.util.List; /** * Finding a camera card, and the permission that makes it readable (FR-CAT-10). * *
An import reads the card as files: the survey walks it, the probe reads * each header and the copy streams each original, all through the same * {@code std::fs} code the desktop uses. Android hands out such paths — * {@code /storage/9C33-6BBD/DCIM} — to an app holding "all files access" * ({@code MANAGE_EXTERNAL_STORAGE}, API 30), which covers the root of an SD * card and of a USB card reader. Below API 30 the same paths are readable * with {@code READ_EXTERNAL_STORAGE}. * *
Not the folder picker {@link FolderPicker} uses for albums. A tree * granted through SAF is {@code content://} URIs, not paths, and since API 30 * the picker refuses the root of a card outright; reading a card through it * would mean a second storage implementation under the importer, where this * needs none. * *
Google Play restricts this permission to file managers and the like. * DarkRoom is not distributed through Play, so the restriction does not * apply; it would need revisiting if that changed. */ public final class Cards { private static final String TAG = "DarkRoom"; private Cards() { } /** Whether this app may read a card's files by path. */ public static boolean hasAccess(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return Environment.isExternalStorageManager(); } return context.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; } /** * Open the system page where the user grants it. * *
A settings page rather than a permission dialog because there is no * dialog for this one on API 30 and up: the user flips "Allow access to * manage all files" for this app. Below 30 the context is the application * context, which cannot raise a runtime permission request (that needs an * Activity's result), so the app's own settings page is the route there * too. Either way the app learns of the grant by asking * {@link #hasAccess} again. */ public static void requestAccess(Context context) { Uri self = Uri.parse("package:" + context.getPackageName()); Intent intent; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, self); } else { intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, self); } // The context is not an Activity; see FolderPicker.start. intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { context.startActivity(intent); } catch (RuntimeException e) { // Some builds ship without the per-app page; the list of every // app holding the permission is the fallback that always exists. Log.w(TAG, "no per-app all-files page; opening the list", e); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { Intent list = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); list.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(list); } } } /** * Every mounted volume other than the device's own storage. * *
One string per volume, {@code path \t description \t removable},
* where removable is {@code 1} or {@code 0}: the reason {@link Intents}
* gives for keeping the JNI surface to strings. The primary volume is left
* out — it is the device's internal storage, never a card — and so is
* anything not mounted, which is a card being ejected or one the system
* could not read.
*/
public static String[] volumes(Context context) {
List {@code getDirectory} is API 30. Below it the same answer is the
* hidden {@code getPath}, which every release from 24 to 29 has, reached by
* reflection because android.jar does not declare it.
*/
private static String path(StorageVolume volume) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
File dir = volume.getDirectory();
return dir == null ? null : dir.getPath();
}
try {
Object path = StorageVolume.class.getMethod("getPath").invoke(volume);
return path == null ? null : path.toString();
} catch (ReflectiveOperationException e) {
Log.w(TAG, "StorageVolume.getPath", e);
return null;
}
}
}