android: Implement SAF support & migrate to SDK 31. (#4)
This commit is contained in:
parent
39ab81a098
commit
ef605f7d8f
|
@ -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());
|
||||
}
|
||||
|
||||
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"));
|
||||
addGamesRecursive(database, file, newExtensions, depth - 1);
|
||||
this.addGamesRecursive(database, file.getUri(), newExtensions, depth - 1);
|
||||
} else {
|
||||
String filePath = file.getPath();
|
||||
String filename = file.getUri().toString();
|
||||
|
||||
int extensionStart = filePath.lastIndexOf('.');
|
||||
int extensionStart = filename.lastIndexOf('.');
|
||||
if (extensionStart > 0) {
|
||||
String fileExtension = filePath.substring(extensionStart);
|
||||
String fileExtension = filename.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);
|
||||
}
|
||||
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)) {
|
||||
StartupHandler.handleInit(this);
|
||||
mPlatformGamesFragment = new PlatformGamesFragment();
|
||||
getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
|
||||
.commit();
|
||||
}
|
||||
getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment).commit();
|
||||
} else {
|
||||
mPlatformGamesFragment = (PlatformGamesFragment) getSupportFragmentManager().getFragment(savedInstanceState, "mPlatformGamesFragment");
|
||||
}
|
||||
|
@ -72,7 +63,6 @@ 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;
|
||||
}
|
||||
|
@ -81,7 +71,6 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
|||
}
|
||||
getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
|
@ -119,28 +108,18 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@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"));
|
||||
R.string.select_game_folder);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
PermissionsHandler.checkWritePermission(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
int offset = 0;
|
||||
int numRead;
|
||||
|
||||
try (InputStream is = new FileInputStream(file)) {
|
||||
while (offset < bytes.length
|
||||
&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
|
||||
offset += numRead;
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
// Ensure all the bytes have been read in
|
||||
if (offset < bytes.length) {
|
||||
throw new IOException("Could not completely read file " + file.getName());
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
return bytes;
|
||||
/**
|
||||
* 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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
private static void handleStartupPromptDismiss(MainActivity parent) {
|
||||
parent.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY);
|
||||
}
|
||||
|
||||
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 markFirstBoot() {
|
||||
final SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putBoolean("FirstApplicationLaunch", false);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static void HandleInit(FragmentActivity parent) {
|
||||
if (PermissionsHandler.isFirstBoot(parent)) {
|
||||
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() {
|
||||
|
|
|
@ -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,
|
||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_SetAppDirectory([[maybe_unused]] JNIEnv* env,
|
||||
[[maybe_unused]] jclass clazz,
|
||||
[[maybe_unused]] jstring j_directory) {}
|
||||
[[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>
|
||||
|
@ -33,23 +36,20 @@ JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchEvent(JN
|
|||
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,
|
||||
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_GetDescription(
|
||||
JNIEnv* env, jclass clazz, jstring j_filename);
|
||||
|
||||
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGameId(JNIEnv* env,
|
||||
jclass clazz,
|
||||
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,
|
||||
|
@ -63,8 +63,9 @@ JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetCompany(JNIEn
|
|||
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGitRevision(JNIEnv* env,
|
||||
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_);
|
||||
|
@ -78,8 +79,7 @@ JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_CreateConfigFile(JN
|
|||
|
||||
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,
|
||||
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,
|
||||
|
@ -102,8 +102,7 @@ JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceChanged(JNIE
|
|||
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env,
|
||||
jclass clazz);
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_InitGameIni(JNIEnv* env,
|
||||
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_ReloadSettings(JNIEnv* env,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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";
|
||||
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