From ef605f7d8f8241b95b977d95cf5247c1f2d8a309 Mon Sep 17 00:00:00 2001
From: bunnei <bunneidev@gmail.com>
Date: Fri, 3 Feb 2023 16:13:16 -0800
Subject: [PATCH] android: Implement SAF support & migrate to SDK 31. (#4)

---
 src/android/app/build.gradle                  |   6 +-
 src/android/app/src/main/AndroidManifest.xml  |  13 +-
 .../java/org/yuzu/yuzu_emu/NativeLibrary.java |  22 +-
 .../org/yuzu/yuzu_emu/YuzuApplication.java    |   8 +-
 .../activities/CustomFilePickerActivity.java  |  38 ---
 .../yuzu/yuzu_emu/adapters/GameAdapter.java   |   9 +-
 .../settings/ui/SettingsActivity.java         |   6 -
 .../ui/SettingsActivityPresenter.java         |   3 -
 .../settings/ui/SettingsActivityView.java     |   5 -
 .../fragments/CustomFilePickerFragment.java   | 120 --------
 .../yuzu_emu/fragments/EmulationFragment.java |   4 -
 .../org/yuzu/yuzu_emu/model/GameDatabase.java |  48 ++--
 .../yuzu_emu/model/MinimalDocumentFile.java   |  28 ++
 .../yuzu/yuzu_emu/ui/main/MainActivity.java   |  83 ++----
 .../yuzu/yuzu_emu/ui/main/MainPresenter.java  |   4 +-
 .../utils/DirectoryInitialization.java        | 132 +--------
 .../yuzu/yuzu_emu/utils/DocumentsTree.java    | 125 ++++++++
 .../yuzu_emu/utils/FileBrowserHelper.java     |  65 +----
 .../org/yuzu/yuzu_emu/utils/FileUtil.java     | 270 ++++++++++++++++--
 .../yuzu_emu/utils/PermissionsHandler.java    |  35 ---
 .../yuzu/yuzu_emu/utils/StartupHandler.java   |  44 ++-
 src/android/app/src/main/jni/config.cpp       |  29 +-
 src/android/app/src/main/jni/id_cache.cpp     |  46 +++
 src/android/app/src/main/jni/native.cpp       |  12 +-
 src/android/app/src/main/jni/native.h         |  95 +++---
 .../main/res/layout/filepicker_toolbar.xml    |  32 ---
 .../res/values-night/styles_filepicker.xml    |   5 -
 .../src/main/res/values-w1050dp/dimens.xml    |   1 -
 .../app/src/main/res/values-w820dp/dimens.xml |   1 -
 .../app/src/main/res/values/strings.xml       |   3 +-
 .../app/src/main/res/values/styles.xml        |  16 --
 .../src/main/res/values/styles_filepicker.xml |   5 -
 src/common/CMakeLists.txt                     |   8 +
 src/common/fs/file.cpp                        |  38 +++
 src/common/fs/fs_android.cpp                  |  98 +++++++
 src/common/fs/fs_android.h                    |  62 ++++
 src/common/fs/path_util.cpp                   |  31 +-
 src/common/fs/path_util.h                     |   8 +
 38 files changed, 856 insertions(+), 702 deletions(-)
 delete mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/CustomFilePickerActivity.java
 delete mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CustomFilePickerFragment.java
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MinimalDocumentFile.java
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.java
 delete mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PermissionsHandler.java
 delete mode 100644 src/android/app/src/main/res/layout/filepicker_toolbar.xml
 delete mode 100644 src/android/app/src/main/res/values-night/styles_filepicker.xml
 delete mode 100644 src/android/app/src/main/res/values/styles_filepicker.xml
 create mode 100644 src/common/fs/fs_android.cpp
 create mode 100644 src/common/fs/fs_android.h

diff --git a/src/android/app/build.gradle b/src/android/app/build.gradle
index ffbadce14..c516b2bff 100644
--- a/src/android/app/build.gradle
+++ b/src/android/app/build.gradle
@@ -32,7 +32,7 @@ android {
         // TODO If this is ever modified, change application_id in strings.xml
         applicationId "org.yuzu.yuzu_emu"
         minSdkVersion 28
-        targetSdkVersion 29
+        targetSdkVersion 31
         versionCode autoVersion
         versionName getVersion()
         ndk.abiFilters abiFilter
@@ -126,6 +126,7 @@ dependencies {
     implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
     implementation 'androidx.fragment:fragment:1.5.3'
     implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
+    implementation "androidx.documentfile:documentfile:1.0.1"
     implementation 'com.google.android.material:material:1.6.1'
 
     // For loading huge screenshots from the disk.
@@ -138,9 +139,6 @@ dependencies {
     implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
     implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
     implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
-
-    // Please don't upgrade the billing library as the newer version is not GPL-compatible
-    implementation 'com.android.billingclient:billing:2.0.3'
 }
 
 def getVersion() {
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index 0d7e3f7ad..88e1669cd 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -31,6 +31,7 @@
 
         <activity
             android:name="org.yuzu.yuzu_emu.ui.main.MainActivity"
+            android:exported="true"
             android:theme="@style/YuzuBase"
             android:resizeableActivity="false">
 
@@ -57,18 +58,6 @@
 
         <service android:name="org.yuzu.yuzu_emu.utils.ForegroundService"/>
 
-        <activity
-            android:name="org.yuzu.yuzu_emu.activities.CustomFilePickerActivity"
-            android:label="@string/app_name"
-            android:theme="@style/FilePickerTheme">
-            <intent-filter>
-                <action android:name="android.intent.action.GET_CONTENT" />
-                <category android:name="android.intent.category.DEFAULT" />
-            </intent-filter>
-        </activity>
-
-        <service android:name="org.yuzu.yuzu_emu.utils.DirectoryInitialization"/>
-
         <provider
             android:name="org.yuzu.yuzu_emu.model.GameProvider"
             android:authorities="${applicationId}.provider"
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java
index e15612a36..acb3fc2d6 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java
@@ -25,7 +25,9 @@ import androidx.core.content.ContextCompat;
 import androidx.fragment.app.DialogFragment;
 
 import org.yuzu.yuzu_emu.activities.EmulationActivity;
+import org.yuzu.yuzu_emu.utils.DocumentsTree;
 import org.yuzu.yuzu_emu.utils.EmulationMenuSettings;
+import org.yuzu.yuzu_emu.utils.FileUtil;
 import org.yuzu.yuzu_emu.utils.Log;
 
 import java.lang.ref.WeakReference;
@@ -66,6 +68,20 @@ public final class NativeLibrary {
         // Disallows instantiation.
     }
 
+    public static int openContentUri(String path, String openmode) {
+        if (DocumentsTree.isNativePath(path)) {
+            return YuzuApplication.documentsTree.openContentUri(path, openmode);
+        }
+        return FileUtil.openContentUri(YuzuApplication.getAppContext(), path, openmode);
+    }
+
+    public static long getSize(String path) {
+        if (DocumentsTree.isNativePath(path)) {
+            return YuzuApplication.documentsTree.getFileSize(path);
+        }
+        return FileUtil.getFileSize(YuzuApplication.getAppContext(), path);
+    }
+
     /**
      * Handles button press events for a gamepad.
      *
@@ -147,11 +163,7 @@ public final class NativeLibrary {
 
     public static native String GetGitRevision();
 
-    /**
-     * Sets the current working user directory
-     * If not set, it auto-detects a location
-     */
-    public static native void SetUserDirectory(String directory);
+    public static native void SetAppDirectory(String directory);
 
     // Create the config.ini file.
     public static native void CreateConfigFile();
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.java
index 700916f87..d7b75e5a6 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.java
@@ -11,11 +11,12 @@ import android.content.Context;
 import android.os.Build;
 
 import org.yuzu.yuzu_emu.model.GameDatabase;
+import org.yuzu.yuzu_emu.utils.DocumentsTree;
 import org.yuzu.yuzu_emu.utils.DirectoryInitialization;
-import org.yuzu.yuzu_emu.utils.PermissionsHandler;
 
 public class YuzuApplication extends Application {
     public static GameDatabase databaseHelper;
+    public static DocumentsTree documentsTree;
     private static YuzuApplication application;
 
     private void createNotificationChannel() {
@@ -39,10 +40,9 @@ public class YuzuApplication extends Application {
     public void onCreate() {
         super.onCreate();
         application = this;
+        documentsTree = new DocumentsTree();
 
-        if (PermissionsHandler.hasWriteAccess(getApplicationContext())) {
-            DirectoryInitialization.start(getApplicationContext());
-        }
+        DirectoryInitialization.start(getApplicationContext());
 
         NativeLibrary.LogDeviceInfo();
         createNotificationChannel();
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/CustomFilePickerActivity.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/CustomFilePickerActivity.java
deleted file mode 100644
index a79780814..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/CustomFilePickerActivity.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package org.yuzu.yuzu_emu.activities;
-
-import android.content.Intent;
-import android.os.Environment;
-
-import androidx.annotation.Nullable;
-
-import com.nononsenseapps.filepicker.AbstractFilePickerFragment;
-import com.nononsenseapps.filepicker.FilePickerActivity;
-
-import org.yuzu.yuzu_emu.fragments.CustomFilePickerFragment;
-
-import java.io.File;
-
-public class CustomFilePickerActivity extends FilePickerActivity {
-    public static final String EXTRA_TITLE = "filepicker.intent.TITLE";
-    public static final String EXTRA_EXTENSIONS = "filepicker.intent.EXTENSIONS";
-
-    @Override
-    protected AbstractFilePickerFragment<File> getFragment(
-            @Nullable final String startPath, final int mode, final boolean allowMultiple,
-            final boolean allowCreateDir, final boolean allowExistingFile,
-            final boolean singleClick) {
-        CustomFilePickerFragment fragment = new CustomFilePickerFragment();
-        // startPath is allowed to be null. In that case, default folder should be SD-card and not "/"
-        fragment.setArgs(
-                startPath != null ? startPath : Environment.getExternalStorageDirectory().getPath(),
-                mode, allowMultiple, allowCreateDir, allowExistingFile, singleClick);
-
-        Intent intent = getIntent();
-        int title = intent == null ? 0 : intent.getIntExtra(EXTRA_TITLE, 0);
-        fragment.setTitle(title);
-        String allowedExtensions = intent == null ? "*" : intent.getStringExtra(EXTRA_EXTENSIONS);
-        fragment.setAllowedExtensions(allowedExtensions);
-
-        return fragment;
-    }
-}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java
index fa785741b..cd9f823d4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java
@@ -16,16 +16,16 @@ import androidx.core.content.ContextCompat;
 import androidx.fragment.app.FragmentActivity;
 import androidx.recyclerview.widget.RecyclerView;
 
+import org.yuzu.yuzu_emu.YuzuApplication;
 import org.yuzu.yuzu_emu.R;
 import org.yuzu.yuzu_emu.activities.EmulationActivity;
 import org.yuzu.yuzu_emu.model.GameDatabase;
 import org.yuzu.yuzu_emu.ui.DividerItemDecoration;
+import org.yuzu.yuzu_emu.utils.FileUtil;
 import org.yuzu.yuzu_emu.utils.Log;
 import org.yuzu.yuzu_emu.utils.PicassoUtils;
 import org.yuzu.yuzu_emu.viewholders.GameViewHolder;
 
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.stream.Stream;
 
 /**
@@ -88,8 +88,9 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
                 holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
                 holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
 
-                final Path gamePath = Paths.get(mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
-                holder.textFileName.setText(gamePath.getFileName().toString());
+                String filepath = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
+                String filename = FileUtil.getFilename(YuzuApplication.getAppContext(), filepath);
+                holder.textFileName.setText(filename);
 
                 // TODO These shouldn't be necessary once the move to a DB-based model is complete.
                 holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID);
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.java
index 916ced382..0a1323a1f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.java
@@ -159,12 +159,6 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
         dialog.dismiss();
     }
 
-    @Override
-    public void showPermissionNeededHint() {
-        Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
-                .show();
-    }
-
     @Override
     public void showExternalStorageNotMountedHint() {
         Toast.makeText(this, R.string.external_storage_not_mounted, Toast.LENGTH_SHORT)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.java
index ba6b6762b..25b7758a9 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.java
@@ -78,9 +78,6 @@ public final class SettingsActivityPresenter {
                         if (directoryInitializationState == DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) {
                             mView.hideLoading();
                             loadSettingsUI();
-                        } else if (directoryInitializationState == DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) {
-                            mView.showPermissionNeededHint();
-                            mView.hideLoading();
                         } else if (directoryInitializationState == DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
                             mView.showExternalStorageNotMountedHint();
                             mView.hideLoading();
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.java
index 5aff3bcf7..58ccf31b7 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.java
@@ -76,11 +76,6 @@ public interface SettingsActivityView {
      */
     void hideLoading();
 
-    /**
-     * Show a hint to the user that the app needs write to external storage access
-     */
-    void showPermissionNeededHint();
-
     /**
      * Show a hint to the user that the app needs the external storage to be mounted
      */
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CustomFilePickerFragment.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CustomFilePickerFragment.java
deleted file mode 100644
index 2658b1445..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CustomFilePickerFragment.java
+++ /dev/null
@@ -1,120 +0,0 @@
-package org.yuzu.yuzu_emu.fragments;
-
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Environment;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.widget.Toolbar;
-import androidx.core.content.FileProvider;
-
-import com.nononsenseapps.filepicker.FilePickerFragment;
-
-import org.yuzu.yuzu_emu.R;
-
-import java.io.File;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-public class CustomFilePickerFragment extends FilePickerFragment {
-    private static String ALL_FILES = "*";
-    private int mTitle;
-    private static List<String> extensions = Collections.singletonList(ALL_FILES);
-
-    @NonNull
-    @Override
-    public Uri toUri(@NonNull final File file) {
-        return FileProvider
-                .getUriForFile(getContext(),
-                        getContext().getApplicationContext().getPackageName() + ".filesprovider",
-                        file);
-    }
-
-    @Override
-    public void onActivityCreated(Bundle savedInstanceState) {
-        super.onActivityCreated(savedInstanceState);
-
-        if (mode == MODE_DIR) {
-            TextView ok = getActivity().findViewById(R.id.nnf_button_ok);
-            ok.setText(R.string.select_dir);
-
-            TextView cancel = getActivity().findViewById(R.id.nnf_button_cancel);
-            cancel.setVisibility(View.GONE);
-        }
-    }
-
-    @Override
-    protected View inflateRootView(LayoutInflater inflater, ViewGroup container) {
-        View view = super.inflateRootView(inflater, container);
-        if (mTitle != 0) {
-            Toolbar toolbar = view.findViewById(com.nononsenseapps.filepicker.R.id.nnf_picker_toolbar);
-            ViewGroup parent = (ViewGroup) toolbar.getParent();
-            int index = parent.indexOfChild(toolbar);
-            View newToolbar = inflater.inflate(R.layout.filepicker_toolbar, toolbar, false);
-            TextView title = newToolbar.findViewById(R.id.filepicker_title);
-            title.setText(mTitle);
-            parent.removeView(toolbar);
-            parent.addView(newToolbar, index);
-        }
-        return view;
-    }
-
-    public void setTitle(int title) {
-        mTitle = title;
-    }
-
-    public void setAllowedExtensions(String allowedExtensions) {
-        if (allowedExtensions == null)
-            return;
-
-        extensions = Arrays.asList(allowedExtensions.split(","));
-    }
-
-    @Override
-    protected boolean isItemVisible(@NonNull final File file) {
-        // Some users jump to the conclusion that Dolphin isn't able to detect their
-        // files if the files don't show up in the file picker when mode == MODE_DIR.
-        // To avoid this, show files even when the user needs to select a directory.
-        return (showHiddenItems || !file.isHidden()) &&
-                (file.isDirectory() || extensions.contains(ALL_FILES) ||
-                        extensions.contains(fileExtension(file.getName()).toLowerCase()));
-    }
-
-    @Override
-    public boolean isCheckable(@NonNull final File file) {
-        // We need to make a small correction to the isCheckable logic due to
-        // overriding isItemVisible to show files when mode == MODE_DIR.
-        // AbstractFilePickerFragment always treats files as checkable when
-        // allowExistingFile == true, but we don't want files to be checkable when mode == MODE_DIR.
-        return super.isCheckable(file) && !(mode == MODE_DIR && file.isFile());
-    }
-
-    @Override
-    public void goUp() {
-        if (Environment.getExternalStorageDirectory().getPath().equals(mCurrentPath.getPath())) {
-            goToDir(new File("/storage/"));
-            return;
-        }
-        if (mCurrentPath.equals(new File("/storage/"))){
-            return;
-        }
-        super.goUp();
-    }
-
-    @Override
-    public void onClickDir(@NonNull View view, @NonNull DirViewHolder viewHolder) {
-        if(viewHolder.file.equals(new File("/storage/emulated/")))
-            viewHolder.file = new File("/storage/emulated/0/");
-        super.onClickDir(view, viewHolder);
-    }
-
-    private static String fileExtension(@NonNull String filename) {
-        int i = filename.lastIndexOf('.');
-        return i < 0 ? "" : filename.substring(i + 1);
-    }
-}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java
index f7a242171..32f077944 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java
@@ -155,10 +155,6 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C
                     if (directoryInitializationState ==
                             DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) {
                         mEmulationState.run(activity.isActivityRecreated());
-                    } else if (directoryInitializationState ==
-                            DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) {
-                        Toast.makeText(getContext(), R.string.write_permission_needed, Toast.LENGTH_SHORT)
-                                .show();
                     } else if (directoryInitializationState ==
                             DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
                         Toast.makeText(getContext(), R.string.external_storage_not_mounted,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java
index ac5db1c36..771e35c69 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java
@@ -5,8 +5,10 @@ import android.content.Context;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
 
 import org.yuzu.yuzu_emu.NativeLibrary;
+import org.yuzu.yuzu_emu.utils.FileUtil;
 import org.yuzu.yuzu_emu.utils.Log;
 
 import java.io.File;
@@ -63,10 +65,12 @@ public final class GameDatabase extends SQLiteOpenHelper {
 
     private static final String SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS;
     private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES;
+    private final Context context;
 
     public GameDatabase(Context context) {
         // Superclass constructor builds a database or uses an existing one.
         super(context, "games.db", null, DB_VERSION);
+        this.context = context;
     }
 
     @Override
@@ -123,8 +127,6 @@ public final class GameDatabase extends SQLiteOpenHelper {
             File game = new File(gamePath);
 
             if (!game.exists()) {
-                Log.error("[GameDatabase] Game file no longer exists. Removing from the library: " +
-                        gamePath);
                 database.delete(TABLE_NAME_GAMES,
                         KEY_DB_ID + " = ?",
                         new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))});
@@ -150,9 +152,9 @@ public final class GameDatabase extends SQLiteOpenHelper {
         while (folderCursor.moveToNext()) {
             String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH);
 
-            File folder = new File(folderPath);
+            Uri folderUri = Uri.parse(folderPath);
             // If the folder is empty because it no longer exists, remove it from the library.
-            if (!folder.exists()) {
+            if (FileUtil.listFiles(context, folderUri).length == 0) {
                 Log.error(
                         "[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath);
                 database.delete(TABLE_NAME_FOLDERS,
@@ -160,7 +162,7 @@ public final class GameDatabase extends SQLiteOpenHelper {
                         new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))});
             }
 
-            addGamesRecursive(database, folder, allowedExtensions, 3);
+            this.addGamesRecursive(database, folderUri, allowedExtensions, 3);
         }
 
         fileCursor.close();
@@ -169,33 +171,27 @@ public final class GameDatabase extends SQLiteOpenHelper {
         database.close();
     }
 
-    private static void addGamesRecursive(SQLiteDatabase database, File parent, Set<String> allowedExtensions, int depth) {
+    private void addGamesRecursive(SQLiteDatabase database, Uri parent, Set<String> allowedExtensions, int depth) {
         if (depth <= 0) {
             return;
         }
 
-        File[] children = parent.listFiles();
-        if (children != null) {
-            for (File file : children) {
-                if (file.isHidden()) {
-                    continue;
-                }
+        MinimalDocumentFile[] children = FileUtil.listFiles(context, parent);
+        for (MinimalDocumentFile file : children) {
+            if (file.isDirectory()) {
+                Set<String> newExtensions = new HashSet<>(Arrays.asList(
+                        ".xci", ".nsp", ".nca", ".nro"));
+                this.addGamesRecursive(database, file.getUri(), newExtensions, depth - 1);
+            } else {
+                String filename = file.getUri().toString();
 
-                if (file.isDirectory()) {
-                    Set<String> newExtensions = new HashSet<>(Arrays.asList(
-                            ".xci", ".nsp", ".nca", ".nro"));
-                    addGamesRecursive(database, file, newExtensions, depth - 1);
-                } else {
-                    String filePath = file.getPath();
+                int extensionStart = filename.lastIndexOf('.');
+                if (extensionStart > 0) {
+                    String fileExtension = filename.substring(extensionStart);
 
-                    int extensionStart = filePath.lastIndexOf('.');
-                    if (extensionStart > 0) {
-                        String fileExtension = filePath.substring(extensionStart);
-
-                        // Check that the file has an extension we care about before trying to read out of it.
-                        if (allowedExtensions.contains(fileExtension.toLowerCase())) {
-                            attemptToAddGame(database, filePath);
-                        }
+                    // Check that the file has an extension we care about before trying to read out of it.
+                    if (allowedExtensions.contains(fileExtension.toLowerCase())) {
+                        attemptToAddGame(database, filename);
                     }
                 }
             }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MinimalDocumentFile.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MinimalDocumentFile.java
new file mode 100644
index 000000000..4ec001a7f
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MinimalDocumentFile.java
@@ -0,0 +1,28 @@
+package org.yuzu.yuzu_emu.model;
+
+import android.net.Uri;
+import android.provider.DocumentsContract;
+
+public class MinimalDocumentFile {
+    private final String filename;
+    private final Uri uri;
+    private final String mimeType;
+
+    public MinimalDocumentFile(String filename, String mimeType, Uri uri) {
+        this.filename = filename;
+        this.mimeType = mimeType;
+        this.uri = uri;
+    }
+
+    public String getFilename() {
+        return filename;
+    }
+
+    public Uri getUri() {
+        return uri;
+    }
+
+    public boolean isDirectory() {
+        return mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
+    }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java
index d419750a3..26ff14914 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java
@@ -1,12 +1,11 @@
 package org.yuzu.yuzu_emu.ui.main;
 
 import android.content.Intent;
-import android.content.pm.PackageManager;
+import android.net.Uri;
 import android.os.Bundle;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
-import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.AppCompatActivity;
@@ -18,16 +17,11 @@ import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity;
 import org.yuzu.yuzu_emu.model.GameProvider;
 import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment;
 import org.yuzu.yuzu_emu.utils.AddDirectoryHelper;
-import org.yuzu.yuzu_emu.utils.DirectoryInitialization;
 import org.yuzu.yuzu_emu.utils.FileBrowserHelper;
-import org.yuzu.yuzu_emu.utils.PermissionsHandler;
 import org.yuzu.yuzu_emu.utils.PicassoUtils;
 import org.yuzu.yuzu_emu.utils.StartupHandler;
 import org.yuzu.yuzu_emu.utils.ThemeUtil;
 
-import java.util.Arrays;
-import java.util.Collections;
-
 /**
  * The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which
  * individually display a grid of available games for each Fragment, in a tabbed layout.
@@ -54,12 +48,9 @@ public final class MainActivity extends AppCompatActivity implements MainView {
         mPresenter.onCreate();
 
         if (savedInstanceState == null) {
-            StartupHandler.HandleInit(this);
-            if (PermissionsHandler.hasWriteAccess(this)) {
-                mPlatformGamesFragment = new PlatformGamesFragment();
-                getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
-                        .commit();
-            }
+            StartupHandler.handleInit(this);
+            mPlatformGamesFragment = new PlatformGamesFragment();
+            getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment).commit();
         } else {
             mPlatformGamesFragment = (PlatformGamesFragment) getSupportFragmentManager().getFragment(savedInstanceState, "mPlatformGamesFragment");
         }
@@ -72,15 +63,13 @@ public final class MainActivity extends AppCompatActivity implements MainView {
     @Override
     protected void onSaveInstanceState(@NonNull Bundle outState) {
         super.onSaveInstanceState(outState);
-        if (PermissionsHandler.hasWriteAccess(this)) {
-            if (getSupportFragmentManager() == null) {
-                return;
-            }
-            if (outState == null) {
-                return;
-            }
-            getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment);
+        if (getSupportFragmentManager() == null) {
+            return;
         }
+        if (outState == null) {
+            return;
+        }
+        getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment);
     }
 
     @Override
@@ -119,27 +108,17 @@ public final class MainActivity extends AppCompatActivity implements MainView {
 
     @Override
     public void launchSettingsActivity(String menuTag) {
-        if (PermissionsHandler.hasWriteAccess(this)) {
-            SettingsActivity.launch(this, menuTag, "");
-        } else {
-            PermissionsHandler.checkWritePermission(this);
-        }
+        SettingsActivity.launch(this, menuTag, "");
     }
 
     @Override
     public void launchFileListActivity(int request) {
-        if (PermissionsHandler.hasWriteAccess(this)) {
-            switch (request) {
-                case MainPresenter.REQUEST_ADD_DIRECTORY:
-                    FileBrowserHelper.openDirectoryPicker(this,
-                                                      MainPresenter.REQUEST_ADD_DIRECTORY,
-                                                      R.string.select_game_folder,
-                                                      Arrays.asList("nso", "nro", "nca", "xci",
-                                                                    "nsp", "kip"));
-                    break;
-            }
-        } else {
-            PermissionsHandler.checkWritePermission(this);
+        switch (request) {
+            case MainPresenter.REQUEST_ADD_DIRECTORY:
+                FileBrowserHelper.openDirectoryPicker(this,
+                                                  MainPresenter.REQUEST_ADD_DIRECTORY,
+                                                  R.string.select_game_folder);
+                break;
         }
     }
 
@@ -155,6 +134,8 @@ public final class MainActivity extends AppCompatActivity implements MainView {
             case MainPresenter.REQUEST_ADD_DIRECTORY:
                 // If the user picked a file, as opposed to just backing out.
                 if (resultCode == MainActivity.RESULT_OK) {
+                    int takeFlags = (Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                    getContentResolver().takePersistableUriPermission(Uri.parse(result.getDataString()), takeFlags);
                     // When a new directory is picked, we currently will reset the existing games
                     // database. This effectively means that only one game directory is supported.
                     // TODO(bunnei): Consider fixing this in the future, or removing code for this.
@@ -166,32 +147,6 @@ public final class MainActivity extends AppCompatActivity implements MainView {
         }
     }
 
-    @Override
-    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
-        switch (requestCode) {
-            case PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION:
-                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
-                    DirectoryInitialization.start(this);
-
-                    mPlatformGamesFragment = new PlatformGamesFragment();
-                    getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
-                            .commit();
-
-                    // Immediately prompt user to select a game directory on first boot
-                    if (mPresenter != null) {
-                        mPresenter.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY);
-                    }
-                } else {
-                    Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
-                            .show();
-                }
-                break;
-            default:
-                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
-                break;
-        }
-    }
-
     /**
      * Called by the framework whenever any actionbar/toolbar icon is clicked.
      *
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.java
index 4cf643552..01f577600 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.java
@@ -22,7 +22,7 @@ public final class MainPresenter {
     public void onCreate() {
         String versionName = BuildConfig.VERSION_NAME;
         mView.setVersionString(versionName);
-        refeshGameList();
+        refreshGameList();
     }
 
     public void launchFileListActivity(int request) {
@@ -63,7 +63,7 @@ public final class MainPresenter {
         mDirToAdd = dir;
     }
 
-    public void refeshGameList() {
+    public void refreshGameList() {
         GameDatabase databaseHelper = YuzuApplication.databaseHelper;
         databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
         mView.refresh();
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.java
index bac52bb2a..f922ae183 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.java
@@ -1,35 +1,16 @@
-/**
- * Copyright 2014 Dolphin Emulator Project
- * Licensed under GPLv2+
- * Refer to the license.txt file included.
- */
-
 package org.yuzu.yuzu_emu.utils;
 
 import android.content.Context;
 import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.Environment;
-import android.preference.PreferenceManager;
-
 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 
 import org.yuzu.yuzu_emu.NativeLibrary;
 
-import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-/**
- * A service that spawns its own thread in order to copy several binary and shader files
- * from the yuzu APK to the external file system.
- */
 public final class DirectoryInitialization {
     public static final String BROADCAST_ACTION = "org.yuzu.yuzu_emu.BROADCAST";
-
     public static final String EXTRA_STATE = "directoryState";
     private static volatile DirectoryInitializationState directoryState = null;
     private static String userPath;
@@ -37,7 +18,6 @@ public final class DirectoryInitialization {
 
     public static void start(Context context) {
         // Can take a few seconds to run, so don't block UI thread.
-        //noinspection TrivialFunctionalExpressionUsage
         ((Runnable) () -> init(context)).run();
     }
 
@@ -46,31 +26,15 @@ public final class DirectoryInitialization {
             return;
 
         if (directoryState != DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) {
-            if (PermissionsHandler.hasWriteAccess(context)) {
-                if (setUserDirectory()) {
-                    initializeInternalStorage(context);
-                    NativeLibrary.CreateConfigFile();
-                    directoryState = DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED;
-                } else {
-                    directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE;
-                }
-            } else {
-                directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED;
-            }
+            initializeInternalStorage(context);
+            NativeLibrary.CreateConfigFile();
+            directoryState = DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED;
         }
 
         isDirectoryInitializationRunning.set(false);
         sendBroadcastState(directoryState, context);
     }
 
-    private static void deleteDirectoryRecursively(File file) {
-        if (file.isDirectory()) {
-            for (File child : file.listFiles())
-                deleteDirectoryRecursively(child);
-        }
-        file.delete();
-    }
-
     public static boolean areDirectoriesReady() {
         return directoryState == DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED;
     }
@@ -85,41 +49,13 @@ public final class DirectoryInitialization {
         return userPath;
     }
 
-    private static native void SetSysDirectory(String path);
-
-    private static boolean setUserDirectory() {
-        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
-            File externalPath = Environment.getExternalStorageDirectory();
-            if (externalPath != null) {
-                userPath = externalPath.getAbsolutePath() + "/yuzu-emu";
-                Log.debug("[DirectoryInitialization] User Dir: " + userPath);
-                // NativeLibrary.SetUserDirectory(userPath);
-                return true;
-            }
-
+    public static void initializeInternalStorage(Context context) {
+        try {
+            userPath = context.getExternalFilesDir(null).getCanonicalPath();
+            NativeLibrary.SetAppDirectory(userPath);
+        } catch(IOException e) {
+            e.printStackTrace();
         }
-
-        return false;
-    }
-
-    private static void initializeInternalStorage(Context context) {
-        File sysDirectory = new File(context.getFilesDir(), "Sys");
-
-        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
-        String revision = NativeLibrary.GetGitRevision();
-        if (!preferences.getString("sysDirectoryVersion", "").equals(revision)) {
-            // There is no extracted Sys directory, or there is a Sys directory from another
-            // version of yuzu that might contain outdated files. Let's (re-)extract Sys.
-            deleteDirectoryRecursively(sysDirectory);
-            copyAssetFolder("Sys", sysDirectory, true, context);
-
-            SharedPreferences.Editor editor = preferences.edit();
-            editor.putString("sysDirectoryVersion", revision);
-            editor.apply();
-        }
-
-        // Let the native code know where the Sys directory is.
-        SetSysDirectory(sysDirectory.getPath());
     }
 
     private static void sendBroadcastState(DirectoryInitializationState state, Context context) {
@@ -129,58 +65,8 @@ public final class DirectoryInitialization {
         LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent);
     }
 
-    private static void copyAsset(String asset, File output, Boolean overwrite, Context context) {
-        Log.verbose("[DirectoryInitialization] Copying File " + asset + " to " + output);
-
-        try {
-            if (!output.exists() || overwrite) {
-                InputStream in = context.getAssets().open(asset);
-                OutputStream out = new FileOutputStream(output);
-                copyFile(in, out);
-                in.close();
-                out.close();
-            }
-        } catch (IOException e) {
-            Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset +
-                    e.getMessage());
-        }
-    }
-
-    private static void copyAssetFolder(String assetFolder, File outputFolder, Boolean overwrite,
-                                        Context context) {
-        Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " +
-                outputFolder);
-
-        try {
-            boolean createdFolder = false;
-            for (String file : context.getAssets().list(assetFolder)) {
-                if (!createdFolder) {
-                    outputFolder.mkdir();
-                    createdFolder = true;
-                }
-                copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file),
-                        overwrite, context);
-                copyAsset(assetFolder + File.separator + file, new File(outputFolder, file), overwrite,
-                        context);
-            }
-        } catch (IOException e) {
-            Log.error("[DirectoryInitialization] Failed to copy asset folder: " + assetFolder +
-                    e.getMessage());
-        }
-    }
-
-    private static void copyFile(InputStream in, OutputStream out) throws IOException {
-        byte[] buffer = new byte[1024];
-        int read;
-
-        while ((read = in.read(buffer)) != -1) {
-            out.write(buffer, 0, read);
-        }
-    }
-
     public enum DirectoryInitializationState {
         YUZU_DIRECTORIES_INITIALIZED,
-        EXTERNAL_STORAGE_PERMISSION_NEEDED,
         CANT_FIND_EXTERNAL_STORAGE
     }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.java
new file mode 100644
index 000000000..beb790ab1
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.java
@@ -0,0 +1,125 @@
+package org.yuzu.yuzu_emu.utils;
+
+import android.content.Context;
+import android.net.Uri;
+
+import androidx.annotation.Nullable;
+import androidx.documentfile.provider.DocumentFile;
+
+import org.yuzu.yuzu_emu.YuzuApplication;
+import org.yuzu.yuzu_emu.model.MinimalDocumentFile;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+public class DocumentsTree {
+    private DocumentsNode root;
+    private final Context context;
+    public static final String DELIMITER = "/";
+
+    public DocumentsTree() {
+        context = YuzuApplication.getAppContext();
+    }
+
+    public void setRoot(Uri rootUri) {
+        root = null;
+        root = new DocumentsNode();
+        root.uri = rootUri;
+        root.isDirectory = true;
+    }
+
+    public int openContentUri(String filepath, String openmode) {
+        DocumentsNode node = resolvePath(filepath);
+        if (node == null) {
+            return -1;
+        }
+        return FileUtil.openContentUri(context, node.uri.toString(), openmode);
+    }
+
+    public long getFileSize(String filepath) {
+        DocumentsNode node = resolvePath(filepath);
+        if (node == null || node.isDirectory) {
+            return 0;
+        }
+        return FileUtil.getFileSize(context, node.uri.toString());
+    }
+
+    public boolean Exists(String filepath) {
+        return resolvePath(filepath) != null;
+    }
+
+    @Nullable
+    private DocumentsNode resolvePath(String filepath) {
+        StringTokenizer tokens = new StringTokenizer(filepath, DELIMITER, false);
+        DocumentsNode iterator = root;
+        while (tokens.hasMoreTokens()) {
+            String token = tokens.nextToken();
+            if (token.isEmpty()) continue;
+            iterator = find(iterator, token);
+            if (iterator == null) return null;
+        }
+        return iterator;
+    }
+
+    @Nullable
+    private DocumentsNode find(DocumentsNode parent, String filename) {
+        if (parent.isDirectory && !parent.loaded) {
+            structTree(parent);
+        }
+        return parent.children.get(filename);
+    }
+
+    /**
+     * Construct current level directory tree
+     * @param parent parent node of this level
+     */
+    private void structTree(DocumentsNode parent) {
+        MinimalDocumentFile[] documents = FileUtil.listFiles(context, parent.uri);
+        for (MinimalDocumentFile document: documents) {
+            DocumentsNode node = new DocumentsNode(document);
+            node.parent = parent;
+            parent.children.put(node.name, node);
+        }
+        parent.loaded = true;
+    }
+
+    public static boolean isNativePath(String path) {
+        if (path.length() > 0) {
+            return path.charAt(0) == '/';
+        }
+        return false;
+    }
+
+    private static class DocumentsNode {
+        private DocumentsNode parent;
+        private final Map<String, DocumentsNode> children = new HashMap<>();
+        private String name;
+        private Uri uri;
+        private boolean loaded = false;
+        private boolean isDirectory = false;
+
+        private DocumentsNode() {}
+        private DocumentsNode(MinimalDocumentFile document) {
+            name = document.getFilename();
+            uri = document.getUri();
+            isDirectory = document.isDirectory();
+            loaded = !isDirectory;
+        }
+        private DocumentsNode(DocumentFile document, boolean isCreateDir) {
+            name = document.getName();
+            uri = document.getUri();
+            isDirectory = isCreateDir;
+            loaded = true;
+        }
+
+        private void rename(String name) {
+            if (parent == null) {
+                return;
+            }
+            parent.children.remove(this.name);
+            this.name = name;
+            parent.children.put(name, this);
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileBrowserHelper.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileBrowserHelper.java
index ad3ec3dc1..6175f39c4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileBrowserHelper.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileBrowserHelper.java
@@ -1,73 +1,16 @@
 package org.yuzu.yuzu_emu.utils;
 
 import android.content.Intent;
-import android.net.Uri;
-import android.os.Environment;
-
-import androidx.annotation.Nullable;
 import androidx.fragment.app.FragmentActivity;
 
-import com.nononsenseapps.filepicker.FilePickerActivity;
-import com.nononsenseapps.filepicker.Utils;
-
-import org.yuzu.yuzu_emu.activities.CustomFilePickerActivity;
-
-import java.io.File;
-import java.util.List;
-
 public final class FileBrowserHelper {
-    public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title, List<String> extensions) {
-        Intent i = new Intent(activity, CustomFilePickerActivity.class);
-
-        i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false);
-        i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false);
-        i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR);
-        i.putExtra(FilePickerActivity.EXTRA_START_PATH,
-                Environment.getExternalStorageDirectory().getPath());
-        i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title);
-        i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions));
-
+    public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title) {
+        Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
+        i.putExtra(Intent.EXTRA_TITLE, title);
         activity.startActivityForResult(i, requestCode);
     }
 
-    public static void openFilePicker(FragmentActivity activity, int requestCode, int title,
-                                      List<String> extensions, boolean allowMultiple) {
-        Intent i = new Intent(activity, CustomFilePickerActivity.class);
-
-        i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, allowMultiple);
-        i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false);
-        i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE);
-        i.putExtra(FilePickerActivity.EXTRA_START_PATH,
-                Environment.getExternalStorageDirectory().getPath());
-        i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title);
-        i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions));
-
-        activity.startActivityForResult(i, requestCode);
-    }
-
-    @Nullable
     public static String getSelectedDirectory(Intent result) {
-        // Use the provided utility method to parse the result
-        List<Uri> files = Utils.getSelectedFilesFromResult(result);
-        if (!files.isEmpty()) {
-            File file = Utils.getFileForUri(files.get(0));
-            return file.getAbsolutePath();
-        }
-
-        return null;
-    }
-
-    @Nullable
-    public static String[] getSelectedFiles(Intent result) {
-        // Use the provided utility method to parse the result
-        List<Uri> files = Utils.getSelectedFilesFromResult(result);
-        if (!files.isEmpty()) {
-            String[] paths = new String[files.size()];
-            for (int i = 0; i < files.size(); i++)
-                paths[i] = Utils.getFileForUri(files.get(i)).getAbsolutePath();
-            return paths;
-        }
-
-        return null;
+        return result.getDataString();
     }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.java
index 11d06c7ee..624fd4a88 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.java
@@ -1,37 +1,261 @@
 package org.yuzu.yuzu_emu.utils;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsContract;
+
+import androidx.annotation.Nullable;
+import androidx.documentfile.provider.DocumentFile;
+
+import org.yuzu.yuzu_emu.model.MinimalDocumentFile;
+
 import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.List;
 
 public class FileUtil {
-    public static byte[] getBytesFromFile(File file) throws IOException {
-        final long length = file.length();
+    static final String PATH_TREE = "tree";
+    static final String DECODE_METHOD = "UTF-8";
+    static final String APPLICATION_OCTET_STREAM = "application/octet-stream";
+    static final String TEXT_PLAIN = "text/plain";
 
-        // You cannot create an array using a long type.
-        if (length > Integer.MAX_VALUE) {
-            // File is too large
-            throw new IOException("File is too large!");
+    /**
+     * Create a file from directory with filename.
+     * @param context Application context
+     * @param directory parent path for file.
+     * @param filename file display name.
+     * @return boolean
+     */
+    @Nullable
+    public static DocumentFile createFile(Context context, String directory, String filename) {
+        try {
+            Uri directoryUri = Uri.parse(directory);
+            DocumentFile parent = DocumentFile.fromTreeUri(context, directoryUri);
+            if (parent == null) return null;
+            filename = URLDecoder.decode(filename, DECODE_METHOD);
+            String mimeType = APPLICATION_OCTET_STREAM;
+            if (filename.endsWith(".txt")) {
+                mimeType = TEXT_PLAIN;
+            }
+            DocumentFile exists = parent.findFile(filename);
+            if (exists != null) return exists;
+            return parent.createFile(mimeType, filename);
+        } catch (Exception e) {
+            Log.error("[FileUtil]: Cannot create file, error: " + e.getMessage());
         }
+        return null;
+    }
 
-        byte[] bytes = new byte[(int) length];
+    /**
+     * Create a directory from directory with filename.
+     * @param context Application context
+     * @param directory parent path for directory.
+     * @param directoryName directory display name.
+     * @return boolean
+     */
+    @Nullable
+    public static DocumentFile createDir(Context context, String directory, String directoryName) {
+        try {
+            Uri directoryUri = Uri.parse(directory);
+            DocumentFile parent = DocumentFile.fromTreeUri(context, directoryUri);
+            if (parent == null) return null;
+            directoryName = URLDecoder.decode(directoryName, DECODE_METHOD);
+            DocumentFile isExist = parent.findFile(directoryName);
+            if (isExist != null) return isExist;
+            return parent.createDirectory(directoryName);
+        } catch (Exception e) {
+            Log.error("[FileUtil]: Cannot create file, error: " + e.getMessage());
+        }
+        return null;
+    }
 
-        int offset = 0;
-        int numRead;
+    /**
+     * Open content uri and return file descriptor to JNI.
+     * @param context Application context
+     * @param path Native content uri path
+     * @param openmode will be one of "r", "r", "rw", "wa", "rwa"
+     * @return file descriptor
+     */
+    public static int openContentUri(Context context, String path, String openmode) {
+        try {
+            Uri uri = Uri.parse(path);
+            ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, openmode);
+            if (parcelFileDescriptor == null) {
+                Log.error("[FileUtil]: Cannot get the file descriptor from uri: " + path);
+                return -1;
+            }
+            return parcelFileDescriptor.detachFd();
+        }
+        catch (Exception e) {
+            Log.error("[FileUtil]: Cannot open content uri, error: " + e.getMessage());
+        }
+        return -1;
+    }
 
-        try (InputStream is = new FileInputStream(file)) {
-            while (offset < bytes.length
-                    && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
-                offset += numRead;
+    /**
+     * Reference:  https://stackoverflow.com/questions/42186820/documentfile-is-very-slow
+     * This function will be faster than DoucmentFile.listFiles
+     * @param context Application context
+     * @param uri Directory uri.
+     * @return CheapDocument lists.
+     */
+    public static MinimalDocumentFile[] listFiles(Context context, Uri uri) {
+        final ContentResolver resolver = context.getContentResolver();
+        final String[] columns = new String[]{
+                DocumentsContract.Document.COLUMN_DOCUMENT_ID,
+                DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+                DocumentsContract.Document.COLUMN_MIME_TYPE,
+        };
+        Cursor c = null;
+        final List<MinimalDocumentFile> results = new ArrayList<>();
+        try {
+            String docId;
+            if (isRootTreeUri(uri)) {
+                docId = DocumentsContract.getTreeDocumentId(uri);
+            } else {
+                docId = DocumentsContract.getDocumentId(uri);
+            }
+            final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId);
+            c = resolver.query(childrenUri, columns, null, null, null);
+            while(c.moveToNext()) {
+                final String documentId = c.getString(0);
+                final String documentName = c.getString(1);
+                final String documentMimeType = c.getString(2);
+                final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId);
+                MinimalDocumentFile document = new MinimalDocumentFile(documentName, documentMimeType, documentUri);
+                results.add(document);
+            }
+        } catch (Exception e) {
+            Log.error("[FileUtil]: Cannot list file error: " + e.getMessage());
+        } finally {
+            closeQuietly(c);
+        }
+        return results.toArray(new MinimalDocumentFile[0]);
+    }
+
+    /**
+     * Check whether given path exists.
+     * @param path Native content uri path
+     * @return bool
+     */
+    public static boolean Exists(Context context, String path) {
+        Cursor c = null;
+        try {
+            Uri mUri = Uri.parse(path);
+            final String[] columns = new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID };
+            c = context.getContentResolver().query(mUri, columns, null, null, null);
+            return c.getCount() > 0;
+        } catch (Exception e) {
+            Log.info("[FileUtil] Cannot find file from given path, error: " + e.getMessage());
+        } finally {
+            closeQuietly(c);
+        }
+        return false;
+    }
+
+    /**
+     * Check whether given path is a directory
+     * @param path content uri path
+     * @return bool
+     */
+    public static boolean isDirectory(Context context, String path) {
+        final ContentResolver resolver = context.getContentResolver();
+        final String[] columns = new String[] {
+                DocumentsContract.Document.COLUMN_MIME_TYPE
+        };
+        boolean isDirectory = false;
+        Cursor c = null;
+        try {
+            Uri mUri = Uri.parse(path);
+            c = resolver.query(mUri, columns, null, null, null);
+            c.moveToNext();
+            final String mimeType = c.getString(0);
+            isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
+        } catch (Exception e) {
+            Log.error("[FileUtil]: Cannot list files, error: " + e.getMessage());
+        } finally {
+            closeQuietly(c);
+        }
+        return isDirectory;
+    }
+
+    /**
+     * Get file display name from given path
+     * @param path content uri path
+     * @return String display name
+     */
+    public static String getFilename(Context context, String path) {
+        final ContentResolver resolver = context.getContentResolver();
+        final String[] columns = new String[] {
+                DocumentsContract.Document.COLUMN_DISPLAY_NAME
+        };
+        String filename = "";
+        Cursor c = null;
+        try {
+            Uri mUri = Uri.parse(path);
+            c = resolver.query(mUri, columns, null, null, null);
+            c.moveToNext();
+            filename = c.getString(0);
+        } catch (Exception e) {
+            Log.error("[FileUtil]: Cannot get file size, error: " + e.getMessage());
+        } finally {
+            closeQuietly(c);
+        }
+        return filename;
+    }
+
+    public static String[] getFilesName(Context context, String path) {
+        Uri uri = Uri.parse(path);
+        List<String> files = new ArrayList<>();
+        for (MinimalDocumentFile file: FileUtil.listFiles(context, uri)) {
+            files.add(file.getFilename());
+        }
+        return files.toArray(new String[0]);
+    }
+
+    /**
+     * Get file size from given path.
+     * @param path content uri path
+     * @return long file size
+     */
+    public static long getFileSize(Context context, String path) {
+        final ContentResolver resolver = context.getContentResolver();
+        final String[] columns = new String[] {
+                DocumentsContract.Document.COLUMN_SIZE
+        };
+        long size = 0;
+        Cursor c =null;
+        try {
+            Uri mUri = Uri.parse(path);
+            c = resolver.query(mUri, columns, null, null, null);
+            c.moveToNext();
+            size = c.getLong(0);
+        } catch (Exception e) {
+            Log.error("[FileUtil]: Cannot get file size, error: " + e.getMessage());
+        } finally {
+            closeQuietly(c);
+        }
+        return size;
+    }
+
+    public static boolean isRootTreeUri(Uri uri) {
+        final List<String> paths = uri.getPathSegments();
+        return paths.size() == 2 && PATH_TREE.equals(paths.get(0));
+    }
+
+    public static void closeQuietly(AutoCloseable closeable) {
+        if (closeable != null) {
+            try {
+                closeable.close();
+            } catch (RuntimeException rethrown) {
+                throw rethrown;
+            } catch (Exception ignored) {
             }
         }
-
-        // Ensure all the bytes have been read in
-        if (offset < bytes.length) {
-            throw new IOException("Could not completely read file " + file.getName());
-        }
-
-        return bytes;
     }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PermissionsHandler.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PermissionsHandler.java
deleted file mode 100644
index 2eb200da4..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PermissionsHandler.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package org.yuzu.yuzu_emu.utils;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.Build;
-
-import androidx.core.content.ContextCompat;
-import androidx.fragment.app.FragmentActivity;
-
-import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
-
-public class PermissionsHandler {
-    public static final int REQUEST_CODE_WRITE_PERMISSION = 500;
-
-    // We use permissions acceptance as an indicator if this is a first boot for the user.
-    public static boolean isFirstBoot(final FragmentActivity activity) {
-        return ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED;
-    }
-
-    @TargetApi(Build.VERSION_CODES.M)
-    public static boolean checkWritePermission(final FragmentActivity activity) {
-        if (isFirstBoot(activity)) {
-            activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE},
-                    REQUEST_CODE_WRITE_PERMISSION);
-            return false;
-        }
-
-        return true;
-    }
-
-    public static boolean hasWriteAccess(Context context) {
-        return ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
-    }
-}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.java
index 5d22e8e08..6d3e58e18 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.java
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.java
@@ -1,44 +1,38 @@
 package org.yuzu.yuzu_emu.utils;
 
-import android.content.Intent;
-import android.os.Bundle;
-import android.text.TextUtils;
-
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
 import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.FragmentActivity;
 
 import org.yuzu.yuzu_emu.R;
-import org.yuzu.yuzu_emu.activities.EmulationActivity;
+import org.yuzu.yuzu_emu.YuzuApplication;
+import org.yuzu.yuzu_emu.ui.main.MainActivity;
+import org.yuzu.yuzu_emu.ui.main.MainPresenter;
 
 public final class StartupHandler {
-    private static void handlePermissionsCheck(FragmentActivity parent) {
-        // Ask the user to grant write permission if it's not already granted
-        PermissionsHandler.checkWritePermission(parent);
+    private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext());
 
-        String start_file = "";
-        Bundle extras = parent.getIntent().getExtras();
-        if (extras != null) {
-            start_file = extras.getString("AutoStartFile");
-        }
-
-        if (!TextUtils.isEmpty(start_file)) {
-            // Start the emulation activity, send the ISO passed in and finish the main activity
-            Intent emulation_intent = new Intent(parent, EmulationActivity.class);
-            emulation_intent.putExtra("SelectedGame", start_file);
-            parent.startActivity(emulation_intent);
-            parent.finish();
-        }
+    private static void handleStartupPromptDismiss(MainActivity parent) {
+        parent.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY);
     }
 
-    public static void HandleInit(FragmentActivity parent) {
-        if (PermissionsHandler.isFirstBoot(parent)) {
+    private static void markFirstBoot() {
+        final SharedPreferences.Editor editor = mPreferences.edit();
+        editor.putBoolean("FirstApplicationLaunch", false);
+        editor.apply();
+    }
+
+    public static void handleInit(MainActivity parent) {
+        if (mPreferences.getBoolean("FirstApplicationLaunch", true)) {
+            markFirstBoot();
+
             // Prompt user with standard first boot disclaimer
             new AlertDialog.Builder(parent)
                     .setTitle(R.string.app_name)
                     .setIcon(R.mipmap.ic_launcher)
                     .setMessage(parent.getResources().getString(R.string.app_disclaimer))
                     .setPositiveButton(android.R.string.ok, null)
-                    .setOnDismissListener(dialogInterface -> handlePermissionsCheck(parent))
+                    .setOnDismissListener(dialogInterface -> handleStartupPromptDismiss(parent))
                     .show();
         }
     }
diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp
index 326dab5fc..0a3cb9162 100644
--- a/src/android/app/src/main/jni/config.cpp
+++ b/src/android/app/src/main/jni/config.cpp
@@ -18,11 +18,8 @@
 
 namespace FS = Common::FS;
 
-const std::filesystem::path default_config_path =
-    FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "config.ini";
-
 Config::Config(std::optional<std::filesystem::path> config_path)
-    : config_loc{config_path.value_or(default_config_path)},
+    : config_loc{config_path.value_or(FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "config.ini")},
       config{std::make_unique<INIReader>(FS::PathToUTF8String(config_loc))} {
     Reload();
 }
@@ -66,8 +63,8 @@ void Config::ReadSetting(const std::string& group, Settings::Setting<bool>& sett
 
 template <typename Type, bool ranged>
 void Config::ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting) {
-    setting = static_cast<Type>(config->GetInteger(group, setting.GetLabel(),
-                                                        static_cast<long>(setting.GetDefault())));
+    setting = static_cast<Type>(
+        config->GetInteger(group, setting.GetLabel(), static_cast<long>(setting.GetDefault())));
 }
 
 void Config::ReadValues() {
@@ -93,9 +90,9 @@ void Config::ReadValues() {
         for (int i = 0; i < num_touch_from_button_maps; ++i) {
             Settings::TouchFromButtonMap map;
             map.name = config->Get("ControlsGeneral",
-                                        std::string("touch_from_button_maps_") + std::to_string(i) +
-                                            std::string("_name"),
-                                        "default");
+                                   std::string("touch_from_button_maps_") + std::to_string(i) +
+                                       std::string("_name"),
+                                   "default");
             const int num_touch_maps = config->GetInteger(
                 "ControlsGeneral",
                 std::string("touch_from_button_maps_") + std::to_string(i) + std::string("_count"),
@@ -105,9 +102,9 @@ void Config::ReadValues() {
             for (int j = 0; j < num_touch_maps; ++j) {
                 std::string touch_mapping =
                     config->Get("ControlsGeneral",
-                                     std::string("touch_from_button_maps_") + std::to_string(i) +
-                                         std::string("_bind_") + std::to_string(j),
-                                     "");
+                                std::string("touch_from_button_maps_") + std::to_string(i) +
+                                    std::string("_bind_") + std::to_string(j),
+                                "");
                 map.buttons.emplace_back(std::move(touch_mapping));
             }
 
@@ -127,16 +124,16 @@ void Config::ReadValues() {
     ReadSetting("Data Storage", Settings::values.use_virtual_sd);
     FS::SetYuzuPath(FS::YuzuPath::NANDDir,
                     config->Get("Data Storage", "nand_directory",
-                                     FS::GetYuzuPathString(FS::YuzuPath::NANDDir)));
+                                FS::GetYuzuPathString(FS::YuzuPath::NANDDir)));
     FS::SetYuzuPath(FS::YuzuPath::SDMCDir,
                     config->Get("Data Storage", "sdmc_directory",
-                                     FS::GetYuzuPathString(FS::YuzuPath::SDMCDir)));
+                                FS::GetYuzuPathString(FS::YuzuPath::SDMCDir)));
     FS::SetYuzuPath(FS::YuzuPath::LoadDir,
                     config->Get("Data Storage", "load_directory",
-                                     FS::GetYuzuPathString(FS::YuzuPath::LoadDir)));
+                                FS::GetYuzuPathString(FS::YuzuPath::LoadDir)));
     FS::SetYuzuPath(FS::YuzuPath::DumpDir,
                     config->Get("Data Storage", "dump_directory",
-                                     FS::GetYuzuPathString(FS::YuzuPath::DumpDir)));
+                                FS::GetYuzuPathString(FS::YuzuPath::DumpDir)));
     ReadSetting("Data Storage", Settings::values.gamecard_inserted);
     ReadSetting("Data Storage", Settings::values.gamecard_current_game);
     ReadSetting("Data Storage", Settings::values.gamecard_path);
diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp
index 2955122be..8f085798d 100644
--- a/src/android/app/src/main/jni/id_cache.cpp
+++ b/src/android/app/src/main/jni/id_cache.cpp
@@ -1,9 +1,17 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <jni.h>
+
+#include "common/fs/fs_android.h"
 #include "jni/id_cache.h"
 
 static JavaVM* s_java_vm;
 static jclass s_native_library_class;
 static jmethodID s_exit_emulation_activity;
 
+static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
+
 namespace IDCache {
 
 JNIEnv* GetEnvForThread() {
@@ -34,3 +42,41 @@ jmethodID GetExitEmulationActivity() {
 }
 
 } // namespace IDCache
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+jint JNI_OnLoad(JavaVM* vm, void* reserved) {
+    s_java_vm = vm;
+
+    JNIEnv* env;
+    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK)
+        return JNI_ERR;
+
+    // Initialize Java classes
+    const jclass native_library_class = env->FindClass("org/yuzu/yuzu_emu/NativeLibrary");
+    s_native_library_class = reinterpret_cast<jclass>(env->NewGlobalRef(native_library_class));
+    s_exit_emulation_activity =
+        env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
+
+    // Initialize Android Storage
+    Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
+
+    return JNI_VERSION;
+}
+
+void JNI_OnUnload(JavaVM* vm, void* reserved) {
+    JNIEnv* env;
+    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK) {
+        return;
+    }
+
+    // UnInitialize Android Storage
+    Common::FS::Android::UnRegisterCallbacks();
+    env->DeleteGlobalRef(s_native_library_class);
+}
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index f0df6cac1..c1880db46 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
 #include <codecvt>
 #include <locale>
 #include <string>
@@ -7,6 +10,7 @@
 #include <android/native_window_jni.h>
 
 #include "common/detached_tasks.h"
+#include "common/fs/path_util.h"
 #include "common/logging/backend.h"
 #include "common/logging/log.h"
 #include "common/microprofile.h"
@@ -257,9 +261,11 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_NotifyOrientationChange(JNIEnv* env,
                                                                    jint layout_option,
                                                                    jint rotation) {}
 
-void Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserDirectory([[maybe_unused]] JNIEnv* env,
-                                                            [[maybe_unused]] jclass clazz,
-                                                            [[maybe_unused]] jstring j_directory) {}
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_SetAppDirectory([[maybe_unused]] JNIEnv* env,
+                                                           [[maybe_unused]] jclass clazz,
+                                                           [[maybe_unused]] jstring j_directory) {
+    Common::FS::SetAppDirectory(GetJString(env, j_directory));
+}
 
 void Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation([[maybe_unused]] JNIEnv* env,
                                                             [[maybe_unused]] jclass clazz) {}
diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h
index 3b23f380b..fbe015b55 100644
--- a/src/android/app/src/main/jni/native.h
+++ b/src/android/app/src/main/jni/native.h
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
 #pragma once
 
 #include <jni.h>
@@ -8,16 +11,16 @@ extern "C" {
 #endif
 
 JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation(JNIEnv* env,
-                                                                                jclass clazz);
-
-JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation(JNIEnv* env,
                                                                               jclass clazz);
 
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation(JNIEnv* env,
+                                                                            jclass clazz);
+
 JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation(JNIEnv* env,
-                                                                             jclass clazz);
+                                                                           jclass clazz);
 
 JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning(JNIEnv* env,
-                                                                             jclass clazz);
+                                                                           jclass clazz);
 
 JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadEvent(
     JNIEnv* env, jclass clazz, jstring j_device, jint j_button, jint action);
@@ -29,61 +32,58 @@ JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadAxisEv
     JNIEnv* env, jclass clazz, jstring j_device, jint axis_id, jfloat axis_val);
 
 JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchEvent(JNIEnv* env,
-                                                                                jclass clazz,
-                                                                                jfloat x, jfloat y,
-                                                                                jboolean pressed);
+                                                                              jclass clazz,
+                                                                              jfloat x, jfloat y,
+                                                                              jboolean pressed);
 
-JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env,
-                                                                            jclass clazz, jfloat x,
-                                                                            jfloat y);
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz,
+                                                                          jfloat x, jfloat y);
 
-JNIEXPORT jintArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon(JNIEnv* env,
-                                                                            jclass clazz,
-                                                                            jstring j_file);
+JNIEXPORT jintArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon(JNIEnv* env, jclass clazz,
+                                                                          jstring j_file);
 
-JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle(JNIEnv* env,
+JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle(JNIEnv* env, jclass clazz,
+                                                                         jstring j_filename);
+
+JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDescription(JNIEnv* env,
+                                                                               jclass clazz,
+                                                                               jstring j_filename);
+
+JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGameId(JNIEnv* env, jclass clazz,
+                                                                          jstring j_filename);
+
+JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetRegions(JNIEnv* env,
                                                                            jclass clazz,
                                                                            jstring j_filename);
 
-JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDescription(
-    JNIEnv* env, jclass clazz, jstring j_filename);
-
-JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGameId(JNIEnv* env,
-                                                                            jclass clazz,
-                                                                            jstring j_filename);
-
-JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetRegions(JNIEnv* env,
-                                                                             jclass clazz,
-                                                                             jstring j_filename);
-
 JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetCompany(JNIEnv* env,
-                                                                             jclass clazz,
-                                                                             jstring j_filename);
+                                                                           jclass clazz,
+                                                                           jstring j_filename);
 
 JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGitRevision(JNIEnv* env,
-                                                                                 jclass clazz);
+                                                                               jclass clazz);
 
-JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserDirectory(
-    JNIEnv* env, jclass clazz, jstring j_directory);
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetAppDirectory(JNIEnv* env,
+                                                                             jclass clazz,
+                                                                             jstring j_directory);
 
 JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_SetSysDirectory(
     JNIEnv* env, jclass clazz, jstring path_);
 
 JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetSysDirectory(JNIEnv* env,
-                                                                               jclass clazz,
-                                                                               jstring path);
+                                                                             jclass clazz,
+                                                                             jstring path);
 
 JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env,
-                                                                                jclass clazz);
+                                                                              jclass clazz);
 
 JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env,
-                                                                              jclass clazz);
-JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetProfiling(JNIEnv* env,
-                                                                            jclass clazz,
-                                                                            jboolean enable);
+                                                                            jclass clazz);
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetProfiling(JNIEnv* env, jclass clazz,
+                                                                          jboolean enable);
 
 JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_WriteProfileResults(JNIEnv* env,
-                                                                                   jclass clazz);
+                                                                                 jclass clazz);
 
 JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_NotifyOrientationChange(
     JNIEnv* env, jclass clazz, jint layout_option, jint rotation);
@@ -96,18 +96,17 @@ Java_org_yuzu_yuzu_1emu_NativeLibrary_Run__Ljava_lang_String_2Ljava_lang_String_
     JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate);
 
 JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env,
-                                                                              jclass clazz,
-                                                                              jobject surf);
+                                                                            jclass clazz,
+                                                                            jobject surf);
 
 JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env,
-                                                                                jclass clazz);
+                                                                              jclass clazz);
 
-JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_InitGameIni(JNIEnv* env,
-                                                                           jclass clazz,
-                                                                           jstring j_game_id);
+JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_InitGameIni(JNIEnv* env, jclass clazz,
+                                                                         jstring j_game_id);
 
 JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadSettings(JNIEnv* env,
-                                                                              jclass clazz);
+                                                                            jclass clazz);
 
 JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserSetting(
     JNIEnv* env, jclass clazz, jstring j_game_id, jstring j_section, jstring j_key,
@@ -117,10 +116,10 @@ JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetUserSetting(
     JNIEnv* env, jclass clazz, jstring game_id, jstring section, jstring key);
 
 JNIEXPORT jdoubleArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetPerfStats(JNIEnv* env,
-                                                                                    jclass clazz);
+                                                                                  jclass clazz);
 
 JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env,
-                                                                             jclass clazz);
+                                                                           jclass clazz);
 
 #ifdef __cplusplus
 }
diff --git a/src/android/app/src/main/res/layout/filepicker_toolbar.xml b/src/android/app/src/main/res/layout/filepicker_toolbar.xml
deleted file mode 100644
index 644934171..000000000
--- a/src/android/app/src/main/res/layout/filepicker_toolbar.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/nnf_picker_toolbar"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:layout_alignParentTop="true"
-    android:background="?attr/colorPrimary"
-    android:minHeight="?attr/actionBarSize"
-    android:theme="?nnf_toolbarTheme">
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical">
-
-        <TextView
-            android:id="@+id/filepicker_title"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:ellipsize="start"
-            android:singleLine="true"
-            android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />
-
-        <TextView
-            android:id="@+id/nnf_current_dir"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:ellipsize="start"
-            android:singleLine="true"
-            android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle" />
-    </LinearLayout>
-</androidx.appcompat.widget.Toolbar>
diff --git a/src/android/app/src/main/res/values-night/styles_filepicker.xml b/src/android/app/src/main/res/values-night/styles_filepicker.xml
deleted file mode 100644
index 1a175cdcf..000000000
--- a/src/android/app/src/main/res/values-night/styles_filepicker.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-
-    <style name="FilePickerBaseTheme" parent="NNF_BaseTheme" />
-</resources>
diff --git a/src/android/app/src/main/res/values-w1050dp/dimens.xml b/src/android/app/src/main/res/values-w1050dp/dimens.xml
index 92fcb2b66..78481cb1c 100644
--- a/src/android/app/src/main/res/values-w1050dp/dimens.xml
+++ b/src/android/app/src/main/res/values-w1050dp/dimens.xml
@@ -2,5 +2,4 @@
 <resources>
     <!-- Example customization of dimensions originally defined in res/values/dimens.xml
          (such as screen margins) for screens with more than 1024dp of available width.  -->
-    <dimen name="activity_horizontal_margin">96dp</dimen>
 </resources>
diff --git a/src/android/app/src/main/res/values-w820dp/dimens.xml b/src/android/app/src/main/res/values-w820dp/dimens.xml
index d27181e85..1b1ada235 100644
--- a/src/android/app/src/main/res/values-w820dp/dimens.xml
+++ b/src/android/app/src/main/res/values-w820dp/dimens.xml
@@ -1,5 +1,4 @@
 <resources>
     <!-- Example customization of dimensions originally defined in res/values/dimens.xml
          (such as screen margins) for screens with more than 820dp of available width.  -->
-    <dimen name="activity_horizontal_margin">64dp</dimen>
 </resources>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index cc84f700e..893f6aa1a 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -48,7 +48,7 @@
     <string name="grid_menu_core_settings">Settings</string>
 
     <!-- Add Directory Screen-->
-    <string name="select_game_folder">Select Game Folder</string>
+    <string name="select_game_folder">Select game folder</string>
     <string name="install_cia_title">Install CIA</string>
 
     <!-- Preferences Screen -->
@@ -71,7 +71,6 @@
     <string name="emulation_touch_overlay_reset">Reset Overlay</string>
     <string name="emulation_close_game_message">Are you sure that you would like to close the current game?</string>
 
-    <string name="write_permission_needed">You need to allow write access to external storage for the emulator to work</string>
     <string name="load_settings">Loading Settings...</string>
 
     <string name="external_storage_not_mounted">The external storage needs to be available in order to use yuzu</string>
diff --git a/src/android/app/src/main/res/values/styles.xml b/src/android/app/src/main/res/values/styles.xml
index 62f24bad3..fdedc9b2e 100644
--- a/src/android/app/src/main/res/values/styles.xml
+++ b/src/android/app/src/main/res/values/styles.xml
@@ -61,22 +61,6 @@
         <item name="android:windowAllowReturnTransitionOverlap">true</item>
     </style>
 
-    <!-- Inherit from a base file picker theme that handles day/night -->
-    <style name="FilePickerTheme" parent="FilePickerBaseTheme">
-        <item name="colorSurface">@color/view_background</item>
-        <item name="colorOnSurface">@color/view_text</item>
-        <item name="colorPrimary">@color/citra_orange</item>
-        <item name="colorPrimaryDark">@color/citra_orange_dark</item>
-        <item name="colorAccent">@color/citra_accent</item>
-        <item name="android:windowBackground">@color/view_background</item>
-
-        <!-- Need to set this also to style create folder dialog -->
-        <item name="alertDialogTheme">@style/FilePickerAlertDialogTheme</item>
-
-        <item name="nnf_list_item_divider">@drawable/gamelist_divider</item>
-        <item name="nnf_toolbarTheme">@style/ThemeOverlay.AppCompat.DayNight.ActionBar</item>
-    </style>
-
     <style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.DayNight.Dialog.Alert">
         <item name="colorSurface">@color/view_background</item>
         <item name="colorOnSurface">@color/view_text</item>
diff --git a/src/android/app/src/main/res/values/styles_filepicker.xml b/src/android/app/src/main/res/values/styles_filepicker.xml
deleted file mode 100644
index 0b0c3fe1a..000000000
--- a/src/android/app/src/main/res/values/styles_filepicker.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<resources>
-
-    <style name="FilePickerBaseTheme" parent="NNF_BaseTheme.Light" />
-</resources>
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index 13ed68b3f..aecb46872 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -155,6 +155,14 @@ if (WIN32)
   target_link_libraries(common PRIVATE ntdll)
 endif()
 
+if(ANDROID)
+    target_sources(common
+        PRIVATE
+            fs/fs_android.cpp
+            fs/fs_android.h
+    )
+endif()
+
 if(ARCHITECTURE_x86_64)
     target_sources(common
         PRIVATE
diff --git a/src/common/fs/file.cpp b/src/common/fs/file.cpp
index 656b03cc5..b0b25eb43 100644
--- a/src/common/fs/file.cpp
+++ b/src/common/fs/file.cpp
@@ -5,6 +5,9 @@
 
 #include "common/fs/file.h"
 #include "common/fs/fs.h"
+#ifdef ANDROID
+#include "common/fs/fs_android.h"
+#endif
 #include "common/logging/log.h"
 
 #ifdef _WIN32
@@ -252,6 +255,23 @@ void IOFile::Open(const fs::path& path, FileAccessMode mode, FileType type, File
     } else {
         _wfopen_s(&file, path.c_str(), AccessModeToWStr(mode, type));
     }
+#elif ANDROID
+    if (Android::IsContentUri(path)) {
+        ASSERT_MSG(mode == FileAccessMode::Read, "Content URI file access is for read-only!");
+        const auto fd = Android::OpenContentUri(path, Android::OpenMode::Read);
+        if (fd != -1) {
+            file = fdopen(fd, "r");
+            const auto error_num = errno;
+            if (error_num != 0 && file == nullptr) {
+                LOG_ERROR(Common_Filesystem, "Error opening file: {}, error: {}", path.c_str(),
+                          strerror(error_num));
+            }
+        } else {
+            LOG_ERROR(Common_Filesystem, "Error opening file: {}", path.c_str());
+        }
+    } else {
+        file = std::fopen(path.c_str(), AccessModeToStr(mode, type));
+    }
 #else
     file = std::fopen(path.c_str(), AccessModeToStr(mode, type));
 #endif
@@ -372,6 +392,23 @@ u64 IOFile::GetSize() const {
     // Flush any unwritten buffered data into the file prior to retrieving the file size.
     std::fflush(file);
 
+#if ANDROID
+    u64 file_size = 0;
+    if (Android::IsContentUri(file_path)) {
+        file_size = Android::GetSize(file_path);
+    } else {
+        std::error_code ec;
+
+        file_size = fs::file_size(file_path, ec);
+
+        if (ec) {
+            LOG_ERROR(Common_Filesystem,
+                      "Failed to retrieve the file size of path={}, ec_message={}",
+                      PathToUTF8String(file_path), ec.message());
+            return 0;
+        }
+    }
+#else
     std::error_code ec;
 
     const auto file_size = fs::file_size(file_path, ec);
@@ -381,6 +418,7 @@ u64 IOFile::GetSize() const {
                   PathToUTF8String(file_path), ec.message());
         return 0;
     }
+#endif
 
     return file_size;
 }
diff --git a/src/common/fs/fs_android.cpp b/src/common/fs/fs_android.cpp
new file mode 100644
index 000000000..298a79bac
--- /dev/null
+++ b/src/common/fs/fs_android.cpp
@@ -0,0 +1,98 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "common/fs/fs_android.h"
+
+namespace Common::FS::Android {
+
+JNIEnv* GetEnvForThread() {
+    thread_local static struct OwnedEnv {
+        OwnedEnv() {
+            status = g_jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
+            if (status == JNI_EDETACHED)
+                g_jvm->AttachCurrentThread(&env, nullptr);
+        }
+
+        ~OwnedEnv() {
+            if (status == JNI_EDETACHED)
+                g_jvm->DetachCurrentThread();
+        }
+
+        int status;
+        JNIEnv* env = nullptr;
+    } owned;
+    return owned.env;
+}
+
+void RegisterCallbacks(JNIEnv* env, jclass clazz) {
+    env->GetJavaVM(&g_jvm);
+    native_library = clazz;
+
+#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature)                   \
+    F(JMethodID, JMethodName, Signature)
+#define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature)               \
+    F(JMethodID, JMethodName, Signature)
+#define F(JMethodID, JMethodName, Signature)                                                       \
+    JMethodID = env->GetStaticMethodID(native_library, JMethodName, Signature);
+    ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
+    ANDROID_STORAGE_FUNCTIONS(FS)
+#undef F
+#undef FS
+#undef FR
+}
+
+void UnRegisterCallbacks() {
+#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) F(JMethodID)
+#define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature) F(JMethodID)
+#define F(JMethodID) JMethodID = nullptr;
+    ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
+    ANDROID_STORAGE_FUNCTIONS(FS)
+#undef F
+#undef FS
+#undef FR
+}
+
+bool IsContentUri(const std::string& path) {
+    constexpr std::string_view prefix = "content://";
+    if (path.size() < prefix.size()) [[unlikely]] {
+        return false;
+    }
+
+    return path.find(prefix) == 0;
+}
+
+int OpenContentUri(const std::string& filepath, OpenMode openmode) {
+    if (open_content_uri == nullptr)
+        return -1;
+
+    const char* mode = "";
+    switch (openmode) {
+    case OpenMode::Read:
+        mode = "r";
+        break;
+    default:
+        UNIMPLEMENTED();
+        return -1;
+    }
+    auto env = GetEnvForThread();
+    jstring j_filepath = env->NewStringUTF(filepath.c_str());
+    jstring j_mode = env->NewStringUTF(mode);
+    return env->CallStaticIntMethod(native_library, open_content_uri, j_filepath, j_mode);
+}
+
+#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature)                   \
+    F(FunctionName, ReturnValue, JMethodID, Caller)
+#define F(FunctionName, ReturnValue, JMethodID, Caller)                                            \
+    ReturnValue FunctionName(const std::string& filepath) {                                        \
+        if (JMethodID == nullptr) {                                                                \
+            return 0;                                                                              \
+        }                                                                                          \
+        auto env = GetEnvForThread();                                                              \
+        jstring j_filepath = env->NewStringUTF(filepath.c_str());                                  \
+        return env->Caller(native_library, JMethodID, j_filepath);                                 \
+    }
+ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
+#undef F
+#undef FR
+
+} // namespace Common::FS::Android
diff --git a/src/common/fs/fs_android.h b/src/common/fs/fs_android.h
new file mode 100644
index 000000000..bb8a52648
--- /dev/null
+++ b/src/common/fs/fs_android.h
@@ -0,0 +1,62 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <string>
+#include <vector>
+#include <jni.h>
+
+#define ANDROID_STORAGE_FUNCTIONS(V)                                                               \
+    V(OpenContentUri, int, (const std::string& filepath, OpenMode openmode), open_content_uri,     \
+      "openContentUri", "(Ljava/lang/String;Ljava/lang/String;)I")
+
+#define ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(V)                                                 \
+    V(GetSize, std::uint64_t, get_size, CallStaticLongMethod, "getSize", "(Ljava/lang/String;)J")
+
+namespace Common::FS::Android {
+
+static JavaVM* g_jvm = nullptr;
+static jclass native_library = nullptr;
+
+#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) F(JMethodID)
+#define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature) F(JMethodID)
+#define F(JMethodID) static jmethodID JMethodID = nullptr;
+ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
+ANDROID_STORAGE_FUNCTIONS(FS)
+#undef F
+#undef FS
+#undef FR
+
+enum class OpenMode {
+    Read,
+    Write,
+    ReadWrite,
+    WriteAppend,
+    WriteTruncate,
+    ReadWriteAppend,
+    ReadWriteTruncate,
+    Never
+};
+
+void RegisterCallbacks(JNIEnv* env, jclass clazz);
+
+void UnRegisterCallbacks();
+
+bool IsContentUri(const std::string& path);
+
+#define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature)               \
+    F(FunctionName, Parameters, ReturnValue)
+#define F(FunctionName, Parameters, ReturnValue) ReturnValue FunctionName Parameters;
+ANDROID_STORAGE_FUNCTIONS(FS)
+#undef F
+#undef FS
+
+#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature)                   \
+    F(FunctionName, ReturnValue)
+#define F(FunctionName, ReturnValue) ReturnValue FunctionName(const std::string& filepath);
+ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
+#undef F
+#undef FR
+
+} // namespace Common::FS::Android
diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp
index ca755b053..e026a13d9 100644
--- a/src/common/fs/path_util.cpp
+++ b/src/common/fs/path_util.cpp
@@ -6,6 +6,9 @@
 #include <unordered_map>
 
 #include "common/fs/fs.h"
+#ifdef ANDROID
+#include "common/fs/fs_android.h"
+#endif
 #include "common/fs/fs_paths.h"
 #include "common/fs/path_util.h"
 #include "common/logging/log.h"
@@ -80,9 +83,7 @@ public:
         yuzu_paths.insert_or_assign(yuzu_path, new_path);
     }
 
-private:
-    PathManagerImpl() {
-        fs::path yuzu_path;
+    void Reinitialize(fs::path yuzu_path = {}) {
         fs::path yuzu_path_cache;
         fs::path yuzu_path_config;
 
@@ -96,12 +97,9 @@ private:
         yuzu_path_cache = yuzu_path / CACHE_DIR;
         yuzu_path_config = yuzu_path / CONFIG_DIR;
 #elif ANDROID
-        // On Android internal storage is mounted as "/sdcard"
-        if (Exists("/sdcard")) {
-            yuzu_path = "/sdcard/yuzu-emu";
-            yuzu_path_cache = yuzu_path / CACHE_DIR;
-            yuzu_path_config = yuzu_path / CONFIG_DIR;
-        }
+        ASSERT(!yuzu_path.empty());
+        yuzu_path_cache = yuzu_path / CACHE_DIR;
+        yuzu_path_config = yuzu_path / CONFIG_DIR;
 #else
         yuzu_path = GetCurrentDir() / PORTABLE_DIR;
 
@@ -129,6 +127,11 @@ private:
         GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR);
     }
 
+private:
+    PathManagerImpl() {
+        Reinitialize();
+    }
+
     ~PathManagerImpl() = default;
 
     void GenerateYuzuPath(YuzuPath yuzu_path, const fs::path& new_path) {
@@ -217,6 +220,10 @@ fs::path RemoveTrailingSeparators(const fs::path& path) {
     return fs::path{string_path};
 }
 
+void SetAppDirectory(const std::string& app_directory) {
+    PathManagerImpl::GetInstance().Reinitialize(app_directory);
+}
+
 const fs::path& GetYuzuPath(YuzuPath yuzu_path) {
     return PathManagerImpl::GetInstance().GetYuzuPathImpl(yuzu_path);
 }
@@ -357,6 +364,12 @@ std::vector<std::string> SplitPathComponents(std::string_view filename) {
 
 std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) {
     std::string path(path_);
+#ifdef ANDROID
+    if (Android::IsContentUri(path)) {
+        return path;
+    }
+#endif // ANDROID
+
     char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\';
     char type2 = directory_separator == DirectorySeparator::BackwardSlash ? '\\' : '/';
 
diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h
index 13d713f1e..7cfe85b70 100644
--- a/src/common/fs/path_util.h
+++ b/src/common/fs/path_util.h
@@ -180,6 +180,14 @@ template <typename Path>
 }
 #endif
 
+/**
+ * Sets the directory used for application storage. Used on Android where we do not know internal
+ * storage until informed by the frontend.
+ *
+ * @param app_directory Directory to use for application storage.
+ */
+void SetAppDirectory(const std::string& app_directory);
+
 /**
  * Gets the filesystem path associated with the YuzuPath enum.
  *