diff options
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 | ||
| 146 | def getVersion() { | 144 | def getVersion() { |
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index 0d7e3f7ad..88e1669cd 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml | |||
| @@ -31,6 +31,7 @@ | |||
| 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; | |||
| 25 | import androidx.fragment.app.DialogFragment; | 25 | import androidx.fragment.app.DialogFragment; |
| 26 | 26 | ||
| 27 | import org.yuzu.yuzu_emu.activities.EmulationActivity; | 27 | import org.yuzu.yuzu_emu.activities.EmulationActivity; |
| 28 | import org.yuzu.yuzu_emu.utils.DocumentsTree; | ||
| 28 | import org.yuzu.yuzu_emu.utils.EmulationMenuSettings; | 29 | import org.yuzu.yuzu_emu.utils.EmulationMenuSettings; |
| 30 | import org.yuzu.yuzu_emu.utils.FileUtil; | ||
| 29 | import org.yuzu.yuzu_emu.utils.Log; | 31 | import org.yuzu.yuzu_emu.utils.Log; |
| 30 | 32 | ||
| 31 | import java.lang.ref.WeakReference; | 33 | import 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; | |||
| 11 | import android.os.Build; | 11 | import android.os.Build; |
| 12 | 12 | ||
| 13 | import org.yuzu.yuzu_emu.model.GameDatabase; | 13 | import org.yuzu.yuzu_emu.model.GameDatabase; |
| 14 | import org.yuzu.yuzu_emu.utils.DocumentsTree; | ||
| 14 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization; | 15 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization; |
| 15 | import org.yuzu.yuzu_emu.utils.PermissionsHandler; | ||
| 16 | 16 | ||
| 17 | public class YuzuApplication extends Application { | 17 | public 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 @@ | |||
| 1 | package org.yuzu.yuzu_emu.activities; | ||
| 2 | |||
| 3 | import android.content.Intent; | ||
| 4 | import android.os.Environment; | ||
| 5 | |||
| 6 | import androidx.annotation.Nullable; | ||
| 7 | |||
| 8 | import com.nononsenseapps.filepicker.AbstractFilePickerFragment; | ||
| 9 | import com.nononsenseapps.filepicker.FilePickerActivity; | ||
| 10 | |||
| 11 | import org.yuzu.yuzu_emu.fragments.CustomFilePickerFragment; | ||
| 12 | |||
| 13 | import java.io.File; | ||
| 14 | |||
| 15 | public 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; | |||
| 16 | import androidx.fragment.app.FragmentActivity; | 16 | import androidx.fragment.app.FragmentActivity; |
| 17 | import androidx.recyclerview.widget.RecyclerView; | 17 | import androidx.recyclerview.widget.RecyclerView; |
| 18 | 18 | ||
| 19 | import org.yuzu.yuzu_emu.YuzuApplication; | ||
| 19 | import org.yuzu.yuzu_emu.R; | 20 | import org.yuzu.yuzu_emu.R; |
| 20 | import org.yuzu.yuzu_emu.activities.EmulationActivity; | 21 | import org.yuzu.yuzu_emu.activities.EmulationActivity; |
| 21 | import org.yuzu.yuzu_emu.model.GameDatabase; | 22 | import org.yuzu.yuzu_emu.model.GameDatabase; |
| 22 | import org.yuzu.yuzu_emu.ui.DividerItemDecoration; | 23 | import org.yuzu.yuzu_emu.ui.DividerItemDecoration; |
| 24 | import org.yuzu.yuzu_emu.utils.FileUtil; | ||
| 23 | import org.yuzu.yuzu_emu.utils.Log; | 25 | import org.yuzu.yuzu_emu.utils.Log; |
| 24 | import org.yuzu.yuzu_emu.utils.PicassoUtils; | 26 | import org.yuzu.yuzu_emu.utils.PicassoUtils; |
| 25 | import org.yuzu.yuzu_emu.viewholders.GameViewHolder; | 27 | import org.yuzu.yuzu_emu.viewholders.GameViewHolder; |
| 26 | 28 | ||
| 27 | import java.nio.file.Path; | ||
| 28 | import java.nio.file.Paths; | ||
| 29 | import java.util.stream.Stream; | 29 | import 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 @@ | |||
| 1 | package org.yuzu.yuzu_emu.fragments; | ||
| 2 | |||
| 3 | import android.net.Uri; | ||
| 4 | import android.os.Bundle; | ||
| 5 | import android.os.Environment; | ||
| 6 | import android.view.LayoutInflater; | ||
| 7 | import android.view.View; | ||
| 8 | import android.view.ViewGroup; | ||
| 9 | import android.widget.TextView; | ||
| 10 | |||
| 11 | import androidx.annotation.NonNull; | ||
| 12 | import androidx.appcompat.widget.Toolbar; | ||
| 13 | import androidx.core.content.FileProvider; | ||
| 14 | |||
| 15 | import com.nononsenseapps.filepicker.FilePickerFragment; | ||
| 16 | |||
| 17 | import org.yuzu.yuzu_emu.R; | ||
| 18 | |||
| 19 | import java.io.File; | ||
| 20 | import java.util.Arrays; | ||
| 21 | import java.util.Collections; | ||
| 22 | import java.util.List; | ||
| 23 | |||
| 24 | public 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; | |||
| 5 | import android.database.Cursor; | 5 | import android.database.Cursor; |
| 6 | import android.database.sqlite.SQLiteDatabase; | 6 | import android.database.sqlite.SQLiteDatabase; |
| 7 | import android.database.sqlite.SQLiteOpenHelper; | 7 | import android.database.sqlite.SQLiteOpenHelper; |
| 8 | import android.net.Uri; | ||
| 8 | 9 | ||
| 9 | import org.yuzu.yuzu_emu.NativeLibrary; | 10 | import org.yuzu.yuzu_emu.NativeLibrary; |
| 11 | import org.yuzu.yuzu_emu.utils.FileUtil; | ||
| 10 | import org.yuzu.yuzu_emu.utils.Log; | 12 | import org.yuzu.yuzu_emu.utils.Log; |
| 11 | 13 | ||
| 12 | import java.io.File; | 14 | import 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 @@ | |||
| 1 | package org.yuzu.yuzu_emu.model; | ||
| 2 | |||
| 3 | import android.net.Uri; | ||
| 4 | import android.provider.DocumentsContract; | ||
| 5 | |||
| 6 | public 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 @@ | |||
| 1 | package org.yuzu.yuzu_emu.ui.main; | 1 | package org.yuzu.yuzu_emu.ui.main; |
| 2 | 2 | ||
| 3 | import android.content.Intent; | 3 | import android.content.Intent; |
| 4 | import android.content.pm.PackageManager; | 4 | import android.net.Uri; |
| 5 | import android.os.Bundle; | 5 | import android.os.Bundle; |
| 6 | import android.view.Menu; | 6 | import android.view.Menu; |
| 7 | import android.view.MenuInflater; | 7 | import android.view.MenuInflater; |
| 8 | import android.view.MenuItem; | 8 | import android.view.MenuItem; |
| 9 | import android.widget.Toast; | ||
| 10 | 9 | ||
| 11 | import androidx.annotation.NonNull; | 10 | import androidx.annotation.NonNull; |
| 12 | import androidx.appcompat.app.AppCompatActivity; | 11 | import androidx.appcompat.app.AppCompatActivity; |
| @@ -18,16 +17,11 @@ import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity; | |||
| 18 | import org.yuzu.yuzu_emu.model.GameProvider; | 17 | import org.yuzu.yuzu_emu.model.GameProvider; |
| 19 | import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment; | 18 | import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment; |
| 20 | import org.yuzu.yuzu_emu.utils.AddDirectoryHelper; | 19 | import org.yuzu.yuzu_emu.utils.AddDirectoryHelper; |
| 21 | import org.yuzu.yuzu_emu.utils.DirectoryInitialization; | ||
| 22 | import org.yuzu.yuzu_emu.utils.FileBrowserHelper; | 20 | import org.yuzu.yuzu_emu.utils.FileBrowserHelper; |
| 23 | import org.yuzu.yuzu_emu.utils.PermissionsHandler; | ||
| 24 | import org.yuzu.yuzu_emu.utils.PicassoUtils; | 21 | import org.yuzu.yuzu_emu.utils.PicassoUtils; |
| 25 | import org.yuzu.yuzu_emu.utils.StartupHandler; | 22 | import org.yuzu.yuzu_emu.utils.StartupHandler; |
| 26 | import org.yuzu.yuzu_emu.utils.ThemeUtil; | 23 | import org.yuzu.yuzu_emu.utils.ThemeUtil; |
| 27 | 24 | ||
| 28 | import java.util.Arrays; | ||
| 29 | import 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 | |||
| 7 | package org.yuzu.yuzu_emu.utils; | 1 | package org.yuzu.yuzu_emu.utils; |
| 8 | 2 | ||
| 9 | import android.content.Context; | 3 | import android.content.Context; |
| 10 | import android.content.Intent; | 4 | import android.content.Intent; |
| 11 | import android.content.SharedPreferences; | ||
| 12 | import android.os.Environment; | ||
| 13 | import android.preference.PreferenceManager; | ||
| 14 | |||
| 15 | import androidx.localbroadcastmanager.content.LocalBroadcastManager; | 5 | import androidx.localbroadcastmanager.content.LocalBroadcastManager; |
| 16 | 6 | ||
| 17 | import org.yuzu.yuzu_emu.NativeLibrary; | 7 | import org.yuzu.yuzu_emu.NativeLibrary; |
| 18 | 8 | ||
| 19 | import java.io.File; | ||
| 20 | import java.io.FileOutputStream; | ||
| 21 | import java.io.IOException; | 9 | import java.io.IOException; |
| 22 | import java.io.InputStream; | ||
| 23 | import java.io.OutputStream; | ||
| 24 | import java.util.concurrent.atomic.AtomicBoolean; | 10 | import 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 | */ | ||
| 30 | public final class DirectoryInitialization { | 12 | public 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 @@ | |||
| 1 | package org.yuzu.yuzu_emu.utils; | ||
| 2 | |||
| 3 | import android.content.Context; | ||
| 4 | import android.net.Uri; | ||
| 5 | |||
| 6 | import androidx.annotation.Nullable; | ||
| 7 | import androidx.documentfile.provider.DocumentFile; | ||
| 8 | |||
| 9 | import org.yuzu.yuzu_emu.YuzuApplication; | ||
| 10 | import org.yuzu.yuzu_emu.model.MinimalDocumentFile; | ||
| 11 | |||
| 12 | import java.util.HashMap; | ||
| 13 | import java.util.Map; | ||
| 14 | import java.util.StringTokenizer; | ||
| 15 | |||
| 16 | public 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 @@ | |||
| 1 | package org.yuzu.yuzu_emu.utils; | 1 | package org.yuzu.yuzu_emu.utils; |
| 2 | 2 | ||
| 3 | import android.content.Intent; | 3 | import android.content.Intent; |
| 4 | import android.net.Uri; | ||
| 5 | import android.os.Environment; | ||
| 6 | |||
| 7 | import androidx.annotation.Nullable; | ||
| 8 | import androidx.fragment.app.FragmentActivity; | 4 | import androidx.fragment.app.FragmentActivity; |
| 9 | 5 | ||
| 10 | import com.nononsenseapps.filepicker.FilePickerActivity; | ||
| 11 | import com.nononsenseapps.filepicker.Utils; | ||
| 12 | |||
| 13 | import org.yuzu.yuzu_emu.activities.CustomFilePickerActivity; | ||
| 14 | |||
| 15 | import java.io.File; | ||
| 16 | import java.util.List; | ||
| 17 | |||
| 18 | public final class FileBrowserHelper { | 6 | public 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 @@ | |||
| 1 | package org.yuzu.yuzu_emu.utils; | 1 | package org.yuzu.yuzu_emu.utils; |
| 2 | 2 | ||
| 3 | import java.io.File; | 3 | import android.content.ContentResolver; |
| 4 | import java.io.FileInputStream; | 4 | import android.content.Context; |
| 5 | import java.io.IOException; | 5 | import android.database.Cursor; |
| 6 | import android.net.Uri; | ||
| 7 | import android.os.ParcelFileDescriptor; | ||
| 8 | import android.provider.DocumentsContract; | ||
| 9 | |||
| 10 | import androidx.annotation.Nullable; | ||
| 11 | import androidx.documentfile.provider.DocumentFile; | ||
| 12 | |||
| 13 | import org.yuzu.yuzu_emu.model.MinimalDocumentFile; | ||
| 14 | |||
| 6 | import java.io.InputStream; | 15 | import java.io.InputStream; |
| 16 | import java.io.OutputStream; | ||
| 17 | import java.net.URLDecoder; | ||
| 18 | import java.util.ArrayList; | ||
| 19 | import java.util.List; | ||
| 7 | 20 | ||
| 8 | public class FileUtil { | 21 | public 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 @@ | |||
| 1 | package org.yuzu.yuzu_emu.utils; | ||
| 2 | |||
| 3 | import android.annotation.TargetApi; | ||
| 4 | import android.content.Context; | ||
| 5 | import android.content.pm.PackageManager; | ||
| 6 | import android.os.Build; | ||
| 7 | |||
| 8 | import androidx.core.content.ContextCompat; | ||
| 9 | import androidx.fragment.app.FragmentActivity; | ||
| 10 | |||
| 11 | import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; | ||
| 12 | |||
| 13 | public 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 @@ | |||
| 1 | package org.yuzu.yuzu_emu.utils; | 1 | package org.yuzu.yuzu_emu.utils; |
| 2 | 2 | ||
| 3 | import android.content.Intent; | 3 | import android.content.SharedPreferences; |
| 4 | import android.os.Bundle; | 4 | import android.preference.PreferenceManager; |
| 5 | import android.text.TextUtils; | ||
| 6 | |||
| 7 | import androidx.appcompat.app.AlertDialog; | 5 | import androidx.appcompat.app.AlertDialog; |
| 8 | import androidx.fragment.app.FragmentActivity; | ||
| 9 | 6 | ||
| 10 | import org.yuzu.yuzu_emu.R; | 7 | import org.yuzu.yuzu_emu.R; |
| 11 | import org.yuzu.yuzu_emu.activities.EmulationActivity; | 8 | import org.yuzu.yuzu_emu.YuzuApplication; |
| 9 | import org.yuzu.yuzu_emu.ui.main.MainActivity; | ||
| 10 | import org.yuzu.yuzu_emu.ui.main.MainPresenter; | ||
| 12 | 11 | ||
| 13 | public final class StartupHandler { | 12 | public 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 | ||
| 19 | namespace FS = Common::FS; | 19 | namespace FS = Common::FS; |
| 20 | 20 | ||
| 21 | const std::filesystem::path default_config_path = | ||
| 22 | FS::GetYuzuPath(FS::YuzuPath::ConfigDir) / "config.ini"; | ||
| 23 | |||
| 24 | Config::Config(std::optional<std::filesystem::path> config_path) | 21 | Config::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 | ||
| 67 | template <typename Type, bool ranged> | 64 | template <typename Type, bool ranged> |
| 68 | void Config::ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting) { | 65 | void 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 | ||
| 73 | void Config::ReadValues() { | 70 | void 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 | ||
| 3 | static JavaVM* s_java_vm; | 9 | static JavaVM* s_java_vm; |
| 4 | static jclass s_native_library_class; | 10 | static jclass s_native_library_class; |
| 5 | static jmethodID s_exit_emulation_activity; | 11 | static jmethodID s_exit_emulation_activity; |
| 6 | 12 | ||
| 13 | static constexpr jint JNI_VERSION = JNI_VERSION_1_6; | ||
| 14 | |||
| 7 | namespace IDCache { | 15 | namespace IDCache { |
| 8 | 16 | ||
| 9 | JNIEnv* GetEnvForThread() { | 17 | JNIEnv* GetEnvForThread() { |
| @@ -34,3 +42,41 @@ jmethodID GetExitEmulationActivity() { | |||
| 34 | } | 42 | } |
| 35 | 43 | ||
| 36 | } // namespace IDCache | 44 | } // namespace IDCache |
| 45 | |||
| 46 | #ifdef __cplusplus | ||
| 47 | extern "C" { | ||
| 48 | #endif | ||
| 49 | |||
| 50 | jint 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 | |||
| 69 | void 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 | ||
| 260 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserDirectory([[maybe_unused]] JNIEnv* env, | 264 | void 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 | ||
| 264 | void Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation([[maybe_unused]] JNIEnv* env, | 270 | void 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 | ||
| 10 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation(JNIEnv* env, | 13 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation(JNIEnv* env, |
| 11 | jclass clazz); | 14 | jclass clazz); |
| 12 | 15 | ||
| 13 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation(JNIEnv* env, | 16 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_PauseEmulation(JNIEnv* env, |
| 14 | jclass clazz); | 17 | jclass clazz); |
| 15 | 18 | ||
| 16 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation(JNIEnv* env, | 19 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_StopEmulation(JNIEnv* env, |
| 17 | jclass clazz); | 20 | jclass clazz); |
| 18 | 21 | ||
| 19 | JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning(JNIEnv* env, | 22 | JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_IsRunning(JNIEnv* env, |
| 20 | jclass clazz); | 23 | jclass clazz); |
| 21 | 24 | ||
| 22 | JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadEvent( | 25 | JNIEXPORT 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 | ||
| 31 | JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchEvent(JNIEnv* env, | 34 | JNIEXPORT 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 | ||
| 36 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, | 39 | JNIEXPORT 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 | ||
| 40 | JNIEXPORT jintArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon(JNIEnv* env, | 42 | JNIEXPORT 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 | ||
| 44 | JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetTitle(JNIEnv* env, | 45 | JNIEXPORT 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 | ||
| 48 | JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetDescription( | 48 | JNIEXPORT 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 | ||
| 51 | JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGameId(JNIEnv* env, | 52 | JNIEXPORT 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 | ||
| 55 | JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetRegions(JNIEnv* env, | 55 | JNIEXPORT 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 | ||
| 59 | JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetCompany(JNIEnv* env, | 59 | JNIEXPORT 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 | ||
| 63 | JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGitRevision(JNIEnv* env, | 63 | JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGitRevision(JNIEnv* env, |
| 64 | jclass clazz); | 64 | jclass clazz); |
| 65 | 65 | ||
| 66 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserDirectory( | 66 | JNIEXPORT 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 | ||
| 69 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_SetSysDirectory( | 70 | JNIEXPORT 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 | ||
| 72 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetSysDirectory(JNIEnv* env, | 73 | JNIEXPORT 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 | ||
| 76 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env, | 77 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env, |
| 77 | jclass clazz); | 78 | jclass clazz); |
| 78 | 79 | ||
| 79 | JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env, | 80 | JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env, |
| 80 | jclass clazz); | 81 | jclass clazz); |
| 81 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetProfiling(JNIEnv* env, | 82 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetProfiling(JNIEnv* env, jclass clazz, |
| 82 | jclass clazz, | 83 | jboolean enable); |
| 83 | jboolean enable); | ||
| 84 | 84 | ||
| 85 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_WriteProfileResults(JNIEnv* env, | 85 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_WriteProfileResults(JNIEnv* env, |
| 86 | jclass clazz); | 86 | jclass clazz); |
| 87 | 87 | ||
| 88 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_NotifyOrientationChange( | 88 | JNIEXPORT 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 | ||
| 98 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env, | 98 | JNIEXPORT 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 | ||
| 102 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env, | 102 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env, |
| 103 | jclass clazz); | 103 | jclass clazz); |
| 104 | 104 | ||
| 105 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_InitGameIni(JNIEnv* env, | 105 | JNIEXPORT 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 | ||
| 109 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadSettings(JNIEnv* env, | 108 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadSettings(JNIEnv* env, |
| 110 | jclass clazz); | 109 | jclass clazz); |
| 111 | 110 | ||
| 112 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserSetting( | 111 | JNIEXPORT 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 | ||
| 119 | JNIEXPORT jdoubleArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetPerfStats(JNIEnv* env, | 118 | JNIEXPORT jdoubleArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetPerfStats(JNIEnv* env, |
| 120 | jclass clazz); | 119 | jclass clazz); |
| 121 | 120 | ||
| 122 | JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env, | 121 | JNIEXPORT 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) |
| 156 | endif() | 156 | endif() |
| 157 | 157 | ||
| 158 | if(ANDROID) | ||
| 159 | target_sources(common | ||
| 160 | PRIVATE | ||
| 161 | fs/fs_android.cpp | ||
| 162 | fs/fs_android.h | ||
| 163 | ) | ||
| 164 | endif() | ||
| 165 | |||
| 158 | if(ARCHITECTURE_x86_64) | 166 | if(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 | |||
| 6 | namespace Common::FS::Android { | ||
| 7 | |||
| 8 | JNIEnv* 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 | |||
| 27 | void 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 | |||
| 44 | void 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 | |||
| 55 | bool 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 | |||
| 64 | int 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 | } | ||
| 94 | ANDROID_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 | |||
| 17 | namespace Common::FS::Android { | ||
| 18 | |||
| 19 | static JavaVM* g_jvm = nullptr; | ||
| 20 | static 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; | ||
| 25 | ANDROID_SINGLE_PATH_DETERMINE_FUNCTIONS(FR) | ||
| 26 | ANDROID_STORAGE_FUNCTIONS(FS) | ||
| 27 | #undef F | ||
| 28 | #undef FS | ||
| 29 | #undef FR | ||
| 30 | |||
| 31 | enum class OpenMode { | ||
| 32 | Read, | ||
| 33 | Write, | ||
| 34 | ReadWrite, | ||
| 35 | WriteAppend, | ||
| 36 | WriteTruncate, | ||
| 37 | ReadWriteAppend, | ||
| 38 | ReadWriteTruncate, | ||
| 39 | Never | ||
| 40 | }; | ||
| 41 | |||
| 42 | void RegisterCallbacks(JNIEnv* env, jclass clazz); | ||
| 43 | |||
| 44 | void UnRegisterCallbacks(); | ||
| 45 | |||
| 46 | bool 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; | ||
| 51 | ANDROID_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); | ||
| 58 | ANDROID_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 | ||
| 83 | private: | 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 | ||
| 130 | private: | ||
| 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 | ||
| 223 | void SetAppDirectory(const std::string& app_directory) { | ||
| 224 | PathManagerImpl::GetInstance().Reinitialize(app_directory); | ||
| 225 | } | ||
| 226 | |||
| 220 | const fs::path& GetYuzuPath(YuzuPath yuzu_path) { | 227 | const 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 | ||
| 358 | std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) { | 365 | std::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 | */ | ||
| 189 | void 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 |