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