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
|
// TODO If this is ever modified, change application_id in strings.xml
|
||||||
applicationId "org.yuzu.yuzu_emu"
|
applicationId "org.yuzu.yuzu_emu"
|
||||||
minSdkVersion 28
|
minSdkVersion 28
|
||||||
targetSdkVersion 29
|
targetSdkVersion 31
|
||||||
versionCode autoVersion
|
versionCode autoVersion
|
||||||
versionName getVersion()
|
versionName getVersion()
|
||||||
ndk.abiFilters abiFilter
|
ndk.abiFilters abiFilter
|
||||||
|
@ -126,6 +126,7 @@ dependencies {
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1'
|
||||||
implementation 'androidx.fragment:fragment:1.5.3'
|
implementation 'androidx.fragment:fragment:1.5.3'
|
||||||
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
|
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
|
||||||
|
implementation "androidx.documentfile:documentfile:1.0.1"
|
||||||
implementation 'com.google.android.material:material:1.6.1'
|
implementation 'com.google.android.material:material:1.6.1'
|
||||||
|
|
||||||
// For loading huge screenshots from the disk.
|
// For loading huge screenshots from the disk.
|
||||||
|
@ -138,9 +139,6 @@ dependencies {
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout: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() {
|
def getVersion() {
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.yuzu.yuzu_emu.ui.main.MainActivity"
|
android:name="org.yuzu.yuzu_emu.ui.main.MainActivity"
|
||||||
|
android:exported="true"
|
||||||
android:theme="@style/YuzuBase"
|
android:theme="@style/YuzuBase"
|
||||||
android:resizeableActivity="false">
|
android:resizeableActivity="false">
|
||||||
|
|
||||||
|
@ -57,18 +58,6 @@
|
||||||
|
|
||||||
<service android:name="org.yuzu.yuzu_emu.utils.ForegroundService"/>
|
<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
|
<provider
|
||||||
android:name="org.yuzu.yuzu_emu.model.GameProvider"
|
android:name="org.yuzu.yuzu_emu.model.GameProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
|
|
|
@ -25,7 +25,9 @@ import androidx.core.content.ContextCompat;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
|
||||||
import org.yuzu.yuzu_emu.activities.EmulationActivity;
|
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.EmulationMenuSettings;
|
||||||
|
import org.yuzu.yuzu_emu.utils.FileUtil;
|
||||||
import org.yuzu.yuzu_emu.utils.Log;
|
import org.yuzu.yuzu_emu.utils.Log;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
|
@ -66,6 +68,20 @@ public final class NativeLibrary {
|
||||||
// Disallows instantiation.
|
// 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.
|
* Handles button press events for a gamepad.
|
||||||
*
|
*
|
||||||
|
@ -147,11 +163,7 @@ public final class NativeLibrary {
|
||||||
|
|
||||||
public static native String GetGitRevision();
|
public static native String GetGitRevision();
|
||||||
|
|
||||||
/**
|
public static native void SetAppDirectory(String directory);
|
||||||
* Sets the current working user directory
|
|
||||||
* If not set, it auto-detects a location
|
|
||||||
*/
|
|
||||||
public static native void SetUserDirectory(String directory);
|
|
||||||
|
|
||||||
// Create the config.ini file.
|
// Create the config.ini file.
|
||||||
public static native void CreateConfigFile();
|
public static native void CreateConfigFile();
|
||||||
|
|
|
@ -11,11 +11,12 @@ import android.content.Context;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import org.yuzu.yuzu_emu.model.GameDatabase;
|
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.DirectoryInitialization;
|
||||||
import org.yuzu.yuzu_emu.utils.PermissionsHandler;
|
|
||||||
|
|
||||||
public class YuzuApplication extends Application {
|
public class YuzuApplication extends Application {
|
||||||
public static GameDatabase databaseHelper;
|
public static GameDatabase databaseHelper;
|
||||||
|
public static DocumentsTree documentsTree;
|
||||||
private static YuzuApplication application;
|
private static YuzuApplication application;
|
||||||
|
|
||||||
private void createNotificationChannel() {
|
private void createNotificationChannel() {
|
||||||
|
@ -39,10 +40,9 @@ public class YuzuApplication extends Application {
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
application = this;
|
application = this;
|
||||||
|
documentsTree = new DocumentsTree();
|
||||||
|
|
||||||
if (PermissionsHandler.hasWriteAccess(getApplicationContext())) {
|
DirectoryInitialization.start(getApplicationContext());
|
||||||
DirectoryInitialization.start(getApplicationContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
NativeLibrary.LogDeviceInfo();
|
NativeLibrary.LogDeviceInfo();
|
||||||
createNotificationChannel();
|
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.fragment.app.FragmentActivity;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.yuzu.yuzu_emu.YuzuApplication;
|
||||||
import org.yuzu.yuzu_emu.R;
|
import org.yuzu.yuzu_emu.R;
|
||||||
import org.yuzu.yuzu_emu.activities.EmulationActivity;
|
import org.yuzu.yuzu_emu.activities.EmulationActivity;
|
||||||
import org.yuzu.yuzu_emu.model.GameDatabase;
|
import org.yuzu.yuzu_emu.model.GameDatabase;
|
||||||
import org.yuzu.yuzu_emu.ui.DividerItemDecoration;
|
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.Log;
|
||||||
import org.yuzu.yuzu_emu.utils.PicassoUtils;
|
import org.yuzu.yuzu_emu.utils.PicassoUtils;
|
||||||
import org.yuzu.yuzu_emu.viewholders.GameViewHolder;
|
import org.yuzu.yuzu_emu.viewholders.GameViewHolder;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.stream.Stream;
|
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.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
|
||||||
holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
|
holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
|
||||||
|
|
||||||
final Path gamePath = Paths.get(mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
|
String filepath = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
|
||||||
holder.textFileName.setText(gamePath.getFileName().toString());
|
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.
|
// 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);
|
holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID);
|
||||||
|
|
|
@ -159,12 +159,6 @@ public final class SettingsActivity extends AppCompatActivity implements Setting
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void showPermissionNeededHint() {
|
|
||||||
Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showExternalStorageNotMountedHint() {
|
public void showExternalStorageNotMountedHint() {
|
||||||
Toast.makeText(this, R.string.external_storage_not_mounted, Toast.LENGTH_SHORT)
|
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) {
|
if (directoryInitializationState == DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) {
|
||||||
mView.hideLoading();
|
mView.hideLoading();
|
||||||
loadSettingsUI();
|
loadSettingsUI();
|
||||||
} else if (directoryInitializationState == DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED) {
|
|
||||||
mView.showPermissionNeededHint();
|
|
||||||
mView.hideLoading();
|
|
||||||
} else if (directoryInitializationState == DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
|
} else if (directoryInitializationState == DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
|
||||||
mView.showExternalStorageNotMountedHint();
|
mView.showExternalStorageNotMountedHint();
|
||||||
mView.hideLoading();
|
mView.hideLoading();
|
||||||
|
|
|
@ -76,11 +76,6 @@ public interface SettingsActivityView {
|
||||||
*/
|
*/
|
||||||
void hideLoading();
|
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
|
* 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 ==
|
if (directoryInitializationState ==
|
||||||
DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) {
|
DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) {
|
||||||
mEmulationState.run(activity.isActivityRecreated());
|
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 ==
|
} else if (directoryInitializationState ==
|
||||||
DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
|
DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE) {
|
||||||
Toast.makeText(getContext(), R.string.external_storage_not_mounted,
|
Toast.makeText(getContext(), R.string.external_storage_not_mounted,
|
||||||
|
|
|
@ -5,8 +5,10 @@ import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary;
|
import org.yuzu.yuzu_emu.NativeLibrary;
|
||||||
|
import org.yuzu.yuzu_emu.utils.FileUtil;
|
||||||
import org.yuzu.yuzu_emu.utils.Log;
|
import org.yuzu.yuzu_emu.utils.Log;
|
||||||
|
|
||||||
import java.io.File;
|
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_FOLDERS = "DROP TABLE IF EXISTS " + TABLE_NAME_FOLDERS;
|
||||||
private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES;
|
private static final String SQL_DELETE_GAMES = "DROP TABLE IF EXISTS " + TABLE_NAME_GAMES;
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
public GameDatabase(Context context) {
|
public GameDatabase(Context context) {
|
||||||
// Superclass constructor builds a database or uses an existing one.
|
// Superclass constructor builds a database or uses an existing one.
|
||||||
super(context, "games.db", null, DB_VERSION);
|
super(context, "games.db", null, DB_VERSION);
|
||||||
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -123,8 +127,6 @@ public final class GameDatabase extends SQLiteOpenHelper {
|
||||||
File game = new File(gamePath);
|
File game = new File(gamePath);
|
||||||
|
|
||||||
if (!game.exists()) {
|
if (!game.exists()) {
|
||||||
Log.error("[GameDatabase] Game file no longer exists. Removing from the library: " +
|
|
||||||
gamePath);
|
|
||||||
database.delete(TABLE_NAME_GAMES,
|
database.delete(TABLE_NAME_GAMES,
|
||||||
KEY_DB_ID + " = ?",
|
KEY_DB_ID + " = ?",
|
||||||
new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))});
|
new String[]{Long.toString(fileCursor.getLong(COLUMN_DB_ID))});
|
||||||
|
@ -150,9 +152,9 @@ public final class GameDatabase extends SQLiteOpenHelper {
|
||||||
while (folderCursor.moveToNext()) {
|
while (folderCursor.moveToNext()) {
|
||||||
String folderPath = folderCursor.getString(FOLDER_COLUMN_PATH);
|
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 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(
|
Log.error(
|
||||||
"[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath);
|
"[GameDatabase] Folder no longer exists. Removing from the library: " + folderPath);
|
||||||
database.delete(TABLE_NAME_FOLDERS,
|
database.delete(TABLE_NAME_FOLDERS,
|
||||||
|
@ -160,7 +162,7 @@ public final class GameDatabase extends SQLiteOpenHelper {
|
||||||
new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))});
|
new String[]{Long.toString(folderCursor.getLong(COLUMN_DB_ID))});
|
||||||
}
|
}
|
||||||
|
|
||||||
addGamesRecursive(database, folder, allowedExtensions, 3);
|
this.addGamesRecursive(database, folderUri, allowedExtensions, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
fileCursor.close();
|
fileCursor.close();
|
||||||
|
@ -169,33 +171,27 @@ public final class GameDatabase extends SQLiteOpenHelper {
|
||||||
database.close();
|
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) {
|
if (depth <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
File[] children = parent.listFiles();
|
MinimalDocumentFile[] children = FileUtil.listFiles(context, parent);
|
||||||
if (children != null) {
|
for (MinimalDocumentFile file : children) {
|
||||||
for (File file : children) {
|
if (file.isDirectory()) {
|
||||||
if (file.isHidden()) {
|
Set<String> newExtensions = new HashSet<>(Arrays.asList(
|
||||||
continue;
|
".xci", ".nsp", ".nca", ".nro"));
|
||||||
}
|
this.addGamesRecursive(database, file.getUri(), newExtensions, depth - 1);
|
||||||
|
} else {
|
||||||
|
String filename = file.getUri().toString();
|
||||||
|
|
||||||
if (file.isDirectory()) {
|
int extensionStart = filename.lastIndexOf('.');
|
||||||
Set<String> newExtensions = new HashSet<>(Arrays.asList(
|
if (extensionStart > 0) {
|
||||||
".xci", ".nsp", ".nca", ".nro"));
|
String fileExtension = filename.substring(extensionStart);
|
||||||
addGamesRecursive(database, file, newExtensions, depth - 1);
|
|
||||||
} else {
|
|
||||||
String filePath = file.getPath();
|
|
||||||
|
|
||||||
int extensionStart = filePath.lastIndexOf('.');
|
// Check that the file has an extension we care about before trying to read out of it.
|
||||||
if (extensionStart > 0) {
|
if (allowedExtensions.contains(fileExtension.toLowerCase())) {
|
||||||
String fileExtension = filePath.substring(extensionStart);
|
attemptToAddGame(database, filename);
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
package org.yuzu.yuzu_emu.ui.main;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
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.model.GameProvider;
|
||||||
import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment;
|
import org.yuzu.yuzu_emu.ui.platform.PlatformGamesFragment;
|
||||||
import org.yuzu.yuzu_emu.utils.AddDirectoryHelper;
|
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.FileBrowserHelper;
|
||||||
import org.yuzu.yuzu_emu.utils.PermissionsHandler;
|
|
||||||
import org.yuzu.yuzu_emu.utils.PicassoUtils;
|
import org.yuzu.yuzu_emu.utils.PicassoUtils;
|
||||||
import org.yuzu.yuzu_emu.utils.StartupHandler;
|
import org.yuzu.yuzu_emu.utils.StartupHandler;
|
||||||
import org.yuzu.yuzu_emu.utils.ThemeUtil;
|
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
|
* 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.
|
* 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();
|
mPresenter.onCreate();
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
StartupHandler.HandleInit(this);
|
StartupHandler.handleInit(this);
|
||||||
if (PermissionsHandler.hasWriteAccess(this)) {
|
mPlatformGamesFragment = new PlatformGamesFragment();
|
||||||
mPlatformGamesFragment = new PlatformGamesFragment();
|
getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment).commit();
|
||||||
getSupportFragmentManager().beginTransaction().add(mFrameLayoutId, mPlatformGamesFragment)
|
|
||||||
.commit();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
mPlatformGamesFragment = (PlatformGamesFragment) getSupportFragmentManager().getFragment(savedInstanceState, "mPlatformGamesFragment");
|
mPlatformGamesFragment = (PlatformGamesFragment) getSupportFragmentManager().getFragment(savedInstanceState, "mPlatformGamesFragment");
|
||||||
}
|
}
|
||||||
|
@ -72,15 +63,13 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
if (PermissionsHandler.hasWriteAccess(this)) {
|
if (getSupportFragmentManager() == null) {
|
||||||
if (getSupportFragmentManager() == null) {
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (outState == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment);
|
|
||||||
}
|
}
|
||||||
|
if (outState == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getSupportFragmentManager().putFragment(outState, "mPlatformGamesFragment", mPlatformGamesFragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -119,27 +108,17 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void launchSettingsActivity(String menuTag) {
|
public void launchSettingsActivity(String menuTag) {
|
||||||
if (PermissionsHandler.hasWriteAccess(this)) {
|
SettingsActivity.launch(this, menuTag, "");
|
||||||
SettingsActivity.launch(this, menuTag, "");
|
|
||||||
} else {
|
|
||||||
PermissionsHandler.checkWritePermission(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void launchFileListActivity(int request) {
|
public void launchFileListActivity(int request) {
|
||||||
if (PermissionsHandler.hasWriteAccess(this)) {
|
switch (request) {
|
||||||
switch (request) {
|
case MainPresenter.REQUEST_ADD_DIRECTORY:
|
||||||
case MainPresenter.REQUEST_ADD_DIRECTORY:
|
FileBrowserHelper.openDirectoryPicker(this,
|
||||||
FileBrowserHelper.openDirectoryPicker(this,
|
MainPresenter.REQUEST_ADD_DIRECTORY,
|
||||||
MainPresenter.REQUEST_ADD_DIRECTORY,
|
R.string.select_game_folder);
|
||||||
R.string.select_game_folder,
|
break;
|
||||||
Arrays.asList("nso", "nro", "nca", "xci",
|
|
||||||
"nsp", "kip"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
PermissionsHandler.checkWritePermission(this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,6 +134,8 @@ public final class MainActivity extends AppCompatActivity implements MainView {
|
||||||
case MainPresenter.REQUEST_ADD_DIRECTORY:
|
case MainPresenter.REQUEST_ADD_DIRECTORY:
|
||||||
// If the user picked a file, as opposed to just backing out.
|
// If the user picked a file, as opposed to just backing out.
|
||||||
if (resultCode == MainActivity.RESULT_OK) {
|
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
|
// When a new directory is picked, we currently will reset the existing games
|
||||||
// database. This effectively means that only one game directory is supported.
|
// database. This effectively means that only one game directory is supported.
|
||||||
// TODO(bunnei): Consider fixing this in the future, or removing code for this.
|
// 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.
|
* Called by the framework whenever any actionbar/toolbar icon is clicked.
|
||||||
*
|
*
|
||||||
|
|
|
@ -22,7 +22,7 @@ public final class MainPresenter {
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
String versionName = BuildConfig.VERSION_NAME;
|
String versionName = BuildConfig.VERSION_NAME;
|
||||||
mView.setVersionString(versionName);
|
mView.setVersionString(versionName);
|
||||||
refeshGameList();
|
refreshGameList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void launchFileListActivity(int request) {
|
public void launchFileListActivity(int request) {
|
||||||
|
@ -63,7 +63,7 @@ public final class MainPresenter {
|
||||||
mDirToAdd = dir;
|
mDirToAdd = dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refeshGameList() {
|
public void refreshGameList() {
|
||||||
GameDatabase databaseHelper = YuzuApplication.databaseHelper;
|
GameDatabase databaseHelper = YuzuApplication.databaseHelper;
|
||||||
databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
|
databaseHelper.scanLibrary(databaseHelper.getWritableDatabase());
|
||||||
mView.refresh();
|
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;
|
package org.yuzu.yuzu_emu.utils;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
|
|
||||||
import org.yuzu.yuzu_emu.NativeLibrary;
|
import org.yuzu.yuzu_emu.NativeLibrary;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
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 final class DirectoryInitialization {
|
||||||
public static final String BROADCAST_ACTION = "org.yuzu.yuzu_emu.BROADCAST";
|
public static final String BROADCAST_ACTION = "org.yuzu.yuzu_emu.BROADCAST";
|
||||||
|
|
||||||
public static final String EXTRA_STATE = "directoryState";
|
public static final String EXTRA_STATE = "directoryState";
|
||||||
private static volatile DirectoryInitializationState directoryState = null;
|
private static volatile DirectoryInitializationState directoryState = null;
|
||||||
private static String userPath;
|
private static String userPath;
|
||||||
|
@ -37,7 +18,6 @@ public final class DirectoryInitialization {
|
||||||
|
|
||||||
public static void start(Context context) {
|
public static void start(Context context) {
|
||||||
// Can take a few seconds to run, so don't block UI thread.
|
// Can take a few seconds to run, so don't block UI thread.
|
||||||
//noinspection TrivialFunctionalExpressionUsage
|
|
||||||
((Runnable) () -> init(context)).run();
|
((Runnable) () -> init(context)).run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,31 +26,15 @@ public final class DirectoryInitialization {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (directoryState != DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) {
|
if (directoryState != DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED) {
|
||||||
if (PermissionsHandler.hasWriteAccess(context)) {
|
initializeInternalStorage(context);
|
||||||
if (setUserDirectory()) {
|
NativeLibrary.CreateConfigFile();
|
||||||
initializeInternalStorage(context);
|
directoryState = DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED;
|
||||||
NativeLibrary.CreateConfigFile();
|
|
||||||
directoryState = DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED;
|
|
||||||
} else {
|
|
||||||
directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isDirectoryInitializationRunning.set(false);
|
isDirectoryInitializationRunning.set(false);
|
||||||
sendBroadcastState(directoryState, context);
|
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() {
|
public static boolean areDirectoriesReady() {
|
||||||
return directoryState == DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED;
|
return directoryState == DirectoryInitializationState.YUZU_DIRECTORIES_INITIALIZED;
|
||||||
}
|
}
|
||||||
|
@ -85,41 +49,13 @@ public final class DirectoryInitialization {
|
||||||
return userPath;
|
return userPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static native void SetSysDirectory(String path);
|
public static void initializeInternalStorage(Context context) {
|
||||||
|
try {
|
||||||
private static boolean setUserDirectory() {
|
userPath = context.getExternalFilesDir(null).getCanonicalPath();
|
||||||
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
|
NativeLibrary.SetAppDirectory(userPath);
|
||||||
File externalPath = Environment.getExternalStorageDirectory();
|
} catch(IOException e) {
|
||||||
if (externalPath != null) {
|
e.printStackTrace();
|
||||||
userPath = externalPath.getAbsolutePath() + "/yuzu-emu";
|
|
||||||
Log.debug("[DirectoryInitialization] User Dir: " + userPath);
|
|
||||||
// NativeLibrary.SetUserDirectory(userPath);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
private static void sendBroadcastState(DirectoryInitializationState state, Context context) {
|
||||||
|
@ -129,58 +65,8 @@ public final class DirectoryInitialization {
|
||||||
LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent);
|
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 {
|
public enum DirectoryInitializationState {
|
||||||
YUZU_DIRECTORIES_INITIALIZED,
|
YUZU_DIRECTORIES_INITIALIZED,
|
||||||
EXTERNAL_STORAGE_PERMISSION_NEEDED,
|
|
||||||
CANT_FIND_EXTERNAL_STORAGE
|
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;
|
package org.yuzu.yuzu_emu.utils;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Environment;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.FragmentActivity;
|
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 final class FileBrowserHelper {
|
||||||
public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title, List<String> extensions) {
|
public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title) {
|
||||||
Intent i = new Intent(activity, CustomFilePickerActivity.class);
|
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
|
i.putExtra(Intent.EXTRA_TITLE, title);
|
||||||
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));
|
|
||||||
|
|
||||||
activity.startActivityForResult(i, requestCode);
|
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) {
|
public static String getSelectedDirectory(Intent result) {
|
||||||
// Use the provided utility method to parse the result
|
return result.getDataString();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,261 @@
|
||||||
package org.yuzu.yuzu_emu.utils;
|
package org.yuzu.yuzu_emu.utils;
|
||||||
|
|
||||||
import java.io.File;
|
import android.content.ContentResolver;
|
||||||
import java.io.FileInputStream;
|
import android.content.Context;
|
||||||
import java.io.IOException;
|
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.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class FileUtil {
|
public class FileUtil {
|
||||||
public static byte[] getBytesFromFile(File file) throws IOException {
|
static final String PATH_TREE = "tree";
|
||||||
final long length = file.length();
|
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) {
|
* Create a file from directory with filename.
|
||||||
// File is too large
|
* @param context Application context
|
||||||
throw new IOException("File is too large!");
|
* @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
|
* Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow
|
||||||
&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
|
* This function will be faster than DoucmentFile.listFiles
|
||||||
offset += numRead;
|
* @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;
|
package org.yuzu.yuzu_emu.utils;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.preference.PreferenceManager;
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
|
|
||||||
import org.yuzu.yuzu_emu.R;
|
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 {
|
public final class StartupHandler {
|
||||||
private static void handlePermissionsCheck(FragmentActivity parent) {
|
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext());
|
||||||
// Ask the user to grant write permission if it's not already granted
|
|
||||||
PermissionsHandler.checkWritePermission(parent);
|
|
||||||
|
|
||||||
String start_file = "";
|
private static void handleStartupPromptDismiss(MainActivity parent) {
|
||||||
Bundle extras = parent.getIntent().getExtras();
|
parent.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void HandleInit(FragmentActivity parent) {
|
private static void markFirstBoot() {
|
||||||
if (PermissionsHandler.isFirstBoot(parent)) {
|
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
|
// Prompt user with standard first boot disclaimer
|
||||||
new AlertDialog.Builder(parent)
|
new AlertDialog.Builder(parent)
|
||||||
.setTitle(R.string.app_name)
|
.setTitle(R.string.app_name)
|
||||||
.setIcon(R.mipmap.ic_launcher)
|
.setIcon(R.mipmap.ic_launcher)
|
||||||
.setMessage(parent.getResources().getString(R.string.app_disclaimer))
|
.setMessage(parent.getResources().getString(R.string.app_disclaimer))
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
.setOnDismissListener(dialogInterface -> handlePermissionsCheck(parent))
|
.setOnDismissListener(dialogInterface -> handleStartupPromptDismiss(parent))
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,8 @@
|
||||||
|
|
||||||
namespace FS = Common::FS;
|
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::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))} {
|
config{std::make_unique<INIReader>(FS::PathToUTF8String(config_loc))} {
|
||||||
Reload();
|
Reload();
|
||||||
}
|
}
|
||||||
|
@ -66,8 +63,8 @@ void Config::ReadSetting(const std::string& group, Settings::Setting<bool>& sett
|
||||||
|
|
||||||
template <typename Type, bool ranged>
|
template <typename Type, bool ranged>
|
||||||
void Config::ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting) {
|
void Config::ReadSetting(const std::string& group, Settings::Setting<Type, ranged>& setting) {
|
||||||
setting = static_cast<Type>(config->GetInteger(group, setting.GetLabel(),
|
setting = static_cast<Type>(
|
||||||
static_cast<long>(setting.GetDefault())));
|
config->GetInteger(group, setting.GetLabel(), static_cast<long>(setting.GetDefault())));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Config::ReadValues() {
|
void Config::ReadValues() {
|
||||||
|
@ -93,9 +90,9 @@ void Config::ReadValues() {
|
||||||
for (int i = 0; i < num_touch_from_button_maps; ++i) {
|
for (int i = 0; i < num_touch_from_button_maps; ++i) {
|
||||||
Settings::TouchFromButtonMap map;
|
Settings::TouchFromButtonMap map;
|
||||||
map.name = config->Get("ControlsGeneral",
|
map.name = config->Get("ControlsGeneral",
|
||||||
std::string("touch_from_button_maps_") + std::to_string(i) +
|
std::string("touch_from_button_maps_") + std::to_string(i) +
|
||||||
std::string("_name"),
|
std::string("_name"),
|
||||||
"default");
|
"default");
|
||||||
const int num_touch_maps = config->GetInteger(
|
const int num_touch_maps = config->GetInteger(
|
||||||
"ControlsGeneral",
|
"ControlsGeneral",
|
||||||
std::string("touch_from_button_maps_") + std::to_string(i) + std::string("_count"),
|
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) {
|
for (int j = 0; j < num_touch_maps; ++j) {
|
||||||
std::string touch_mapping =
|
std::string touch_mapping =
|
||||||
config->Get("ControlsGeneral",
|
config->Get("ControlsGeneral",
|
||||||
std::string("touch_from_button_maps_") + std::to_string(i) +
|
std::string("touch_from_button_maps_") + std::to_string(i) +
|
||||||
std::string("_bind_") + std::to_string(j),
|
std::string("_bind_") + std::to_string(j),
|
||||||
"");
|
"");
|
||||||
map.buttons.emplace_back(std::move(touch_mapping));
|
map.buttons.emplace_back(std::move(touch_mapping));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,16 +124,16 @@ void Config::ReadValues() {
|
||||||
ReadSetting("Data Storage", Settings::values.use_virtual_sd);
|
ReadSetting("Data Storage", Settings::values.use_virtual_sd);
|
||||||
FS::SetYuzuPath(FS::YuzuPath::NANDDir,
|
FS::SetYuzuPath(FS::YuzuPath::NANDDir,
|
||||||
config->Get("Data Storage", "nand_directory",
|
config->Get("Data Storage", "nand_directory",
|
||||||
FS::GetYuzuPathString(FS::YuzuPath::NANDDir)));
|
FS::GetYuzuPathString(FS::YuzuPath::NANDDir)));
|
||||||
FS::SetYuzuPath(FS::YuzuPath::SDMCDir,
|
FS::SetYuzuPath(FS::YuzuPath::SDMCDir,
|
||||||
config->Get("Data Storage", "sdmc_directory",
|
config->Get("Data Storage", "sdmc_directory",
|
||||||
FS::GetYuzuPathString(FS::YuzuPath::SDMCDir)));
|
FS::GetYuzuPathString(FS::YuzuPath::SDMCDir)));
|
||||||
FS::SetYuzuPath(FS::YuzuPath::LoadDir,
|
FS::SetYuzuPath(FS::YuzuPath::LoadDir,
|
||||||
config->Get("Data Storage", "load_directory",
|
config->Get("Data Storage", "load_directory",
|
||||||
FS::GetYuzuPathString(FS::YuzuPath::LoadDir)));
|
FS::GetYuzuPathString(FS::YuzuPath::LoadDir)));
|
||||||
FS::SetYuzuPath(FS::YuzuPath::DumpDir,
|
FS::SetYuzuPath(FS::YuzuPath::DumpDir,
|
||||||
config->Get("Data Storage", "dump_directory",
|
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_inserted);
|
||||||
ReadSetting("Data Storage", Settings::values.gamecard_current_game);
|
ReadSetting("Data Storage", Settings::values.gamecard_current_game);
|
||||||
ReadSetting("Data Storage", Settings::values.gamecard_path);
|
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"
|
#include "jni/id_cache.h"
|
||||||
|
|
||||||
static JavaVM* s_java_vm;
|
static JavaVM* s_java_vm;
|
||||||
static jclass s_native_library_class;
|
static jclass s_native_library_class;
|
||||||
static jmethodID s_exit_emulation_activity;
|
static jmethodID s_exit_emulation_activity;
|
||||||
|
|
||||||
|
static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
|
||||||
|
|
||||||
namespace IDCache {
|
namespace IDCache {
|
||||||
|
|
||||||
JNIEnv* GetEnvForThread() {
|
JNIEnv* GetEnvForThread() {
|
||||||
|
@ -34,3 +42,41 @@ jmethodID GetExitEmulationActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace IDCache
|
} // 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 <codecvt>
|
||||||
#include <locale>
|
#include <locale>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
@ -7,6 +10,7 @@
|
||||||
#include <android/native_window_jni.h>
|
#include <android/native_window_jni.h>
|
||||||
|
|
||||||
#include "common/detached_tasks.h"
|
#include "common/detached_tasks.h"
|
||||||
|
#include "common/fs/path_util.h"
|
||||||
#include "common/logging/backend.h"
|
#include "common/logging/backend.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "common/microprofile.h"
|
#include "common/microprofile.h"
|
||||||
|
@ -257,9 +261,11 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_NotifyOrientationChange(JNIEnv* env,
|
||||||
jint layout_option,
|
jint layout_option,
|
||||||
jint rotation) {}
|
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]] 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,
|
void Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation([[maybe_unused]] JNIEnv* env,
|
||||||
[[maybe_unused]] jclass clazz) {}
|
[[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
|
#pragma once
|
||||||
|
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
|
@ -8,16 +11,16 @@ extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_UnPauseEmulation(JNIEnv* env,
|
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);
|
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,
|
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,
|
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(
|
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onGamePadEvent(
|
||||||
JNIEnv* env, jclass clazz, jstring j_device, jint j_button, jint action);
|
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);
|
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,
|
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchEvent(JNIEnv* env,
|
||||||
jclass clazz,
|
jclass clazz,
|
||||||
jfloat x, jfloat y,
|
jfloat x, jfloat y,
|
||||||
jboolean pressed);
|
jboolean pressed);
|
||||||
|
|
||||||
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env,
|
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_onTouchMoved(JNIEnv* env, jclass clazz,
|
||||||
jclass clazz, jfloat x,
|
jfloat x, jfloat y);
|
||||||
jfloat y);
|
|
||||||
|
|
||||||
JNIEXPORT jintArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon(JNIEnv* env,
|
JNIEXPORT jintArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetIcon(JNIEnv* env, jclass clazz,
|
||||||
jclass clazz,
|
jstring j_file);
|
||||||
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,
|
jclass clazz,
|
||||||
jstring j_filename);
|
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,
|
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetCompany(JNIEnv* env,
|
||||||
jclass clazz,
|
jclass clazz,
|
||||||
jstring j_filename);
|
jstring j_filename);
|
||||||
|
|
||||||
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetGitRevision(JNIEnv* env,
|
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(
|
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetAppDirectory(JNIEnv* env,
|
||||||
JNIEnv* env, jclass clazz, jstring j_directory);
|
jclass clazz,
|
||||||
|
jstring j_directory);
|
||||||
|
|
||||||
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_SetSysDirectory(
|
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_SetSysDirectory(
|
||||||
JNIEnv* env, jclass clazz, jstring path_);
|
JNIEnv* env, jclass clazz, jstring path_);
|
||||||
|
|
||||||
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetSysDirectory(JNIEnv* env,
|
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetSysDirectory(JNIEnv* env,
|
||||||
jclass clazz,
|
jclass clazz,
|
||||||
jstring path);
|
jstring path);
|
||||||
|
|
||||||
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_CreateConfigFile(JNIEnv* env,
|
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,
|
JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_DefaultCPUCore(JNIEnv* env,
|
||||||
jclass clazz);
|
jclass clazz);
|
||||||
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetProfiling(JNIEnv* env,
|
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetProfiling(JNIEnv* env, jclass clazz,
|
||||||
jclass clazz,
|
jboolean enable);
|
||||||
jboolean enable);
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_WriteProfileResults(JNIEnv* env,
|
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(
|
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_NotifyOrientationChange(
|
||||||
JNIEnv* env, jclass clazz, jint layout_option, jint rotation);
|
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);
|
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,
|
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceChanged(JNIEnv* env,
|
||||||
jclass clazz,
|
jclass clazz,
|
||||||
jobject surf);
|
jobject surf);
|
||||||
|
|
||||||
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SurfaceDestroyed(JNIEnv* env,
|
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,
|
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_InitGameIni(JNIEnv* env, jclass clazz,
|
||||||
jclass clazz,
|
jstring j_game_id);
|
||||||
jstring j_game_id);
|
|
||||||
|
|
||||||
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_ReloadSettings(JNIEnv* env,
|
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(
|
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_SetUserSetting(
|
||||||
JNIEnv* env, jclass clazz, jstring j_game_id, jstring j_section, jstring j_key,
|
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);
|
JNIEnv* env, jclass clazz, jstring game_id, jstring section, jstring key);
|
||||||
|
|
||||||
JNIEXPORT jdoubleArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_GetPerfStats(JNIEnv* env,
|
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,
|
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_LogDeviceInfo(JNIEnv* env,
|
||||||
jclass clazz);
|
jclass clazz);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#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>
|
<resources>
|
||||||
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
|
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
|
||||||
(such as screen margins) for screens with more than 1024dp of available width. -->
|
(such as screen margins) for screens with more than 1024dp of available width. -->
|
||||||
<dimen name="activity_horizontal_margin">96dp</dimen>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
|
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
|
||||||
(such as screen margins) for screens with more than 820dp of available width. -->
|
(such as screen margins) for screens with more than 820dp of available width. -->
|
||||||
<dimen name="activity_horizontal_margin">64dp</dimen>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
<string name="grid_menu_core_settings">Settings</string>
|
<string name="grid_menu_core_settings">Settings</string>
|
||||||
|
|
||||||
<!-- Add Directory Screen-->
|
<!-- 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>
|
<string name="install_cia_title">Install CIA</string>
|
||||||
|
|
||||||
<!-- Preferences Screen -->
|
<!-- Preferences Screen -->
|
||||||
|
@ -71,7 +71,6 @@
|
||||||
<string name="emulation_touch_overlay_reset">Reset Overlay</string>
|
<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="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="load_settings">Loading Settings...</string>
|
||||||
|
|
||||||
<string name="external_storage_not_mounted">The external storage needs to be available in order to use yuzu</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>
|
<item name="android:windowAllowReturnTransitionOverlap">true</item>
|
||||||
</style>
|
</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">
|
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.DayNight.Dialog.Alert">
|
||||||
<item name="colorSurface">@color/view_background</item>
|
<item name="colorSurface">@color/view_background</item>
|
||||||
<item name="colorOnSurface">@color/view_text</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)
|
target_link_libraries(common PRIVATE ntdll)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(ANDROID)
|
||||||
|
target_sources(common
|
||||||
|
PRIVATE
|
||||||
|
fs/fs_android.cpp
|
||||||
|
fs/fs_android.h
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(ARCHITECTURE_x86_64)
|
if(ARCHITECTURE_x86_64)
|
||||||
target_sources(common
|
target_sources(common
|
||||||
PRIVATE
|
PRIVATE
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
|
|
||||||
#include "common/fs/file.h"
|
#include "common/fs/file.h"
|
||||||
#include "common/fs/fs.h"
|
#include "common/fs/fs.h"
|
||||||
|
#ifdef ANDROID
|
||||||
|
#include "common/fs/fs_android.h"
|
||||||
|
#endif
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
@ -252,6 +255,23 @@ void IOFile::Open(const fs::path& path, FileAccessMode mode, FileType type, File
|
||||||
} else {
|
} else {
|
||||||
_wfopen_s(&file, path.c_str(), AccessModeToWStr(mode, type));
|
_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
|
#else
|
||||||
file = std::fopen(path.c_str(), AccessModeToStr(mode, type));
|
file = std::fopen(path.c_str(), AccessModeToStr(mode, type));
|
||||||
#endif
|
#endif
|
||||||
|
@ -372,6 +392,23 @@ u64 IOFile::GetSize() const {
|
||||||
// Flush any unwritten buffered data into the file prior to retrieving the file size.
|
// Flush any unwritten buffered data into the file prior to retrieving the file size.
|
||||||
std::fflush(file);
|
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;
|
std::error_code ec;
|
||||||
|
|
||||||
const auto file_size = fs::file_size(file_path, ec);
|
const auto file_size = fs::file_size(file_path, ec);
|
||||||
|
@ -381,6 +418,7 @@ u64 IOFile::GetSize() const {
|
||||||
PathToUTF8String(file_path), ec.message());
|
PathToUTF8String(file_path), ec.message());
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
return file_size;
|
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 <unordered_map>
|
||||||
|
|
||||||
#include "common/fs/fs.h"
|
#include "common/fs/fs.h"
|
||||||
|
#ifdef ANDROID
|
||||||
|
#include "common/fs/fs_android.h"
|
||||||
|
#endif
|
||||||
#include "common/fs/fs_paths.h"
|
#include "common/fs/fs_paths.h"
|
||||||
#include "common/fs/path_util.h"
|
#include "common/fs/path_util.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
@ -80,9 +83,7 @@ public:
|
||||||
yuzu_paths.insert_or_assign(yuzu_path, new_path);
|
yuzu_paths.insert_or_assign(yuzu_path, new_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
void Reinitialize(fs::path yuzu_path = {}) {
|
||||||
PathManagerImpl() {
|
|
||||||
fs::path yuzu_path;
|
|
||||||
fs::path yuzu_path_cache;
|
fs::path yuzu_path_cache;
|
||||||
fs::path yuzu_path_config;
|
fs::path yuzu_path_config;
|
||||||
|
|
||||||
|
@ -96,12 +97,9 @@ private:
|
||||||
yuzu_path_cache = yuzu_path / CACHE_DIR;
|
yuzu_path_cache = yuzu_path / CACHE_DIR;
|
||||||
yuzu_path_config = yuzu_path / CONFIG_DIR;
|
yuzu_path_config = yuzu_path / CONFIG_DIR;
|
||||||
#elif ANDROID
|
#elif ANDROID
|
||||||
// On Android internal storage is mounted as "/sdcard"
|
ASSERT(!yuzu_path.empty());
|
||||||
if (Exists("/sdcard")) {
|
yuzu_path_cache = yuzu_path / CACHE_DIR;
|
||||||
yuzu_path = "/sdcard/yuzu-emu";
|
yuzu_path_config = yuzu_path / CONFIG_DIR;
|
||||||
yuzu_path_cache = yuzu_path / CACHE_DIR;
|
|
||||||
yuzu_path_config = yuzu_path / CONFIG_DIR;
|
|
||||||
}
|
|
||||||
#else
|
#else
|
||||||
yuzu_path = GetCurrentDir() / PORTABLE_DIR;
|
yuzu_path = GetCurrentDir() / PORTABLE_DIR;
|
||||||
|
|
||||||
|
@ -129,6 +127,11 @@ private:
|
||||||
GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR);
|
GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
PathManagerImpl() {
|
||||||
|
Reinitialize();
|
||||||
|
}
|
||||||
|
|
||||||
~PathManagerImpl() = default;
|
~PathManagerImpl() = default;
|
||||||
|
|
||||||
void GenerateYuzuPath(YuzuPath yuzu_path, const fs::path& new_path) {
|
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};
|
return fs::path{string_path};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SetAppDirectory(const std::string& app_directory) {
|
||||||
|
PathManagerImpl::GetInstance().Reinitialize(app_directory);
|
||||||
|
}
|
||||||
|
|
||||||
const fs::path& GetYuzuPath(YuzuPath yuzu_path) {
|
const fs::path& GetYuzuPath(YuzuPath yuzu_path) {
|
||||||
return PathManagerImpl::GetInstance().GetYuzuPathImpl(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 SanitizePath(std::string_view path_, DirectorySeparator directory_separator) {
|
||||||
std::string path(path_);
|
std::string path(path_);
|
||||||
|
#ifdef ANDROID
|
||||||
|
if (Android::IsContentUri(path)) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
#endif // ANDROID
|
||||||
|
|
||||||
char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\';
|
char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\';
|
||||||
char type2 = directory_separator == DirectorySeparator::BackwardSlash ? '\\' : '/';
|
char type2 = directory_separator == DirectorySeparator::BackwardSlash ? '\\' : '/';
|
||||||
|
|
||||||
|
|
|
@ -180,6 +180,14 @@ template <typename Path>
|
||||||
}
|
}
|
||||||
#endif
|
#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.
|
* Gets the filesystem path associated with the YuzuPath enum.
|
||||||
*
|
*
|
||||||
|
|
Reference in New Issue