summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar bunnei2023-02-03 16:13:16 -0800
committerGravatar bunnei2023-06-03 00:05:29 -0700
commitef605f7d8f8241b95b977d95cf5247c1f2d8a309 (patch)
tree7e9dcc62168e23115d05119a5854d59544c89b8d
parentandroid: Harden emulation shutdown when loader fails. (diff)
downloadyuzu-ef605f7d8f8241b95b977d95cf5247c1f2d8a309.tar.gz
yuzu-ef605f7d8f8241b95b977d95cf5247c1f2d8a309.tar.xz
yuzu-ef605f7d8f8241b95b977d95cf5247c1f2d8a309.zip
android: Implement SAF support & migrate to SDK 31. (#4)
-rw-r--r--src/android/app/build.gradle6
-rw-r--r--src/android/app/src/main/AndroidManifest.xml13
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java22
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.java8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/CustomFilePickerActivity.java38
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java9
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.java6
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.java3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.java5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CustomFilePickerFragment.java120
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java52
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MinimalDocumentFile.java28
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java83
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.java4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.java132
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.java125
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileBrowserHelper.java65
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.java264
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PermissionsHandler.java35
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.java42
-rw-r--r--src/android/app/src/main/jni/config.cpp29
-rw-r--r--src/android/app/src/main/jni/id_cache.cpp46
-rw-r--r--src/android/app/src/main/jni/native.cpp12
-rw-r--r--src/android/app/src/main/jni/native.h89
-rw-r--r--src/android/app/src/main/res/layout/filepicker_toolbar.xml32
-rw-r--r--src/android/app/src/main/res/values-night/styles_filepicker.xml5
-rw-r--r--src/android/app/src/main/res/values-w1050dp/dimens.xml1
-rw-r--r--src/android/app/src/main/res/values-w820dp/dimens.xml1
-rw-r--r--src/android/app/src/main/res/values/strings.xml3
-rw-r--r--src/android/app/src/main/res/values/styles.xml16
-rw-r--r--src/android/app/src/main/res/values/styles_filepicker.xml5
-rw-r--r--src/common/CMakeLists.txt8
-rw-r--r--src/common/fs/file.cpp38
-rw-r--r--src/common/fs/fs_android.cpp98
-rw-r--r--src/common/fs/fs_android.h62
-rw-r--r--src/common/fs/path_util.cpp31
-rw-r--r--src/common/fs/path_util.h8
38 files changed, 851 insertions, 697 deletions
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 {
32 // TODO If this is ever modified, change application_id in strings.xml 32 // TODO If this is ever modified, change application_id in strings.xml
33 applicationId "org.yuzu.yuzu_emu" 33 applicationId "org.yuzu.yuzu_emu"
34 minSdkVersion 28 34 minSdkVersion 28
35 targetSdkVersion 29 35 targetSdkVersion 31
36 versionCode autoVersion 36 versionCode autoVersion
37 versionName getVersion() 37 versionName getVersion()
38 ndk.abiFilters abiFilter 38 ndk.abiFilters abiFilter
@@ -126,6 +126,7 @@ dependencies {
126 implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1' 126 implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
127 implementation 'androidx.fragment:fragment:1.5.3' 127 implementation 'androidx.fragment:fragment:1.5.3'
128 implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0" 128 implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
129 implementation "androidx.documentfile:documentfile:1.0.1"
129 implementation 'com.google.android.material:material:1.6.1' 130 implementation 'com.google.android.material:material:1.6.1'
130 131
131 // For loading huge screenshots from the disk. 132 // For loading huge screenshots from the disk.
@@ -138,9 +139,6 @@ dependencies {
138 implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 139 implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
139 implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' 140 implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
140 implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' 141 implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
141
142 // Please don't upgrade the billing library as the newer version is not GPL-compatible
143 implementation 'com.android.billingclient:billing:2.0.3'
144} 142}
145 143
146def getVersion() { 144def 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 @@
31 31
32 <activity 32 <activity
33 android:name="org.yuzu.yuzu_emu.ui.main.MainActivity" 33 android:name="org.yuzu.yuzu_emu.ui.main.MainActivity"
34 android:exported="true"
34 android:theme="@style/YuzuBase" 35 android:theme="@style/YuzuBase"
35 android:resizeableActivity="false"> 36 android:resizeableActivity="false">
36 37
@@ -57,18 +58,6 @@
57 58
58 <service android:name="org.yuzu.yuzu_emu.utils.ForegroundService"/> 59 <service android:name="org.yuzu.yuzu_emu.utils.ForegroundService"/>
59 60
60 <activity
61 android:name="org.yuzu.yuzu_emu.activities.CustomFilePickerActivity"
62 android:label="@string/app_name"
63 android:theme="@style/FilePickerTheme">
64 <intent-filter>
65 <action android:name="android.intent.action.GET_CONTENT" />
66 <category android:name="android.intent.category.DEFAULT" />
67 </intent-filter>
68 </activity>
69
70 <service android:name="org.yuzu.yuzu_emu.utils.DirectoryInitialization"/>
71
72 <provider 61 <provider
73 android:name="org.yuzu.yuzu_emu.model.GameProvider" 62 android:name="org.yuzu.yuzu_emu.model.GameProvider"
74 android:authorities="${applicationId}.provider" 63 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;
25import androidx.fragment.app.DialogFragment; 25import androidx.fragment.app.DialogFragment;
26 26
27import org.yuzu.yuzu_emu.activities.EmulationActivity; 27import org.yuzu.yuzu_emu.activities.EmulationActivity;
28import org.yuzu.yuzu_emu.utils.DocumentsTree;
28import org.yuzu.yuzu_emu.utils.EmulationMenuSettings; 29import org.yuzu.yuzu_emu.utils.EmulationMenuSettings;
30import org.yuzu.yuzu_emu.utils.FileUtil;
29import org.yuzu.yuzu_emu.utils.Log; 31import org.yuzu.yuzu_emu.utils.Log;
30 32
31import java.lang.ref.WeakReference; 33import java.lang.ref.WeakReference;
@@ -66,6 +68,20 @@ public final class NativeLibrary {
66 // Disallows instantiation. 68 // Disallows instantiation.
67 } 69 }
68 70
71 public static int openContentUri(String path, String openmode) {
72 if (DocumentsTree.isNativePath(path)) {
73 return YuzuApplication.documentsTree.openContentUri(path, openmode);
74 }
75 return FileUtil.openContentUri(YuzuApplication.getAppContext(), path, openmode);
76 }
77
78 public static long getSize(String path) {
79 if (DocumentsTree.isNativePath(path)) {
80 return YuzuApplication.documentsTree.getFileSize(path);
81 }
82 return FileUtil.getFileSize(YuzuApplication.getAppContext(), path);
83 }
84
69 /** 85 /**
70 * Handles button press events for a gamepad. 86 * Handles button press events for a gamepad.
71 * 87 *
@@ -147,11 +163,7 @@ public final class NativeLibrary {
147 163
148 public static native String GetGitRevision(); 164 public static native String GetGitRevision();
149 165
150 /** 166 public static native void SetAppDirectory(String directory);
151 * Sets the current working user directory
152 * If not set, it auto-detects a location
153 */
154 public static native void SetUserDirectory(String directory);
155 167
156 // Create the config.ini file. 168 // Create the config.ini file.
157 public static native void CreateConfigFile(); 169 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;
11import android.os.Build; 11import android.os.Build;
12 12
13import org.yuzu.yuzu_emu.model.GameDatabase; 13import org.yuzu.yuzu_emu.model.GameDatabase;
14import org.yuzu.yuzu_emu.utils.DocumentsTree;
14import org.yuzu.yuzu_emu.utils.DirectoryInitialization; 15import org.yuzu.yuzu_emu.utils.DirectoryInitialization;
15import org.yuzu.yuzu_emu.utils.PermissionsHandler;
16 16
17public class YuzuApplication extends Application { 17public class YuzuApplication extends Application {
18 public static GameDatabase databaseHelper; 18 public static GameDatabase databaseHelper;
19 public static DocumentsTree documentsTree;
19 private static YuzuApplication application; 20 private static YuzuApplication application;
20 21
21 private void createNotificationChannel() { 22 private void createNotificationChannel() {
@@ -39,10 +40,9 @@ public class YuzuApplication extends Application {
39 public void onCreate() { 40 public void onCreate() {
40 super.onCreate(); 41 super.onCreate();
41 application = this; 42 application = this;
43 documentsTree = new DocumentsTree();
42 44
43 if (PermissionsHandler.hasWriteAccess(getApplicationContext())) { 45 DirectoryInitialization.start(getApplicationContext());
44 DirectoryInitialization.start(getApplicationContext());
45 }
46 46
47 NativeLibrary.LogDeviceInfo(); 47 NativeLibrary.LogDeviceInfo();
48 createNotificationChannel(); 48 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 @@
1package org.yuzu.yuzu_emu.activities;
2
3import android.content.Intent;
4import android.os.Environment;
5
6import androidx.annotation.Nullable;
7
8import com.nononsenseapps.filepicker.AbstractFilePickerFragment;
9import com.nononsenseapps.filepicker.FilePickerActivity;
10
11import org.yuzu.yuzu_emu.fragments.CustomFilePickerFragment;
12
13import java.io.File;
14
15public class CustomFilePickerActivity extends FilePickerActivity {
16 public static final String EXTRA_TITLE = "filepicker.intent.TITLE";
17 public static final String EXTRA_EXTENSIONS = "filepicker.intent.EXTENSIONS";
18
19 @Override
20 protected AbstractFilePickerFragment<File> getFragment(
21 @Nullable final String startPath, final int mode, final boolean allowMultiple,
22 final boolean allowCreateDir, final boolean allowExistingFile,
23 final boolean singleClick) {
24 CustomFilePickerFragment fragment = new CustomFilePickerFragment();
25 // startPath is allowed to be null. In that case, default folder should be SD-card and not "/"
26 fragment.setArgs(
27 startPath != null ? startPath : Environment.getExternalStorageDirectory().getPath(),
28 mode, allowMultiple, allowCreateDir, allowExistingFile, singleClick);
29
30 Intent intent = getIntent();
31 int title = intent == null ? 0 : intent.getIntExtra(EXTRA_TITLE, 0);
32 fragment.setTitle(title);
33 String allowedExtensions = intent == null ? "*" : intent.getStringExtra(EXTRA_EXTENSIONS);
34 fragment.setAllowedExtensions(allowedExtensions);
35
36 return fragment;
37 }
38}
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;
16import androidx.fragment.app.FragmentActivity; 16import androidx.fragment.app.FragmentActivity;
17import androidx.recyclerview.widget.RecyclerView; 17import androidx.recyclerview.widget.RecyclerView;
18 18
19import org.yuzu.yuzu_emu.YuzuApplication;
19import org.yuzu.yuzu_emu.R; 20import org.yuzu.yuzu_emu.R;
20import org.yuzu.yuzu_emu.activities.EmulationActivity; 21import org.yuzu.yuzu_emu.activities.EmulationActivity;
21import org.yuzu.yuzu_emu.model.GameDatabase; 22import org.yuzu.yuzu_emu.model.GameDatabase;
22import org.yuzu.yuzu_emu.ui.DividerItemDecoration; 23import org.yuzu.yuzu_emu.ui.DividerItemDecoration;
24import org.yuzu.yuzu_emu.utils.FileUtil;
23import org.yuzu.yuzu_emu.utils.Log; 25import org.yuzu.yuzu_emu.utils.Log;
24import org.yuzu.yuzu_emu.utils.PicassoUtils; 26import org.yuzu.yuzu_emu.utils.PicassoUtils;
25import org.yuzu.yuzu_emu.viewholders.GameViewHolder; 27import org.yuzu.yuzu_emu.viewholders.GameViewHolder;
26 28
27import java.nio.file.Path;
28import java.nio.file.Paths;
29import java.util.stream.Stream; 29import java.util.stream.Stream;
30 30
31/** 31/**
@@ -88,8 +88,9 @@ public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> impl
88 holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " ")); 88 holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
89 holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY)); 89 holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
90 90
91 final Path gamePath = Paths.get(mCursor.getString(GameDatabase.GAME_COLUMN_PATH)); 91 String filepath = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
92 holder.textFileName.setText(gamePath.getFileName().toString()); 92 String filename = FileUtil.getFilename(YuzuApplication.getAppContext(), filepath);
93 holder.textFileName.setText(filename);
93 94
94 // TODO These shouldn't be necessary once the move to a DB-based model is complete. 95 // TODO These shouldn't be necessary once the move to a DB-based model is complete.
95 holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID); 96 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
@@ -160,12 +160,6 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
160 } 160 }
161 161
162 @Override 162 @Override
163 public void showPermissionNeededHint() {
164 Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
165 .show();
166 }
167
168 @Override
169 public void showExternalStorageNotMountedHint() { 163 public void showExternalStorageNotMountedHint() {
170 Toast.makeText(this, R.string.external_storage_not_mounted, Toast.LENGTH_SHORT) 164 Toast.makeText(this, R.string.external_storage_not_mounted, Toast.LENGTH_SHORT)
171 .show(); 165 .show();
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 {
78 if (directoryInitializationState == DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) { 78 if (directoryInitializationState == DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) {
79 mView.hideLoading(); 79 mView.hideLoading();
80 loadSettingsUI(); 80 loadSettingsUI();
81 } else if (directoryInitializationState == DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) {
82 mView.showPermissionNeededHint();
83 mView.hideLoading();
84 } else if (directoryInitializationState == DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) { 81 } else if (directoryInitializationState == DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
85 mView.showExternalStorageNotMountedHint(); 82 mView.showExternalStorageNotMountedHint();
86 mView.hideLoading(); 83 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
@@ -77,11 +77,6 @@ public interface SettingsActivityView {
77 void hideLoading(); 77 void hideLoading();
78 78
79 /** 79 /**
80 * Show a hint to the user that the app needs write to external storage access
81 */
82 void showPermissionNeededHint();
83
84 /**
85 * Show a hint to the user that the app needs the external storage to be mounted 80 * Show a hint to the user that the app needs the external storage to be mounted
86 */ 81 */
87 void showExternalStorageNotMountedHint(); 82 void showExternalStorageNotMountedHint();
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 @@
1package org.yuzu.yuzu_emu.fragments;
2
3import android.net.Uri;
4import android.os.Bundle;
5import android.os.Environment;
6import android.view.LayoutInflater;
7import android.view.View;
8import android.view.ViewGroup;
9import android.widget.TextView;
10
11import androidx.annotation.NonNull;
12import androidx.appcompat.widget.Toolbar;
13import androidx.core.content.FileProvider;
14
15import com.nononsenseapps.filepicker.FilePickerFragment;
16
17import org.yuzu.yuzu_emu.R;
18
19import java.io.File;
20import java.util.Arrays;
21import java.util.Collections;
22import java.util.List;
23
24public class CustomFilePickerFragment extends FilePickerFragment {
25 private static String ALL_FILES = "*";
26 private int mTitle;
27 private static List<String> extensions = Collections.singletonList(ALL_FILES);
28
29 @NonNull
30 @Override
31 public Uri toUri(@NonNull final File file) {
32 return FileProvider
33 .getUriForFile(getContext(),
34 getContext().getApplicationContext().getPackageName() + ".filesprovider",
35 file);
36 }
37
38 @Override
39 public void onActivityCreated(Bundle savedInstanceState) {
40 super.onActivityCreated(savedInstanceState);
41
42 if (mode == MODE_DIR) {
43 TextView ok = getActivity().findViewById(R.id.nnf_button_ok);
44 ok.setText(R.string.select_dir);
45
46 TextView cancel = getActivity().findViewById(R.id.nnf_button_cancel);
47 cancel.setVisibility(View.GONE);
48 }
49 }
50
51 @Override
52 protected View inflateRootView(LayoutInflater inflater, ViewGroup container) {
53 View view = super.inflateRootView(inflater, container);
54 if (mTitle != 0) {
55 Toolbar toolbar = view.findViewById(com.nononsenseapps.filepicker.R.id.nnf_picker_toolbar);
56 ViewGroup parent = (ViewGroup) toolbar.getParent();
57 int index = parent.indexOfChild(toolbar);
58 View newToolbar = inflater.inflate(R.layout.filepicker_toolbar, toolbar, false);
59 TextView title = newToolbar.findViewById(R.id.filepicker_title);
60 title.setText(mTitle);
61 parent.removeView(toolbar);
62 parent.addView(newToolbar, index);
63 }
64 return view;
65 }
66
67 public void setTitle(int title) {
68 mTitle = title;
69 }
70
71 public void setAllowedExtensions(String allowedExtensions) {
72 if (allowedExtensions == null)
73 return;
74
75 extensions = Arrays.asList(allowedExtensions.split(","));
76 }
77
78 @Override
79 protected boolean isItemVisible(@NonNull final File file) {
80 // Some users jump to the conclusion that Dolphin isn't able to detect their
81 // files if the files don't show up in the file picker when mode == MODE_DIR.
82 // To avoid this, show files even when the user needs to select a directory.
83 return (showHiddenItems || !file.isHidden()) &&
84 (file.isDirectory() || extensions.contains(ALL_FILES) ||
85 extensions.contains(fileExtension(file.getName()).toLowerCase()));
86 }
87
88 @Override
89 public boolean isCheckable(@NonNull final File file) {
90 // We need to make a small correction to the isCheckable logic due to
91 // overriding isItemVisible to show files when mode == MODE_DIR.
92 // AbstractFilePickerFragment always treats files as checkable when
93 // allowExistingFile == true, but we don't want files to be checkable when mode == MODE_DIR.
94 return super.isCheckable(file) && !(mode == MODE_DIR && file.isFile());
95 }
96
97 @Override
98 public void goUp() {
99 if (Environment.getExternalStorageDirectory().getPath().equals(mCurrentPath.getPath())) {
100 goToDir(new File("/storage/"));
101 return;
102 }
103 if (mCurrentPath.equals(new File("/storage/"))){
104 return;
105 }
106 super.goUp();
107 }
108
109 @Override
110 public void onClickDir(@NonNull View view, @NonNull DirViewHolder viewHolder) {
111 if(viewHolder.file.equals(new File("/storage/emulated/")))
112 viewHolder.file = new File("/storage/emulated/0/");
113 super.onClickDir(view, viewHolder);
114 }
115
116 private static String fileExtension(@NonNull String filename) {
117 int i = filename.lastIndexOf('.');
118 return i < 0 ? "" : filename.substring(i + 1);
119 }
120}
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
@@ -156,10 +156,6 @@ public final class EmulationFragment extends Fragment implements SurfaceHolder.C
156 DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) { 156 DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) {
157 mEmulationState.run(activity.isActivityRecreated()); 157 mEmulationState.run(activity.isActivityRecreated());
158 } else if (directoryInitializationState == 158 } else if (directoryInitializationState ==
159 DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) {
160 Toast.makeText(getContext(), R.string.write_permission_needed, Toast.LENGTH_SHORT)
161 .show();
162 } else if (directoryInitializationState ==
163 DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) { 159 DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
164 Toast.makeText(getContext(), R.string.external_storage_not_mounted, 160 Toast.makeText(getContext(), R.string.external_storage_not_mounted,
165 Toast.LENGTH_SHORT) 161 Toast.LENGTH_SHORT)
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;
5import android.database.Cursor; 5import android.database.Cursor;
6import android.database.sqlite.SQLiteDatabase; 6import android.database.sqlite.SQLiteDatabase;
7import android.database.sqlite.SQLiteOpenHelper; 7import android.database.sqlite.SQLiteOpenHelper;
8import android.net.Uri;
8 9
9import org.yuzu.yuzu_emu.NativeLibrary; 10import org.yuzu.yuzu_emu.NativeLibrary;
11import org.yuzu.yuzu_emu.utils.FileUtil;
10import org.yuzu.yuzu_emu.utils.Log; 12import org.yuzu.yuzu_emu.utils.Log;
11 13
12import java.io.File; 14import java.io.File;
@@ -63,10 +65,12 @@ public final class GameDatabase extends SQLiteOpenHelper {
63 65
64 private static final String SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS; 66 private static final String SQL_DELETE_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS;
65 private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES; 67 private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES;
68 private final Context context;
66 69
67 public GameDatabase(Context context) { 70 public GameDatabase(Context context) {
68 // Superclass constructor builds a database or uses an existing one. 71 // Superclass constructor builds a database or uses an existing one.
69 super(context, "games.db", null, DB_VERSION); 72 super(context, "games.db", null, DB_VERSION);
73 this.context = context;
70 } 74 }
71 75
72 @Override 76 @Override
@@ -123,8 +127,6 @@ public final class GameDatabase extends SQLiteOpenHelper {
123 File game = new File(gamePath); 127 File game = new File(gamePath);
124 128
125 if (!game.exists()) { 129 if (!game.exists()) {
126 Log.error("[GameDatabase] Game file no longer exists. Removing from the library: " +
127 gamePath);
128 database.delete(TABLE_NAME_GAMES, 130 database.delete(TABLE_NAME_GAMES,
129 KEY_DB_ID + " = ?", 131 KEY_DB_ID + " = ?",
130 new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))}); 132 new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))});
@@ -150,9 +152,9 @@ public final class GameDatabase extends SQLiteOpenHelper {
150 while (folderCursor.moveToNext()) { 152 while (folderCursor.moveToNext()) {
151 String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH); 153 String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH);
152 154
153 File folder = new File(folderPath); 155 Uri folderUri = Uri.parse(folderPath);
154 // If the folder is empty because it no longer exists, remove it from the library. 156 // If the folder is empty because it no longer exists, remove it from the library.
155 if (!folder.exists()) { 157 if (FileUtil.listFiles(context, folderUri).length == 0) {
156 Log.error( 158 Log.error(
157 "[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath); 159 "[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath);
158 database.delete(TABLE_NAME_FOLDERS, 160 database.delete(TABLE_NAME_FOLDERS,
@@ -160,7 +162,7 @@ public final class GameDatabase extends SQLiteOpenHelper {
160 new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))}); 162 new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))});
161 } 163 }
162 164
163 addGamesRecursive(database, folder, allowedExtensions, 3); 165 this.addGamesRecursive(database, folderUri, allowedExtensions, 3);
164 } 166 }
165 167
166 fileCursor.close(); 168 fileCursor.close();
@@ -169,33 +171,27 @@ public final class GameDatabase extends SQLiteOpenHelper {
169 database.close(); 171 database.close();
170 } 172 }
171 173
172 private static void addGamesRecursive(SQLiteDatabase database, File parent, Set<String> allowedExtensions, int depth) { 174 private void addGamesRecursive(SQLiteDatabase database, Uri parent, Set<String> allowedExtensions, int depth) {
173 if (depth <= 0) { 175 if (depth <= 0) {
174 return; 176 return;
175 } 177 }
176 178
177 File[] children = parent.listFiles(); 179 MinimalDocumentFile[] children = FileUtil.listFiles(context, parent);
178 if (children != null) { 180 for (MinimalDocumentFile file : children) {
179 for (File file : children) { 181 if (file.isDirectory()) {
180 if (file.isHidden()) { 182 Set<String> newExtensions = new HashSet<>(Arrays.asList(
181 continue; 183 ".xci", ".nsp", ".nca", ".nro"));
182 } 184 this.addGamesRecursive(database, file.getUri(), newExtensions, depth - 1);
183 185 } else {
184 if (file.isDirectory()) { 186 String filename = file.getUri().toString();
185 Set<String> newExtensions = new HashSet<>(Arrays.asList( 187
186 ".xci", ".nsp", ".nca", ".nro")); 188 int extensionStart = filename.lastIndexOf('.');
187 addGamesRecursive(database, file, newExtensions, depth - 1); 189 if (extensionStart > 0) {
188 } else { 190 String fileExtension = filename.substring(extensionStart);
189 String filePath = file.getPath(); 191
190 192 // Check that the file has an extension we care about before trying to read out of it.
191 int extensionStart = filePath.lastIndexOf('.'); 193 if (allowedExtensions.contains(fileExtension.toLowerCase())) {
192 if (extensionStart > 0) { 194 attemptToAddGame(database, filename);
193 String fileExtension = filePath.substring(extensionStart);
194
195 // Check that the file has an extension we care about before trying to read out of it.
196 if (allowedExtensions.contains(fileExtension.toLowerCase())) {
197 attemptToAddGame(database, filePath);
198 }
199 } 195 }
200 } 196 }
201 } 197 }
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 @@
1package org.yuzu.yuzu_emu.model;
2
3import android.net.Uri;
4import android.provider.DocumentsContract;
5
6public class MinimalDocumentFile {
7 private final String filename;
8 private final Uri uri;
9 private final String mimeType;
10
11 public MinimalDocumentFile(String filename, String mimeType, Uri uri) {
12 this.filename = filename;
13 this.mimeType = mimeType;
14 this.uri = uri;
15 }
16
17 public String getFilename() {
18 return filename;
19 }
20
21 public Uri getUri() {
22 return uri;
23 }
24
25 public boolean isDirectory() {
26 return mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
27 }
28}
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 @@
1package org.yuzu.yuzu_emu.ui.main; 1package org.yuzu.yuzu_emu.ui.main;
2 2
3import android.content.Intent; 3import android.content.Intent;
4import android.content.pm.PackageManager; 4import android.net.Uri;
5import android.os.Bundle; 5import android.os.Bundle;
6import android.view.Menu; 6import android.view.Menu;
7import android.view.MenuInflater; 7import android.view.MenuInflater;
8import android.view.MenuItem; 8import android.view.MenuItem;
9import android.widget.Toast;
10 9
11import androidx.annotation.NonNull; 10import androidx.annotation.NonNull;
12import androidx.appcompat.app.AppCompatActivity; 11import androidx.appcompat.app.AppCompatActivity;
@@ -18,16 +17,11 @@ import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity;
18import org.yuzu.yuzu_emu.model.GameProvider; 17import org.yuzu.yuzu_emu.model.GameProvider;
19import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment; 18import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment;
20import org.yuzu.yuzu_emu.utils.AddDirectoryHelper; 19import org.yuzu.yuzu_emu.utils.AddDirectoryHelper;
21import org.yuzu.yuzu_emu.utils.DirectoryInitialization;
22import org.yuzu.yuzu_emu.utils.FileBrowserHelper; 20import org.yuzu.yuzu_emu.utils.FileBrowserHelper;
23import org.yuzu.yuzu_emu.utils.PermissionsHandler;
24import org.yuzu.yuzu_emu.utils.PicassoUtils; 21import org.yuzu.yuzu_emu.utils.PicassoUtils;
25import org.yuzu.yuzu_emu.utils.StartupHandler; 22import org.yuzu.yuzu_emu.utils.StartupHandler;
26import org.yuzu.yuzu_emu.utils.ThemeUtil; 23import org.yuzu.yuzu_emu.utils.ThemeUtil;
27 24
28import java.util.Arrays;
29import java.util.Collections;
30
31/** 25/**
32 * The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which 26 * The main Activity of the Lollipop style UI. Manages several PlatformGamesFragments, which
33 * individually display a grid of available games for each Fragment, in a tabbed layout. 27 * 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 {
54 mPresenter.onCreate(); 48 mPresenter.onCreate();
55 49
56 if (savedInstanceState == null) { 50 if (savedInstanceState == null) {
57 StartupHandler.HandleInit(this); 51 StartupHandler.handleInit(this);
58 if (PermissionsHandler.hasWriteAccess(this)) { 52 mPlatformGamesFragment = new PlatformGamesFragment();
59 mPlatformGamesFragment = new PlatformGamesFragment(); 53 getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment).commit();
60 getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
61 .commit();
62 }
63 } else { 54 } else {
64 mPlatformGamesFragment = (PlatformGamesFragment) getSupportFragmentManager().getFragment(savedInstanceState, "mPlatformGamesFragment"); 55 mPlatformGamesFragment = (PlatformGamesFragment) getSupportFragmentManager().getFragment(savedInstanceState, "mPlatformGamesFragment");
65 } 56 }
@@ -72,15 +63,13 @@ public final class MainActivity extends AppCompatActivity implements MainView {
72 @Override 63 @Override
73 protected void onSaveInstanceState(@NonNull Bundle outState) { 64 protected void onSaveInstanceState(@NonNull Bundle outState) {
74 super.onSaveInstanceState(outState); 65 super.onSaveInstanceState(outState);
75 if (PermissionsHandler.hasWriteAccess(this)) { 66 if (getSupportFragmentManager() == null) {
76 if (getSupportFragmentManager() == null) { 67 return;
77 return; 68 }
78 } 69 if (outState == null) {
79 if (outState == null) { 70 return;
80 return;
81 }
82 getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment);
83 } 71 }
72 getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment);
84 } 73 }
85 74
86 @Override 75 @Override
@@ -119,27 +108,17 @@ public final class MainActivity extends AppCompatActivity implements MainView {
119 108
120 @Override 109 @Override
121 public void launchSettingsActivity(String menuTag) { 110 public void launchSettingsActivity(String menuTag) {
122 if (PermissionsHandler.hasWriteAccess(this)) { 111 SettingsActivity.launch(this, menuTag, "");
123 SettingsActivity.launch(this, menuTag, "");
124 } else {
125 PermissionsHandler.checkWritePermission(this);
126 }
127 } 112 }
128 113
129 @Override 114 @Override
130 public void launchFileListActivity(int request) { 115 public void launchFileListActivity(int request) {
131 if (PermissionsHandler.hasWriteAccess(this)) { 116 switch (request) {
132 switch (request) { 117 case MainPresenter.REQUEST_ADD_DIRECTORY:
133 case MainPresenter.REQUEST_ADD_DIRECTORY: 118 FileBrowserHelper.openDirectoryPicker(this,
134 FileBrowserHelper.openDirectoryPicker(this, 119 MainPresenter.REQUEST_ADD_DIRECTORY,
135 MainPresenter.REQUEST_ADD_DIRECTORY, 120 R.string.select_game_folder);
136 R.string.select_game_folder, 121 break;
137 Arrays.asList("nso", "nro", "nca", "xci",
138 "nsp", "kip"));
139 break;
140 }
141 } else {
142 PermissionsHandler.checkWritePermission(this);
143 } 122 }
144 } 123 }
145 124
@@ -155,6 +134,8 @@ public final class MainActivity extends AppCompatActivity implements MainView {
155 case MainPresenter.REQUEST_ADD_DIRECTORY: 134 case MainPresenter.REQUEST_ADD_DIRECTORY:
156 // If the user picked a file, as opposed to just backing out. 135 // If the user picked a file, as opposed to just backing out.
157 if (resultCode == MainActivity.RESULT_OK) { 136 if (resultCode == MainActivity.RESULT_OK) {
137 int takeFlags = (Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
138 getContentResolver().takePersistableUriPermission(Uri.parse(result.getDataString()), takeFlags);
158 // When a new directory is picked, we currently will reset the existing games 139 // When a new directory is picked, we currently will reset the existing games
159 // database. This effectively means that only one game directory is supported. 140 // database. This effectively means that only one game directory is supported.
160 // TODO(bunnei): Consider fixing this in the future, or removing code for this. 141 // 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 {
166 } 147 }
167 } 148 }
168 149
169 @Override
170 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
171 switch (requestCode) {
172 case PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION:
173 if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
174 DirectoryInitialization.start(this);
175
176 mPlatformGamesFragment = new PlatformGamesFragment();
177 getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
178 .commit();
179
180 // Immediately prompt user to select a game directory on first boot
181 if (mPresenter != null) {
182 mPresenter.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY);
183 }
184 } else {
185 Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
186 .show();
187 }
188 break;
189 default:
190 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
191 break;
192 }
193 }
194
195 /** 150 /**
196 * Called by the framework whenever any actionbar/toolbar icon is clicked. 151 * Called by the framework whenever any actionbar/toolbar icon is clicked.
197 * 152 *
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 {
22 public void onCreate() { 22 public void onCreate() {
23 String versionName = BuildConfig.VERSION_NAME; 23 String versionName = BuildConfig.VERSION_NAME;
24 mView.setVersionString(versionName); 24 mView.setVersionString(versionName);
25 refeshGameList(); 25 refreshGameList();
26 } 26 }
27 27
28 public void launchFileListActivity(int request) { 28 public void launchFileListActivity(int request) {
@@ -63,7 +63,7 @@ public final class MainPresenter {
63 mDirToAdd = dir; 63 mDirToAdd = dir;
64 } 64 }
65 65
66 public void refeshGameList() { 66 public void refreshGameList() {
67 GameDatabase databaseHelper = YuzuApplication.databaseHelper; 67 GameDatabase databaseHelper = YuzuApplication.databaseHelper;
68 databaseHelper.scanLibrary(databaseHelper.getWritableDatabase()); 68 databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
69 mView.refresh(); 69 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 @@
1/**
2 * Copyright 2014 Dolphin Emulator Project
3 * Licensed under GPLv2+
4 * Refer to the license.txt file included.
5 */
6
7package org.yuzu.yuzu_emu.utils; 1package org.yuzu.yuzu_emu.utils;
8 2
9import android.content.Context; 3import android.content.Context;
10import android.content.Intent; 4import android.content.Intent;
11import android.content.SharedPreferences;
12import android.os.Environment;
13import android.preference.PreferenceManager;
14
15import androidx.localbroadcastmanager.content.LocalBroadcastManager; 5import androidx.localbroadcastmanager.content.LocalBroadcastManager;
16 6
17import org.yuzu.yuzu_emu.NativeLibrary; 7import org.yuzu.yuzu_emu.NativeLibrary;
18 8
19import java.io.File;
20import java.io.FileOutputStream;
21import java.io.IOException; 9import java.io.IOException;
22import java.io.InputStream;
23import java.io.OutputStream;
24import java.util.concurrent.atomic.AtomicBoolean; 10import java.util.concurrent.atomic.AtomicBoolean;
25 11
26/**
27 * A service that spawns its own thread in order to copy several binary and shader files
28 * from the yuzu APK to the external file system.
29 */
30public final class DirectoryInitialization { 12public final class DirectoryInitialization {
31 public static final String BROADCAST_ACTION = "org.yuzu.yuzu_emu.BROADCAST"; 13 public static final String BROADCAST_ACTION = "org.yuzu.yuzu_emu.BROADCAST";
32
33 public static final String EXTRA_STATE = "directoryState"; 14 public static final String EXTRA_STATE = "directoryState";
34 private static volatile DirectoryInitializationState directoryState = null; 15 private static volatile DirectoryInitializationState directoryState = null;
35 private static String userPath; 16 private static String userPath;
@@ -37,7 +18,6 @@ public final class DirectoryInitialization {
37 18
38 public static void start(Context context) { 19 public static void start(Context context) {
39 // Can take a few seconds to run, so don't block UI thread. 20 // Can take a few seconds to run, so don't block UI thread.
40 //noinspection TrivialFunctionalExpressionUsage
41 ((Runnable) () -> init(context)).run(); 21 ((Runnable) () -> init(context)).run();
42 } 22 }
43 23
@@ -46,31 +26,15 @@ public final class DirectoryInitialization {
46 return; 26 return;
47 27
48 if (directoryState != DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) { 28 if (directoryState != DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) {
49 if (PermissionsHandler.hasWriteAccess(context)) { 29 initializeInternalStorage(context);
50 if (setUserDirectory()) { 30 NativeLibrary.CreateConfigFile();
51 initializeInternalStorage(context); 31 directoryState = DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED;
52 NativeLibrary.CreateConfigFile();
53 directoryState = DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED;
54 } else {
55 directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE;
56 }
57 } else {
58 directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED;
59 }
60 } 32 }
61 33
62 isDirectoryInitializationRunning.set(false); 34 isDirectoryInitializationRunning.set(false);
63 sendBroadcastState(directoryState, context); 35 sendBroadcastState(directoryState, context);
64 } 36 }
65 37
66 private static void deleteDirectoryRecursively(File file) {
67 if (file.isDirectory()) {
68 for (File child : file.listFiles())
69 deleteDirectoryRecursively(child);
70 }
71 file.delete();
72 }
73
74 public static boolean areDirectoriesReady() { 38 public static boolean areDirectoriesReady() {
75 return directoryState == DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED; 39 return directoryState == DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED;
76 } 40 }
@@ -85,41 +49,13 @@ public final class DirectoryInitialization {
85 return userPath; 49 return userPath;
86 } 50 }
87 51
88 private static native void SetSysDirectory(String path); 52 public static void initializeInternalStorage(Context context) {
89 53 try {
90 private static boolean setUserDirectory() { 54 userPath = context.getExternalFilesDir(null).getCanonicalPath();
91 if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { 55 NativeLibrary.SetAppDirectory(userPath);
92 File externalPath = Environment.getExternalStorageDirectory(); 56 } catch(IOException e) {
93 if (externalPath != null) { 57 e.printStackTrace();
94 userPath = externalPath.getAbsolutePath() + "/yuzu-emu";
95 Log.debug("[DirectoryInitialization] User Dir: " + userPath);
96 // NativeLibrary.SetUserDirectory(userPath);
97 return true;
98 }
99
100 }
101
102 return false;
103 }
104
105 private static void initializeInternalStorage(Context context) {
106 File sysDirectory = new File(context.getFilesDir(), "Sys");
107
108 SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
109 String revision = NativeLibrary.GetGitRevision();
110 if (!preferences.getString("sysDirectoryVersion", "").equals(revision)) {
111 // There is no extracted Sys directory, or there is a Sys directory from another
112 // version of yuzu that might contain outdated files. Let's (re-)extract Sys.
113 deleteDirectoryRecursively(sysDirectory);
114 copyAssetFolder("Sys", sysDirectory, true, context);
115
116 SharedPreferences.Editor editor = preferences.edit();
117 editor.putString("sysDirectoryVersion", revision);
118 editor.apply();
119 } 58 }
120
121 // Let the native code know where the Sys directory is.
122 SetSysDirectory(sysDirectory.getPath());
123 } 59 }
124 60
125 private static void sendBroadcastState(DirectoryInitializationState state, Context context) { 61 private static void sendBroadcastState(DirectoryInitializationState state, Context context) {
@@ -129,58 +65,8 @@ public final class DirectoryInitialization {
129 LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent); 65 LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent);
130 } 66 }
131 67
132 private static void copyAsset(String asset, File output, Boolean overwrite, Context context) {
133 Log.verbose("[DirectoryInitialization] Copying File " + asset + " to " + output);
134
135 try {
136 if (!output.exists() || overwrite) {
137 InputStream in = context.getAssets().open(asset);
138 OutputStream out = new FileOutputStream(output);
139 copyFile(in, out);
140 in.close();
141 out.close();
142 }
143 } catch (IOException e) {
144 Log.error("[DirectoryInitialization] Failed to copy asset file: " + asset +
145 e.getMessage());
146 }
147 }
148
149 private static void copyAssetFolder(String assetFolder, File outputFolder, Boolean overwrite,
150 Context context) {
151 Log.verbose("[DirectoryInitialization] Copying Folder " + assetFolder + " to " +
152 outputFolder);
153
154 try {
155 boolean createdFolder = false;
156 for (String file : context.getAssets().list(assetFolder)) {
157 if (!createdFolder) {
158 outputFolder.mkdir();
159 createdFolder = true;
160 }
161 copyAssetFolder(assetFolder + File.separator + file, new File(outputFolder, file),
162 overwrite, context);
163 copyAsset(assetFolder + File.separator + file, new File(outputFolder, file), overwrite,
164 context);
165 }
166 } catch (IOException e) {
167 Log.error("[DirectoryInitialization] Failed to copy asset folder: " + assetFolder +
168 e.getMessage());
169 }
170 }
171
172 private static void copyFile(InputStream in, OutputStream out) throws IOException {
173 byte[] buffer = new byte[1024];
174 int read;
175
176 while ((read = in.read(buffer)) != -1) {
177 out.write(buffer, 0, read);
178 }
179 }
180
181 public enum DirectoryInitializationState { 68 public enum DirectoryInitializationState {
182 YUZU_DIRECTORIES_INITIALIZED, 69 YUZU_DIRECTORIES_INITIALIZED,
183 EXTERNAL_STORAGE_PERMISSION_NEEDED,
184 CANT_FIND_EXTERNAL_STORAGE 70 CANT_FIND_EXTERNAL_STORAGE
185 } 71 }
186} 72}
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 @@
1package org.yuzu.yuzu_emu.utils;
2
3import android.content.Context;
4import android.net.Uri;
5
6import androidx.annotation.Nullable;
7import androidx.documentfile.provider.DocumentFile;
8
9import org.yuzu.yuzu_emu.YuzuApplication;
10import org.yuzu.yuzu_emu.model.MinimalDocumentFile;
11
12import java.util.HashMap;
13import java.util.Map;
14import java.util.StringTokenizer;
15
16public class DocumentsTree {
17 private DocumentsNode root;
18 private final Context context;
19 public static final String DELIMITER = "/";
20
21 public DocumentsTree() {
22 context = YuzuApplication.getAppContext();
23 }
24
25 public void setRoot(Uri rootUri) {
26 root = null;
27 root = new DocumentsNode();
28 root.uri = rootUri;
29 root.isDirectory = true;
30 }
31
32 public int openContentUri(String filepath, String openmode) {
33 DocumentsNode node = resolvePath(filepath);
34 if (node == null) {
35 return -1;
36 }
37 return FileUtil.openContentUri(context, node.uri.toString(), openmode);
38 }
39
40 public long getFileSize(String filepath) {
41 DocumentsNode node = resolvePath(filepath);
42 if (node == null || node.isDirectory) {
43 return 0;
44 }
45 return FileUtil.getFileSize(context, node.uri.toString());
46 }
47
48 public boolean Exists(String filepath) {
49 return resolvePath(filepath) != null;
50 }
51
52 @Nullable
53 private DocumentsNode resolvePath(String filepath) {
54 StringTokenizer tokens = new StringTokenizer(filepath, DELIMITER, false);
55 DocumentsNode iterator = root;
56 while (tokens.hasMoreTokens()) {
57 String token = tokens.nextToken();
58 if (token.isEmpty()) continue;
59 iterator = find(iterator, token);
60 if (iterator == null) return null;
61 }
62 return iterator;
63 }
64
65 @Nullable
66 private DocumentsNode find(DocumentsNode parent, String filename) {
67 if (parent.isDirectory && !parent.loaded) {
68 structTree(parent);
69 }
70 return parent.children.get(filename);
71 }
72
73 /**
74 * Construct current level directory tree
75 * @param parent parent node of this level
76 */
77 private void structTree(DocumentsNode parent) {
78 MinimalDocumentFile[] documents = FileUtil.listFiles(context, parent.uri);
79 for (MinimalDocumentFile document: documents) {
80 DocumentsNode node = new DocumentsNode(document);
81 node.parent = parent;
82 parent.children.put(node.name, node);
83 }
84 parent.loaded = true;
85 }
86
87 public static boolean isNativePath(String path) {
88 if (path.length() > 0) {
89 return path.charAt(0) == '/';
90 }
91 return false;
92 }
93
94 private static class DocumentsNode {
95 private DocumentsNode parent;
96 private final Map<String, DocumentsNode> children = new HashMap<>();
97 private String name;
98 private Uri uri;
99 private boolean loaded = false;
100 private boolean isDirectory = false;
101
102 private DocumentsNode() {}
103 private DocumentsNode(MinimalDocumentFile document) {
104 name = document.getFilename();
105 uri = document.getUri();
106 isDirectory = document.isDirectory();
107 loaded = !isDirectory;
108 }
109 private DocumentsNode(DocumentFile document, boolean isCreateDir) {
110 name = document.getName();
111 uri = document.getUri();
112 isDirectory = isCreateDir;
113 loaded = true;
114 }
115
116 private void rename(String name) {
117 if (parent == null) {
118 return;
119 }
120 parent.children.remove(this.name);
121 this.name = name;
122 parent.children.put(name, this);
123 }
124 }
125}
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 @@
1package org.yuzu.yuzu_emu.utils; 1package org.yuzu.yuzu_emu.utils;
2 2
3import android.content.Intent; 3import android.content.Intent;
4import android.net.Uri;
5import android.os.Environment;
6
7import androidx.annotation.Nullable;
8import androidx.fragment.app.FragmentActivity; 4import androidx.fragment.app.FragmentActivity;
9 5
10import com.nononsenseapps.filepicker.FilePickerActivity;
11import com.nononsenseapps.filepicker.Utils;
12
13import org.yuzu.yuzu_emu.activities.CustomFilePickerActivity;
14
15import java.io.File;
16import java.util.List;
17
18public final class FileBrowserHelper { 6public final class FileBrowserHelper {
19 public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title, List<String> extensions) { 7 public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title) {
20 Intent i = new Intent(activity, CustomFilePickerActivity.class); 8 Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
21 9 i.putExtra(Intent.EXTRA_TITLE, title);
22 i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false);
23 i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false);
24 i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR);
25 i.putExtra(FilePickerActivity.EXTRA_START_PATH,
26 Environment.getExternalStorageDirectory().getPath());
27 i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title);
28 i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions));
29
30 activity.startActivityForResult(i, requestCode); 10 activity.startActivityForResult(i, requestCode);
31 } 11 }
32 12
33 public static void openFilePicker(FragmentActivity activity, int requestCode, int title,
34 List<String> extensions, boolean allowMultiple) {
35 Intent i = new Intent(activity, CustomFilePickerActivity.class);
36
37 i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, allowMultiple);
38 i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false);
39 i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE);
40 i.putExtra(FilePickerActivity.EXTRA_START_PATH,
41 Environment.getExternalStorageDirectory().getPath());
42 i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title);
43 i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions));
44
45 activity.startActivityForResult(i, requestCode);
46 }
47
48 @Nullable
49 public static String getSelectedDirectory(Intent result) { 13 public static String getSelectedDirectory(Intent result) {
50 // Use the provided utility method to parse the result 14 return result.getDataString();
51 List<Uri> files = Utils.getSelectedFilesFromResult(result);
52 if (!files.isEmpty()) {
53 File file = Utils.getFileForUri(files.get(0));
54 return file.getAbsolutePath();
55 }
56
57 return null;
58 }
59
60 @Nullable
61 public static String[] getSelectedFiles(Intent result) {
62 // Use the provided utility method to parse the result
63 List<Uri> files = Utils.getSelectedFilesFromResult(result);
64 if (!files.isEmpty()) {
65 String[] paths = new String[files.size()];
66 for (int i = 0; i < files.size(); i++)
67 paths[i] = Utils.getFileForUri(files.get(i)).getAbsolutePath();
68 return paths;
69 }
70
71 return null;
72 } 15 }
73} 16}
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 @@
1package org.yuzu.yuzu_emu.utils; 1package org.yuzu.yuzu_emu.utils;
2 2
3import java.io.File; 3import android.content.ContentResolver;
4import java.io.FileInputStream; 4import android.content.Context;
5import java.io.IOException; 5import android.database.Cursor;
6import android.net.Uri;
7import android.os.ParcelFileDescriptor;
8import android.provider.DocumentsContract;
9
10import androidx.annotation.Nullable;
11import androidx.documentfile.provider.DocumentFile;
12
13import org.yuzu.yuzu_emu.model.MinimalDocumentFile;
14
6import java.io.InputStream; 15import java.io.InputStream;
16import java.io.OutputStream;
17import java.net.URLDecoder;
18import java.util.ArrayList;
19import java.util.List;
7 20
8public class FileUtil { 21public class FileUtil {
9 public static byte[] getBytesFromFile(File file) throws IOException { 22 static final String PATH_TREE = "tree";
10 final long length = file.length(); 23 static final String DECODE_METHOD = "UTF-8";
24 static final String APPLICATION_OCTET_STREAM = "application/octet-stream";
25 static final String TEXT_PLAIN = "text/plain";
11 26
12 // You cannot create an array using a long type. 27 /**
13 if (length > Integer.MAX_VALUE) { 28 * Create a file from directory with filename.
14 // File is too large 29 * @param context Application context
15 throw new IOException("File is too large!"); 30 * @param directory parent path for file.
31 * @param filename file display name.
32 * @return boolean
33 */
34 @Nullable
35 public static DocumentFile createFile(Context context, String directory, String filename) {
36 try {
37 Uri directoryUri = Uri.parse(directory);
38 DocumentFile parent = DocumentFile.fromTreeUri(context, directoryUri);
39 if (parent == null) return null;
40 filename = URLDecoder.decode(filename, DECODE_METHOD);
41 String mimeType = APPLICATION_OCTET_STREAM;
42 if (filename.endsWith(".txt")) {
43 mimeType = TEXT_PLAIN;
44 }
45 DocumentFile exists = parent.findFile(filename);
46 if (exists != null) return exists;
47 return parent.createFile(mimeType, filename);
48 } catch (Exception e) {
49 Log.error("[FileUtil]: Cannot create file, error: " + e.getMessage());
16 } 50 }
51 return null;
52 }
17 53
18 byte[] bytes = new byte[(int) length]; 54 /**
55 * Create a directory from directory with filename.
56 * @param context Application context
57 * @param directory parent path for directory.
58 * @param directoryName directory display name.
59 * @return boolean
60 */
61 @Nullable
62 public static DocumentFile createDir(Context context, String directory, String directoryName) {
63 try {
64 Uri directoryUri = Uri.parse(directory);
65 DocumentFile parent = DocumentFile.fromTreeUri(context, directoryUri);
66 if (parent == null) return null;
67 directoryName = URLDecoder.decode(directoryName, DECODE_METHOD);
68 DocumentFile isExist = parent.findFile(directoryName);
69 if (isExist != null) return isExist;
70 return parent.createDirectory(directoryName);
71 } catch (Exception e) {
72 Log.error("[FileUtil]: Cannot create file, error: " + e.getMessage());
73 }
74 return null;
75 }
19 76
20 int offset = 0; 77 /**
21 int numRead; 78 * Open content uri and return file descriptor to JNI.
79 * @param context Application context
80 * @param path Native content uri path
81 * @param openmode will be one of "r", "r", "rw", "wa", "rwa"
82 * @return file descriptor
83 */
84 public static int openContentUri(Context context, String path, String openmode) {
85 try {
86 Uri uri = Uri.parse(path);
87 ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, openmode);
88 if (parcelFileDescriptor == null) {
89 Log.error("[FileUtil]: Cannot get the file descriptor from uri: " + path);
90 return -1;
91 }
92 return parcelFileDescriptor.detachFd();
93 }
94 catch (Exception e) {
95 Log.error("[FileUtil]: Cannot open content uri, error: " + e.getMessage());
96 }
97 return -1;
98 }
22 99
23 try (InputStream is = new FileInputStream(file)) { 100 /**
24 while (offset < bytes.length 101 * Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow
25 && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { 102 * This function will be faster than DoucmentFile.listFiles
26 offset += numRead; 103 * @param context Application context
104 * @param uri Directory uri.
105 * @return CheapDocument lists.
106 */
107 public static MinimalDocumentFile[] listFiles(Context context, Uri uri) {
108 final ContentResolver resolver = context.getContentResolver();
109 final String[] columns = new String[]{
110 DocumentsContract.Document.COLUMN_DOCUMENT_ID,
111 DocumentsContract.Document.COLUMN_DISPLAY_NAME,
112 DocumentsContract.Document.COLUMN_MIME_TYPE,
113 };
114 Cursor c = null;
115 final List<MinimalDocumentFile> results = new ArrayList<>();
116 try {
117 String docId;
118 if (isRootTreeUri(uri)) {
119 docId = DocumentsContract.getTreeDocumentId(uri);
120 } else {
121 docId = DocumentsContract.getDocumentId(uri);
122 }
123 final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId);
124 c = resolver.query(childrenUri, columns, null, null, null);
125 while(c.moveToNext()) {
126 final String documentId = c.getString(0);
127 final String documentName = c.getString(1);
128 final String documentMimeType = c.getString(2);
129 final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId);
130 MinimalDocumentFile document = new MinimalDocumentFile(documentName, documentMimeType, documentUri);
131 results.add(document);
27 } 132 }
133 } catch (Exception e) {
134 Log.error("[FileUtil]: Cannot list file error: " + e.getMessage());
135 } finally {
136 closeQuietly(c);
137 }
138 return results.toArray(new MinimalDocumentFile[0]);
139 }
140
141 /**
142 * Check whether given path exists.
143 * @param path Native content uri path
144 * @return bool
145 */
146 public static boolean Exists(Context context, String path) {
147 Cursor c = null;
148 try {
149 Uri mUri = Uri.parse(path);
150 final String[] columns = new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID };
151 c = context.getContentResolver().query(mUri, columns, null, null, null);
152 return c.getCount() > 0;
153 } catch (Exception e) {
154 Log.info("[FileUtil] Cannot find file from given path, error: " + e.getMessage());
155 } finally {
156 closeQuietly(c);
28 } 157 }
158 return false;
159 }
160
161 /**
162 * Check whether given path is a directory
163 * @param path content uri path
164 * @return bool
165 */
166 public static boolean isDirectory(Context context, String path) {
167 final ContentResolver resolver = context.getContentResolver();
168 final String[] columns = new String[] {
169 DocumentsContract.Document.COLUMN_MIME_TYPE
170 };
171 boolean isDirectory = false;
172 Cursor c = null;
173 try {
174 Uri mUri = Uri.parse(path);
175 c = resolver.query(mUri, columns, null, null, null);
176 c.moveToNext();
177 final String mimeType = c.getString(0);
178 isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
179 } catch (Exception e) {
180 Log.error("[FileUtil]: Cannot list files, error: " + e.getMessage());
181 } finally {
182 closeQuietly(c);
183 }
184 return isDirectory;
185 }
29 186
30 // Ensure all the bytes have been read in 187 /**
31 if (offset < bytes.length) { 188 * Get file display name from given path
32 throw new IOException("Could not completely read file " + file.getName()); 189 * @param path content uri path
190 * @return String display name
191 */
192 public static String getFilename(Context context, String path) {
193 final ContentResolver resolver = context.getContentResolver();
194 final String[] columns = new String[] {
195 DocumentsContract.Document.COLUMN_DISPLAY_NAME
196 };
197 String filename = "";
198 Cursor c = null;
199 try {
200 Uri mUri = Uri.parse(path);
201 c = resolver.query(mUri, columns, null, null, null);
202 c.moveToNext();
203 filename = c.getString(0);
204 } catch (Exception e) {
205 Log.error("[FileUtil]: Cannot get file size, error: " + e.getMessage());
206 } finally {
207 closeQuietly(c);
33 } 208 }
209 return filename;
210 }
211
212 public static String[] getFilesName(Context context, String path) {
213 Uri uri = Uri.parse(path);
214 List<String> files = new ArrayList<>();
215 for (MinimalDocumentFile file: FileUtil.listFiles(context, uri)) {
216 files.add(file.getFilename());
217 }
218 return files.toArray(new String[0]);
219 }
34 220
35 return bytes; 221 /**
222 * Get file size from given path.
223 * @param path content uri path
224 * @return long file size
225 */
226 public static long getFileSize(Context context, String path) {
227 final ContentResolver resolver = context.getContentResolver();
228 final String[] columns = new String[] {
229 DocumentsContract.Document.COLUMN_SIZE
230 };
231 long size = 0;
232 Cursor c =null;
233 try {
234 Uri mUri = Uri.parse(path);
235 c = resolver.query(mUri, columns, null, null, null);
236 c.moveToNext();
237 size = c.getLong(0);
238 } catch (Exception e) {
239 Log.error("[FileUtil]: Cannot get file size, error: " + e.getMessage());
240 } finally {
241 closeQuietly(c);
242 }
243 return size;
244 }
245
246 public static boolean isRootTreeUri(Uri uri) {
247 final List<String> paths = uri.getPathSegments();
248 return paths.size() == 2 && PATH_TREE.equals(paths.get(0));
249 }
250
251 public static void closeQuietly(AutoCloseable closeable) {
252 if (closeable != null) {
253 try {
254 closeable.close();
255 } catch (RuntimeException rethrown) {
256 throw rethrown;
257 } catch (Exception ignored) {
258 }
259 }
36 } 260 }
37} 261}
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 @@
1package org.yuzu.yuzu_emu.utils;
2
3import android.annotation.TargetApi;
4import android.content.Context;
5import android.content.pm.PackageManager;
6import android.os.Build;
7
8import androidx.core.content.ContextCompat;
9import androidx.fragment.app.FragmentActivity;
10
11import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
12
13public class PermissionsHandler {
14 public static final int REQUEST_CODE_WRITE_PERMISSION = 500;
15
16 // We use permissions acceptance as an indicator if this is a first boot for the user.
17 public static boolean isFirstBoot(final FragmentActivity activity) {
18 return ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED;
19 }
20
21 @TargetApi(Build.VERSION_CODES.M)
22 public static boolean checkWritePermission(final FragmentActivity activity) {
23 if (isFirstBoot(activity)) {
24 activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE},
25 REQUEST_CODE_WRITE_PERMISSION);
26 return false;
27 }
28
29 return true;
30 }
31
32 public static boolean hasWriteAccess(Context context) {
33 return ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
34 }
35}
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 @@
1package org.yuzu.yuzu_emu.utils; 1package org.yuzu.yuzu_emu.utils;
2 2
3import android.content.Intent; 3import android.content.SharedPreferences;
4import android.os.Bundle; 4import android.preference.PreferenceManager;
5import android.text.TextUtils;
6
7import androidx.appcompat.app.AlertDialog; 5import androidx.appcompat.app.AlertDialog;
8import androidx.fragment.app.FragmentActivity;
9 6
10import org.yuzu.yuzu_emu.R; 7import org.yuzu.yuzu_emu.R;
11import org.yuzu.yuzu_emu.activities.EmulationActivity; 8import org.yuzu.yuzu_emu.YuzuApplication;
9import org.yuzu.yuzu_emu.ui.main.MainActivity;
10import org.yuzu.yuzu_emu.ui.main.MainPresenter;
12 11
13public final class StartupHandler { 12public final class StartupHandler {
14 private static void handlePermissionsCheck(FragmentActivity parent) { 13 private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext());
15 // Ask the user to grant write permission if it's not already granted
16 PermissionsHandler.checkWritePermission(parent);
17 14
18 String start_file = ""; 15 private static void handleStartupPromptDismiss(MainActivity parent) {
19 Bundle extras = parent.getIntent().getExtras(); 16 parent.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY);
20 if (extras != null) { 17 }
21 start_file = extras.getString("AutoStartFile");
22 }
23 18
24 if (!TextUtils.isEmpty(start_file)) { 19 private static void markFirstBoot() {
25 // Start the emulation activity, send the ISO passed in and finish the main activity 20 final SharedPreferences.Editor editor = mPreferences.edit();
26 Intent emulation_intent = new Intent(parent, EmulationActivity.class); 21 editor.putBoolean("FirstApplicationLaunch", false);
27 emulation_intent.putExtra("SelectedGame", start_file); 22 editor.apply();
28 parent.startActivity(emulation_intent);
29 parent.finish();
30 }
31 } 23 }
32 24
33 public static void HandleInit(FragmentActivity parent) { 25 public static void handleInit(MainActivity parent) {
34 if (PermissionsHandler.isFirstBoot(parent)) { 26 if (mPreferences.getBoolean("FirstApplicationLaunch", true)) {
27 markFirstBoot();
28
35 // Prompt user with standard first boot disclaimer 29 // Prompt user with standard first boot disclaimer
36 new AlertDialog.Builder(parent) 30 new AlertDialog.Builder(parent)
37 .setTitle(R.string.app_name) 31 .setTitle(R.string.app_name)
38 .setIcon(R.mipmap.ic_launcher) 32 .setIcon(R.mipmap.ic_launcher)
39 .setMessage(parent.getResources().getString(R.string.app_disclaimer)) 33 .setMessage(parent.getResources().getString(R.string.app_disclaimer))
40 .setPositiveButton(android.R.string.ok, null) 34 .setPositiveButton(android.R.string.ok, null)
41 .setOnDismissListener(dialogInterface -> handlePermissionsCheck(parent)) 35 .setOnDismissListener(dialogInterface -> handleStartupPromptDismiss(parent))
42 .show(); 36 .show();
43 } 37 }
44 } 38 }
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 @@
18 18
19namespace FS = Common::FS; 19namespace FS = Common::FS;
20 20
21const std::filesystem::path default_config_path =
22 FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "config.ini";
23
24Config::Config(std::optional<std::filesystem::path> config_path) 21Config::Config(std::optional<std::filesystem::path> config_path)
25 : config_loc{config_path.value_or(default_config_path)}, 22 : config_loc{config_path.value_or(FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "config.ini")},
26 config{std::make_unique<INIReader>(FS::PathToUTF8String(config_loc))} { 23 config{std::make_unique<INIReader>(FS::PathToUTF8String(config_loc))} {
27 Reload(); 24 Reload();
28} 25}
@@ -66,8 +63,8 @@ void Config::ReadSetting(const std::string& group, Settings::Setting<bool>& sett
66 63
67template <typename Type, bool ranged> 64template <typename Type, bool ranged>
68void Config::ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting) { 65void Config::ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting) {
69 setting = static_cast<Type>(config->GetInteger(group, setting.GetLabel(), 66 setting = static_cast<Type>(
70 static_cast<long>(setting.GetDefault()))); 67 config->GetInteger(group, setting.GetLabel(), static_cast<long>(setting.GetDefault())));
71} 68}
72 69
73void Config::ReadValues() { 70void Config::ReadValues() {
@@ -93,9 +90,9 @@ void Config::ReadValues() {
93 for (int i = 0; i < num_touch_from_button_maps; ++i) { 90 for (int i = 0; i < num_touch_from_button_maps; ++i) {
94 Settings::TouchFromButtonMap map; 91 Settings::TouchFromButtonMap map;
95 map.name = config->Get("ControlsGeneral", 92 map.name = config->Get("ControlsGeneral",
96 std::string("touch_from_button_maps_") + std::to_string(i) + 93 std::string("touch_from_button_maps_") + std::to_string(i) +
97 std::string("_name"), 94 std::string("_name"),
98 "default"); 95 "default");
99 const int num_touch_maps = config->GetInteger( 96 const int num_touch_maps = config->GetInteger(
100 "ControlsGeneral", 97 "ControlsGeneral",
101 std::string("touch_from_button_maps_") + std::to_string(i) + std::string("_count"), 98 std::string("touch_from_button_maps_") + std::to_string(i) + std::string("_count"),
@@ -105,9 +102,9 @@ void Config::ReadValues() {
105 for (int j = 0; j < num_touch_maps; ++j) { 102 for (int j = 0; j < num_touch_maps; ++j) {
106 std::string touch_mapping = 103 std::string touch_mapping =
107 config->Get("ControlsGeneral", 104 config->Get("ControlsGeneral",
108 std::string("touch_from_button_maps_") + std::to_string(i) + 105 std::string("touch_from_button_maps_") + std::to_string(i) +
109 std::string("_bind_") + std::to_string(j), 106 std::string("_bind_") + std::to_string(j),
110 ""); 107 "");
111 map.buttons.emplace_back(std::move(touch_mapping)); 108 map.buttons.emplace_back(std::move(touch_mapping));
112 } 109 }
113 110
@@ -127,16 +124,16 @@ void Config::ReadValues() {
127 ReadSetting("Data Storage", Settings::values.use_virtual_sd); 124 ReadSetting("Data Storage", Settings::values.use_virtual_sd);
128 FS::SetYuzuPath(FS::YuzuPath::NANDDir, 125 FS::SetYuzuPath(FS::YuzuPath::NANDDir,
129 config->Get("Data Storage", "nand_directory", 126 config->Get("Data Storage", "nand_directory",
130 FS::GetYuzuPathString(FS::YuzuPath::NANDDir))); 127 FS::GetYuzuPathString(FS::YuzuPath::NANDDir)));
131 FS::SetYuzuPath(FS::YuzuPath::SDMCDir, 128 FS::SetYuzuPath(FS::YuzuPath::SDMCDir,
132 config->Get("Data Storage", "sdmc_directory", 129 config->Get("Data Storage", "sdmc_directory",
133 FS::GetYuzuPathString(FS::YuzuPath::SDMCDir))); 130 FS::GetYuzuPathString(FS::YuzuPath::SDMCDir)));
134 FS::SetYuzuPath(FS::YuzuPath::LoadDir, 131 FS::SetYuzuPath(FS::YuzuPath::LoadDir,
135 config->Get("Data Storage", "load_directory", 132 config->Get("Data Storage", "load_directory",
136 FS::GetYuzuPathString(FS::YuzuPath::LoadDir))); 133 FS::GetYuzuPathString(FS::YuzuPath::LoadDir)));
137 FS::SetYuzuPath(FS::YuzuPath::DumpDir, 134 FS::SetYuzuPath(FS::YuzuPath::DumpDir,
138 config->Get("Data Storage", "dump_directory", 135 config->Get("Data Storage", "dump_directory",
139 FS::GetYuzuPathString(FS::YuzuPath::DumpDir))); 136 FS::GetYuzuPathString(FS::YuzuPath::DumpDir)));
140 ReadSetting("Data Storage", Settings::values.gamecard_inserted); 137 ReadSetting("Data Storage", Settings::values.gamecard_inserted);
141 ReadSetting("Data Storage", Settings::values.gamecard_current_game); 138 ReadSetting("Data Storage", Settings::values.gamecard_current_game);
142 ReadSetting("Data Storage", Settings::values.gamecard_path); 139 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 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <jni.h>
5
6#include "common/fs/fs_android.h"
1#include "jni/id_cache.h" 7#include "jni/id_cache.h"
2 8
3static JavaVM* s_java_vm; 9static JavaVM* s_java_vm;
4static jclass s_native_library_class; 10static jclass s_native_library_class;
5static jmethodID s_exit_emulation_activity; 11static jmethodID s_exit_emulation_activity;
6 12
13static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
14
7namespace IDCache { 15namespace IDCache {
8 16
9JNIEnv* GetEnvForThread() { 17JNIEnv* GetEnvForThread() {
@@ -34,3 +42,41 @@ jmethodID GetExitEmulationActivity() {
34} 42}
35 43
36} // namespace IDCache 44} // namespace IDCache
45
46#ifdef __cplusplus
47extern "C" {
48#endif
49
50jint JNI_OnLoad(JavaVM* vm, void* reserved) {
51 s_java_vm = vm;
52
53 JNIEnv* env;
54 if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK)
55 return JNI_ERR;
56
57 // Initialize Java classes
58 const jclass native_library_class = env->FindClass("org/yuzu/yuzu_emu/NativeLibrary");
59 s_native_library_class = reinterpret_cast<jclass>(env->NewGlobalRef(native_library_class));
60 s_exit_emulation_activity =
61 env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
62
63 // Initialize Android Storage
64 Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
65
66 return JNI_VERSION;
67}
68
69void JNI_OnUnload(JavaVM* vm, void* reserved) {
70 JNIEnv* env;
71 if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK) {
72 return;
73 }
74
75 // UnInitialize Android Storage
76 Common::FS::Android::UnRegisterCallbacks();
77 env->DeleteGlobalRef(s_native_library_class);
78}
79
80#ifdef __cplusplus
81}
82#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 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
1#include <codecvt> 4#include <codecvt>
2#include <locale> 5#include <locale>
3#include <string> 6#include <string>
@@ -7,6 +10,7 @@
7#include <android/native_window_jni.h> 10#include <android/native_window_jni.h>
8 11
9#include "common/detached_tasks.h" 12#include "common/detached_tasks.h"
13#include "common/fs/path_util.h"
10#include "common/logging/backend.h" 14#include "common/logging/backend.h"
11#include "common/logging/log.h" 15#include "common/logging/log.h"
12#include "common/microprofile.h" 16#include "common/microprofile.h"
@@ -257,9 +261,11 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_NotifyOrientationChange(JNIEnv* env,
257 jint layout_option, 261 jint layout_option,
258 jint rotation) {} 262 jint rotation) {}
259 263
260void Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserDirectory([[maybe_unused]] JNIEnv* env, 264void Java_org_yuzu_yuzu_1emu_NativeLibrary_SetAppDirectory([[maybe_unused]] JNIEnv* env,
261 [[maybe_unused]] jclass clazz, 265 [[maybe_unused]] jclass clazz,
262 [[maybe_unused]] jstring j_directory) {} 266 [[maybe_unused]] jstring j_directory) {
267 Common::FS::SetAppDirectory(GetJString(env, j_directory));
268}
263 269
264void Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation([[maybe_unused]] JNIEnv* env, 270void Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation([[maybe_unused]] JNIEnv* env,
265 [[maybe_unused]] jclass clazz) {} 271 [[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 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
1#pragma once 4#pragma once
2 5
3#include <jni.h> 6#include <jni.h>
@@ -8,16 +11,16 @@ extern "C" {
8#endif 11#endif
9 12
10JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation(JNIEnv* env, 13JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation(JNIEnv* env,
11 jclass clazz); 14 jclass clazz);
12 15
13JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation(JNIEnv* env, 16JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation(JNIEnv* env,
14 jclass clazz); 17 jclass clazz);
15 18
16JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation(JNIEnv* env, 19JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation(JNIEnv* env,
17 jclass clazz); 20 jclass clazz);
18 21
19JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning(JNIEnv* env, 22JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning(JNIEnv* env,
20 jclass clazz); 23 jclass clazz);
21 24
22JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadEvent( 25JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadEvent(
23 JNIEnv* env, jclass clazz, jstring j_device, jint j_button, jint action); 26 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
29 JNIEnv* env, jclass clazz, jstring j_device, jint axis_id, jfloat axis_val); 32 JNIEnv* env, jclass clazz, jstring j_device, jint axis_id, jfloat axis_val);
30 33
31JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchEvent(JNIEnv* env, 34JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchEvent(JNIEnv* env,
32 jclass clazz, 35 jclass clazz,
33 jfloat x, jfloat y, 36 jfloat x, jfloat y,
34 jboolean pressed); 37 jboolean pressed);
35 38
36JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, 39JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz,
37 jclass clazz, jfloat x, 40 jfloat x, jfloat y);
38 jfloat y);
39 41
40JNIEXPORT jintArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon(JNIEnv* env, 42JNIEXPORT jintArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon(JNIEnv* env, jclass clazz,
41 jclass clazz, 43 jstring j_file);
42 jstring j_file);
43 44
44JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle(JNIEnv* env, 45JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle(JNIEnv* env, jclass clazz,
45 jclass clazz, 46 jstring j_filename);
46 jstring j_filename);
47 47
48JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDescription( 48JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDescription(JNIEnv* env,
49 JNIEnv* env, jclass clazz, jstring j_filename); 49 jclass clazz,
50 jstring j_filename);
50 51
51JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGameId(JNIEnv* env, 52JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGameId(JNIEnv* env, jclass clazz,
52 jclass clazz, 53 jstring j_filename);
53 jstring j_filename);
54 54
55JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetRegions(JNIEnv* env, 55JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetRegions(JNIEnv* env,
56 jclass clazz, 56 jclass clazz,
57 jstring j_filename); 57 jstring j_filename);
58 58
59JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetCompany(JNIEnv* env, 59JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetCompany(JNIEnv* env,
60 jclass clazz, 60 jclass clazz,
61 jstring j_filename); 61 jstring j_filename);
62 62
63JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGitRevision(JNIEnv* env, 63JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGitRevision(JNIEnv* env,
64 jclass clazz); 64 jclass clazz);
65 65
66JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserDirectory( 66JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetAppDirectory(JNIEnv* env,
67 JNIEnv* env, jclass clazz, jstring j_directory); 67 jclass clazz,
68 jstring j_directory);
68 69
69JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_SetSysDirectory( 70JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_SetSysDirectory(
70 JNIEnv* env, jclass clazz, jstring path_); 71 JNIEnv* env, jclass clazz, jstring path_);
71 72
72JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetSysDirectory(JNIEnv* env, 73JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetSysDirectory(JNIEnv* env,
73 jclass clazz, 74 jclass clazz,
74 jstring path); 75 jstring path);
75 76
76JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env, 77JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env,
77 jclass clazz); 78 jclass clazz);
78 79
79JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env, 80JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env,
80 jclass clazz); 81 jclass clazz);
81JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetProfiling(JNIEnv* env, 82JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetProfiling(JNIEnv* env, jclass clazz,
82 jclass clazz, 83 jboolean enable);
83 jboolean enable);
84 84
85JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_WriteProfileResults(JNIEnv* env, 85JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_WriteProfileResults(JNIEnv* env,
86 jclass clazz); 86 jclass clazz);
87 87
88JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_NotifyOrientationChange( 88JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_NotifyOrientationChange(
89 JNIEnv* env, jclass clazz, jint layout_option, jint rotation); 89 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_
96 JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate); 96 JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate);
97 97
98JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env, 98JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env,
99 jclass clazz, 99 jclass clazz,
100 jobject surf); 100 jobject surf);
101 101
102JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env, 102JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env,
103 jclass clazz); 103 jclass clazz);
104 104
105JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_InitGameIni(JNIEnv* env, 105JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_InitGameIni(JNIEnv* env, jclass clazz,
106 jclass clazz, 106 jstring j_game_id);
107 jstring j_game_id);
108 107
109JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadSettings(JNIEnv* env, 108JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadSettings(JNIEnv* env,
110 jclass clazz); 109 jclass clazz);
111 110
112JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserSetting( 111JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserSetting(
113 JNIEnv* env, jclass clazz, jstring j_game_id, jstring j_section, jstring j_key, 112 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(
117 JNIEnv* env, jclass clazz, jstring game_id, jstring section, jstring key); 116 JNIEnv* env, jclass clazz, jstring game_id, jstring section, jstring key);
118 117
119JNIEXPORT jdoubleArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetPerfStats(JNIEnv* env, 118JNIEXPORT jdoubleArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetPerfStats(JNIEnv* env,
120 jclass clazz); 119 jclass clazz);
121 120
122JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env, 121JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env,
123 jclass clazz); 122 jclass clazz);
124 123
125#ifdef __cplusplus 124#ifdef __cplusplus
126} 125}
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 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
3 android:id="@+id/nnf_picker_toolbar"
4 android:layout_width="match_parent"
5 android:layout_height="wrap_content"
6 android:layout_alignParentTop="true"
7 android:background="?attr/colorPrimary"
8 android:minHeight="?attr/actionBarSize"
9 android:theme="?nnf_toolbarTheme">
10
11 <LinearLayout
12 android:layout_width="match_parent"
13 android:layout_height="match_parent"
14 android:orientation="vertical">
15
16 <TextView
17 android:id="@+id/filepicker_title"
18 android:layout_width="match_parent"
19 android:layout_height="wrap_content"
20 android:ellipsize="start"
21 android:singleLine="true"
22 android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />
23
24 <TextView
25 android:id="@+id/nnf_current_dir"
26 android:layout_width="match_parent"
27 android:layout_height="wrap_content"
28 android:ellipsize="start"
29 android:singleLine="true"
30 android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle" />
31 </LinearLayout>
32</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 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <style name="FilePickerBaseTheme" parent="NNF_BaseTheme" />
5</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 @@
2<resources> 2<resources>
3 <!-- Example customization of dimensions originally defined in res/values/dimens.xml 3 <!-- Example customization of dimensions originally defined in res/values/dimens.xml
4 (such as screen margins) for screens with more than 1024dp of available width. --> 4 (such as screen margins) for screens with more than 1024dp of available width. -->
5 <dimen name="activity_horizontal_margin">96dp</dimen>
6</resources> 5</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 @@
1<resources> 1<resources>
2 <!-- Example customization of dimensions originally defined in res/values/dimens.xml 2 <!-- Example customization of dimensions originally defined in res/values/dimens.xml
3 (such as screen margins) for screens with more than 820dp of available width. --> 3 (such as screen margins) for screens with more than 820dp of available width. -->
4 <dimen name="activity_horizontal_margin">64dp</dimen>
5</resources> 4</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 @@
48 <string name="grid_menu_core_settings">Settings</string> 48 <string name="grid_menu_core_settings">Settings</string>
49 49
50 <!-- Add Directory Screen--> 50 <!-- Add Directory Screen-->
51 <string name="select_game_folder">Select Game Folder</string> 51 <string name="select_game_folder">Select game folder</string>
52 <string name="install_cia_title">Install CIA</string> 52 <string name="install_cia_title">Install CIA</string>
53 53
54 <!-- Preferences Screen --> 54 <!-- Preferences Screen -->
@@ -71,7 +71,6 @@
71 <string name="emulation_touch_overlay_reset">Reset Overlay</string> 71 <string name="emulation_touch_overlay_reset">Reset Overlay</string>
72 <string name="emulation_close_game_message">Are you sure that you would like to close the current game?</string> 72 <string name="emulation_close_game_message">Are you sure that you would like to close the current game?</string>
73 73
74 <string name="write_permission_needed">You need to allow write access to external storage for the emulator to work</string>
75 <string name="load_settings">Loading Settings...</string> 74 <string name="load_settings">Loading Settings...</string>
76 75
77 <string name="external_storage_not_mounted">The external storage needs to be available in order to use yuzu</string> 76 <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 @@
61 <item name="android:windowAllowReturnTransitionOverlap">true</item> 61 <item name="android:windowAllowReturnTransitionOverlap">true</item>
62 </style> 62 </style>
63 63
64 <!-- Inherit from a base file picker theme that handles day/night -->
65 <style name="FilePickerTheme" parent="FilePickerBaseTheme">
66 <item name="colorSurface">@color/view_background</item>
67 <item name="colorOnSurface">@color/view_text</item>
68 <item name="colorPrimary">@color/citra_orange</item>
69 <item name="colorPrimaryDark">@color/citra_orange_dark</item>
70 <item name="colorAccent">@color/citra_accent</item>
71 <item name="android:windowBackground">@color/view_background</item>
72
73 <!-- Need to set this also to style create folder dialog -->
74 <item name="alertDialogTheme">@style/FilePickerAlertDialogTheme</item>
75
76 <item name="nnf_list_item_divider">@drawable/gamelist_divider</item>
77 <item name="nnf_toolbarTheme">@style/ThemeOverlay.AppCompat.DayNight.ActionBar</item>
78 </style>
79
80 <style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.DayNight.Dialog.Alert"> 64 <style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.DayNight.Dialog.Alert">
81 <item name="colorSurface">@color/view_background</item> 65 <item name="colorSurface">@color/view_background</item>
82 <item name="colorOnSurface">@color/view_text</item> 66 <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 @@
1<?xml version="1.0" encoding="utf-8"?>
2<resources>
3
4 <style name="FilePickerBaseTheme" parent="NNF_BaseTheme.Light" />
5</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)
155 target_link_libraries(common PRIVATE ntdll) 155 target_link_libraries(common PRIVATE ntdll)
156endif() 156endif()
157 157
158if(ANDROID)
159 target_sources(common
160 PRIVATE
161 fs/fs_android.cpp
162 fs/fs_android.h
163 )
164endif()
165
158if(ARCHITECTURE_x86_64) 166if(ARCHITECTURE_x86_64)
159 target_sources(common 167 target_sources(common
160 PRIVATE 168 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 @@
5 5
6#include "common/fs/file.h" 6#include "common/fs/file.h"
7#include "common/fs/fs.h" 7#include "common/fs/fs.h"
8#ifdef ANDROID
9#include "common/fs/fs_android.h"
10#endif
8#include "common/logging/log.h" 11#include "common/logging/log.h"
9 12
10#ifdef _WIN32 13#ifdef _WIN32
@@ -252,6 +255,23 @@ void IOFile::Open(const fs::path& path, FileAccessMode mode, FileType type, File
252 } else { 255 } else {
253 _wfopen_s(&file, path.c_str(), AccessModeToWStr(mode, type)); 256 _wfopen_s(&file, path.c_str(), AccessModeToWStr(mode, type));
254 } 257 }
258#elif ANDROID
259 if (Android::IsContentUri(path)) {
260 ASSERT_MSG(mode == FileAccessMode::Read, "Content URI file access is for read-only!");
261 const auto fd = Android::OpenContentUri(path, Android::OpenMode::Read);
262 if (fd != -1) {
263 file = fdopen(fd, "r");
264 const auto error_num = errno;
265 if (error_num != 0 && file == nullptr) {
266 LOG_ERROR(Common_Filesystem, "Error opening file: {}, error: {}", path.c_str(),
267 strerror(error_num));
268 }
269 } else {
270 LOG_ERROR(Common_Filesystem, "Error opening file: {}", path.c_str());
271 }
272 } else {
273 file = std::fopen(path.c_str(), AccessModeToStr(mode, type));
274 }
255#else 275#else
256 file = std::fopen(path.c_str(), AccessModeToStr(mode, type)); 276 file = std::fopen(path.c_str(), AccessModeToStr(mode, type));
257#endif 277#endif
@@ -372,6 +392,23 @@ u64 IOFile::GetSize() const {
372 // Flush any unwritten buffered data into the file prior to retrieving the file size. 392 // Flush any unwritten buffered data into the file prior to retrieving the file size.
373 std::fflush(file); 393 std::fflush(file);
374 394
395#if ANDROID
396 u64 file_size = 0;
397 if (Android::IsContentUri(file_path)) {
398 file_size = Android::GetSize(file_path);
399 } else {
400 std::error_code ec;
401
402 file_size = fs::file_size(file_path, ec);
403
404 if (ec) {
405 LOG_ERROR(Common_Filesystem,
406 "Failed to retrieve the file size of path={}, ec_message={}",
407 PathToUTF8String(file_path), ec.message());
408 return 0;
409 }
410 }
411#else
375 std::error_code ec; 412 std::error_code ec;
376 413
377 const auto file_size = fs::file_size(file_path, ec); 414 const auto file_size = fs::file_size(file_path, ec);
@@ -381,6 +418,7 @@ u64 IOFile::GetSize() const {
381 PathToUTF8String(file_path), ec.message()); 418 PathToUTF8String(file_path), ec.message());
382 return 0; 419 return 0;
383 } 420 }
421#endif
384 422
385 return file_size; 423 return file_size;
386} 424}
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 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "common/fs/fs_android.h"
5
6namespace Common::FS::Android {
7
8JNIEnv* GetEnvForThread() {
9 thread_local static struct OwnedEnv {
10 OwnedEnv() {
11 status = g_jvm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6);
12 if (status == JNI_EDETACHED)
13 g_jvm->AttachCurrentThread(&env, nullptr);
14 }
15
16 ~OwnedEnv() {
17 if (status == JNI_EDETACHED)
18 g_jvm->DetachCurrentThread();
19 }
20
21 int status;
22 JNIEnv* env = nullptr;
23 } owned;
24 return owned.env;
25}
26
27void RegisterCallbacks(JNIEnv* env, jclass clazz) {
28 env->GetJavaVM(&g_jvm);
29 native_library = clazz;
30
31#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) \
32 F(JMethodID, JMethodName, Signature)
33#define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature) \
34 F(JMethodID, JMethodName, Signature)
35#define F(JMethodID, JMethodName, Signature) \
36 JMethodID = env->GetStaticMethodID(native_library, JMethodName, Signature);
37 ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
38 ANDROID_STORAGE_FUNCTIONS(FS)
39#undef F
40#undef FS
41#undef FR
42}
43
44void UnRegisterCallbacks() {
45#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) F(JMethodID)
46#define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature) F(JMethodID)
47#define F(JMethodID) JMethodID = nullptr;
48 ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
49 ANDROID_STORAGE_FUNCTIONS(FS)
50#undef F
51#undef FS
52#undef FR
53}
54
55bool IsContentUri(const std::string& path) {
56 constexpr std::string_view prefix = "content://";
57 if (path.size() < prefix.size()) [[unlikely]] {
58 return false;
59 }
60
61 return path.find(prefix) == 0;
62}
63
64int OpenContentUri(const std::string& filepath, OpenMode openmode) {
65 if (open_content_uri == nullptr)
66 return -1;
67
68 const char* mode = "";
69 switch (openmode) {
70 case OpenMode::Read:
71 mode = "r";
72 break;
73 default:
74 UNIMPLEMENTED();
75 return -1;
76 }
77 auto env = GetEnvForThread();
78 jstring j_filepath = env->NewStringUTF(filepath.c_str());
79 jstring j_mode = env->NewStringUTF(mode);
80 return env->CallStaticIntMethod(native_library, open_content_uri, j_filepath, j_mode);
81}
82
83#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) \
84 F(FunctionName, ReturnValue, JMethodID, Caller)
85#define F(FunctionName, ReturnValue, JMethodID, Caller) \
86 ReturnValue FunctionName(const std::string& filepath) { \
87 if (JMethodID == nullptr) { \
88 return 0; \
89 } \
90 auto env = GetEnvForThread(); \
91 jstring j_filepath = env->NewStringUTF(filepath.c_str()); \
92 return env->Caller(native_library, JMethodID, j_filepath); \
93 }
94ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
95#undef F
96#undef FR
97
98} // 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 @@
1// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <string>
7#include <vector>
8#include <jni.h>
9
10#define ANDROID_STORAGE_FUNCTIONS(V) \
11 V(OpenContentUri, int, (const std::string& filepath, OpenMode openmode), open_content_uri, \
12 "openContentUri", "(Ljava/lang/String;Ljava/lang/String;)I")
13
14#define ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(V) \
15 V(GetSize, std::uint64_t, get_size, CallStaticLongMethod, "getSize", "(Ljava/lang/String;)J")
16
17namespace Common::FS::Android {
18
19static JavaVM* g_jvm = nullptr;
20static jclass native_library = nullptr;
21
22#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) F(JMethodID)
23#define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature) F(JMethodID)
24#define F(JMethodID) static jmethodID JMethodID = nullptr;
25ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
26ANDROID_STORAGE_FUNCTIONS(FS)
27#undef F
28#undef FS
29#undef FR
30
31enum class OpenMode {
32 Read,
33 Write,
34 ReadWrite,
35 WriteAppend,
36 WriteTruncate,
37 ReadWriteAppend,
38 ReadWriteTruncate,
39 Never
40};
41
42void RegisterCallbacks(JNIEnv* env, jclass clazz);
43
44void UnRegisterCallbacks();
45
46bool IsContentUri(const std::string& path);
47
48#define FS(FunctionName, ReturnValue, Parameters, JMethodID, JMethodName, Signature) \
49 F(FunctionName, Parameters, ReturnValue)
50#define F(FunctionName, Parameters, ReturnValue) ReturnValue FunctionName Parameters;
51ANDROID_STORAGE_FUNCTIONS(FS)
52#undef F
53#undef FS
54
55#define FR(FunctionName, ReturnValue, JMethodID, Caller, JMethodName, Signature) \
56 F(FunctionName, ReturnValue)
57#define F(FunctionName, ReturnValue) ReturnValue FunctionName(const std::string& filepath);
58ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR)
59#undef F
60#undef FR
61
62} // 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 @@
6#include <unordered_map> 6#include <unordered_map>
7 7
8#include "common/fs/fs.h" 8#include "common/fs/fs.h"
9#ifdef ANDROID
10#include "common/fs/fs_android.h"
11#endif
9#include "common/fs/fs_paths.h" 12#include "common/fs/fs_paths.h"
10#include "common/fs/path_util.h" 13#include "common/fs/path_util.h"
11#include "common/logging/log.h" 14#include "common/logging/log.h"
@@ -80,9 +83,7 @@ public:
80 yuzu_paths.insert_or_assign(yuzu_path, new_path); 83 yuzu_paths.insert_or_assign(yuzu_path, new_path);
81 } 84 }
82 85
83private: 86 void Reinitialize(fs::path yuzu_path = {}) {
84 PathManagerImpl() {
85 fs::path yuzu_path;
86 fs::path yuzu_path_cache; 87 fs::path yuzu_path_cache;
87 fs::path yuzu_path_config; 88 fs::path yuzu_path_config;
88 89
@@ -96,12 +97,9 @@ private:
96 yuzu_path_cache = yuzu_path / CACHE_DIR; 97 yuzu_path_cache = yuzu_path / CACHE_DIR;
97 yuzu_path_config = yuzu_path / CONFIG_DIR; 98 yuzu_path_config = yuzu_path / CONFIG_DIR;
98#elif ANDROID 99#elif ANDROID
99 // On Android internal storage is mounted as "/sdcard" 100 ASSERT(!yuzu_path.empty());
100 if (Exists("/sdcard")) { 101 yuzu_path_cache = yuzu_path / CACHE_DIR;
101 yuzu_path = "/sdcard/yuzu-emu"; 102 yuzu_path_config = yuzu_path / CONFIG_DIR;
102 yuzu_path_cache = yuzu_path / CACHE_DIR;
103 yuzu_path_config = yuzu_path / CONFIG_DIR;
104 }
105#else 103#else
106 yuzu_path = GetCurrentDir() / PORTABLE_DIR; 104 yuzu_path = GetCurrentDir() / PORTABLE_DIR;
107 105
@@ -129,6 +127,11 @@ private:
129 GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR); 127 GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR);
130 } 128 }
131 129
130private:
131 PathManagerImpl() {
132 Reinitialize();
133 }
134
132 ~PathManagerImpl() = default; 135 ~PathManagerImpl() = default;
133 136
134 void GenerateYuzuPath(YuzuPath yuzu_path, const fs::path& new_path) { 137 void GenerateYuzuPath(YuzuPath yuzu_path, const fs::path& new_path) {
@@ -217,6 +220,10 @@ fs::path RemoveTrailingSeparators(const fs::path& path) {
217 return fs::path{string_path}; 220 return fs::path{string_path};
218} 221}
219 222
223void SetAppDirectory(const std::string& app_directory) {
224 PathManagerImpl::GetInstance().Reinitialize(app_directory);
225}
226
220const fs::path& GetYuzuPath(YuzuPath yuzu_path) { 227const fs::path& GetYuzuPath(YuzuPath yuzu_path) {
221 return PathManagerImpl::GetInstance().GetYuzuPathImpl(yuzu_path); 228 return PathManagerImpl::GetInstance().GetYuzuPathImpl(yuzu_path);
222} 229}
@@ -357,6 +364,12 @@ std::vector<std::string> SplitPathComponents(std::string_view filename) {
357 364
358std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) { 365std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) {
359 std::string path(path_); 366 std::string path(path_);
367#ifdef ANDROID
368 if (Android::IsContentUri(path)) {
369 return path;
370 }
371#endif // ANDROID
372
360 char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\'; 373 char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\';
361 char type2 = directory_separator == DirectorySeparator::BackwardSlash ? '\\' : '/'; 374 char type2 = directory_separator == DirectorySeparator::BackwardSlash ? '\\' : '/';
362 375
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
@@ -181,6 +181,14 @@ template <typename Path>
181#endif 181#endif
182 182
183/** 183/**
184 * Sets the directory used for application storage. Used on Android where we do not know internal
185 * storage until informed by the frontend.
186 *
187 * @param app_directory Directory to use for application storage.
188 */
189void SetAppDirectory(const std::string& app_directory);
190
191/**
184 * Gets the filesystem path associated with the YuzuPath enum. 192 * Gets the filesystem path associated with the YuzuPath enum.
185 * 193 *
186 * @param yuzu_path YuzuPath enum 194 * @param yuzu_path YuzuPath enum