From 6f38e171b2b4cf64e08a11db7e7ed5b14e753173 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Mon, 17 May 2021 19:22:18 +1000 Subject: [PATCH] Android: Fall back to native paths on <9 Apparently some of these devices have broken scoped storage? --- .../stenzek/duckstation/FileHelper.java | 129 ++++++++++++++++++ .../duckstation/GameDirectoriesActivity.java | 12 ++ .../stenzek/duckstation/MainActivity.java | 11 ++ 3 files changed, 152 insertions(+) diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/FileHelper.java b/android/app/src/main/java/com/github/stenzek/duckstation/FileHelper.java index 703bb4b78..258350d0d 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/FileHelper.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/FileHelper.java @@ -1,15 +1,21 @@ package com.github.stenzek.duckstation; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.ImageDecoder; import android.net.Uri; +import android.os.Build; import android.os.ParcelFileDescriptor; +import android.os.storage.StorageManager; import android.provider.DocumentsContract; import android.provider.MediaStore; +import androidx.annotation.Nullable; + import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; @@ -17,6 +23,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.lang.reflect.Array; +import java.lang.reflect.Method; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -463,6 +471,127 @@ public class FileHelper { } } + private static final String PRIMARY_VOLUME_NAME = "primary"; + + @Nullable + public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) { + if (treeUri == null) return null; + String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri), con); + if (volumePath == null) return File.separator; + if (volumePath.endsWith(File.separator)) + volumePath = volumePath.substring(0, volumePath.length() - 1); + + String documentPath = getDocumentPathFromTreeUri(treeUri); + if (documentPath.endsWith(File.separator)) + documentPath = documentPath.substring(0, documentPath.length() - 1); + + if (documentPath.length() > 0) { + if (documentPath.startsWith(File.separator)) + return volumePath + documentPath; + else + return volumePath + File.separator + documentPath; + } else return volumePath; + } + + @SuppressLint("ObsoleteSdkInt") + private static String getVolumePath(final String volumeId, Context context) { + if (volumeId == null) + return null; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null; + try { + StorageManager mStorageManager = + (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); + Class storageVolumeClazz = Class.forName("android.os.storage.StorageVolume"); + Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList"); + Method getUuid = storageVolumeClazz.getMethod("getUuid"); + Method getPath = storageVolumeClazz.getMethod("getPath"); + Method isPrimary = storageVolumeClazz.getMethod("isPrimary"); + Object result = getVolumeList.invoke(mStorageManager); + + final int length = Array.getLength(result); + for (int i = 0; i < length; i++) { + Object storageVolumeElement = Array.get(result, i); + String uuid = (String) getUuid.invoke(storageVolumeElement); + Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement); + + // primary volume? + if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) + return (String) getPath.invoke(storageVolumeElement); + + // other volumes? + if (uuid != null && uuid.equals(volumeId)) + return (String) getPath.invoke(storageVolumeElement); + } + // not found. + return null; + } catch (Exception ex) { + return null; + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static String getVolumeIdFromTreeUri(final Uri treeUri) { + final String docId = DocumentsContract.getTreeDocumentId(treeUri); + final String[] split = docId.split(":"); + if (split.length > 0) return split[0]; + else return null; + } + + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static String getDocumentPathFromTreeUri(final Uri treeUri) { + final String docId = DocumentsContract.getTreeDocumentId(treeUri); + final String[] split = docId.split(":"); + if ((split.length >= 2) && (split[1] != null)) return split[1]; + else return File.separator; + } + + @Nullable + public static String getFullPathFromUri(@Nullable final Uri treeUri, Context con) { + if (treeUri == null) return null; + String volumePath = getVolumePath(getVolumeIdFromUri(treeUri), con); + if (volumePath == null) return File.separator; + if (volumePath.endsWith(File.separator)) + volumePath = volumePath.substring(0, volumePath.length() - 1); + + String documentPath = getDocumentPathFromUri(treeUri); + if (documentPath.endsWith(File.separator)) + documentPath = documentPath.substring(0, documentPath.length() - 1); + + if (documentPath.length() > 0) { + if (documentPath.startsWith(File.separator)) + return volumePath + documentPath; + else + return volumePath + File.separator + documentPath; + } else return volumePath; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static String getVolumeIdFromUri(final Uri treeUri) { + try { + final String docId = DocumentsContract.getDocumentId(treeUri); + final String[] split = docId.split(":"); + if (split.length > 0) return split[0]; + else return null; + } catch (Exception e) { + return null; + } + } + + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static String getDocumentPathFromUri(final Uri treeUri) { + try { + final String docId = DocumentsContract.getDocumentId(treeUri); + final String[] split = docId.split(":"); + if ((split.length >= 2) && (split[1] != null)) return split[1]; + else return File.separator; + } catch (Exception e) { + return null; + } + } + /** * Java class containing the data for a file in a find operation. */ diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameDirectoriesActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameDirectoriesActivity.java index ea893b8c7..0bba77450 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameDirectoriesActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/GameDirectoriesActivity.java @@ -7,6 +7,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; +import android.os.FileUtils; import android.util.Log; import android.util.Property; import android.view.LayoutInflater; @@ -291,6 +292,17 @@ public class GameDirectoriesActivity extends AppCompatActivity { if (resultCode != RESULT_OK || data.getData() == null) return; + // Use legacy storage on devices older than Android 9... apparently some of them + // have broken storage access framework.... + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.P) { + final String path = FileHelper.getFullPathFromTreeUri(data.getData(), this); + if (path != null) { + addSearchDirectory(this, path, true); + mDirectoryListAdapter.reload(); + return; + } + } + try { getContentResolver().takePersistableUriPermission(data.getData(), Intent.FLAG_GRANT_READ_URI_PERMISSION); diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java index f3139095f..b6a7e44ad 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java @@ -272,6 +272,17 @@ public class MainActivity extends AppCompatActivity { if (resultCode != RESULT_OK || data.getData() == null) return; + // Use legacy storage on devices older than Android 9... apparently some of them + // have broken storage access framework.... + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.P) { + final String path = FileHelper.getFullPathFromTreeUri(data.getData(), this); + if (path != null) { + GameDirectoriesActivity.addSearchDirectory(this, path, true); + mGameList.refresh(false, false, this); + return; + } + } + try { getContentResolver().takePersistableUriPermission(data.getData(), Intent.FLAG_GRANT_READ_URI_PERMISSION);