38 changed files with 851 additions and 697 deletions
-
6src/android/app/build.gradle
-
13src/android/app/src/main/AndroidManifest.xml
-
22src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.java
-
8src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.java
-
38src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/CustomFilePickerActivity.java
-
9src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java
-
6src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.java
-
3src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityPresenter.java
-
5src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivityView.java
-
120src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/CustomFilePickerFragment.java
-
4src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.java
-
52src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDatabase.java
-
28src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MinimalDocumentFile.java
-
83src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.java
-
4src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainPresenter.java
-
132src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.java
-
125src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.java
-
65src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileBrowserHelper.java
-
264src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.java
-
35src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/PermissionsHandler.java
-
42src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/StartupHandler.java
-
29src/android/app/src/main/jni/config.cpp
-
46src/android/app/src/main/jni/id_cache.cpp
-
12src/android/app/src/main/jni/native.cpp
-
89src/android/app/src/main/jni/native.h
-
32src/android/app/src/main/res/layout/filepicker_toolbar.xml
-
5src/android/app/src/main/res/values-night/styles_filepicker.xml
-
1src/android/app/src/main/res/values-w1050dp/dimens.xml
-
1src/android/app/src/main/res/values-w820dp/dimens.xml
-
3src/android/app/src/main/res/values/strings.xml
-
16src/android/app/src/main/res/values/styles.xml
-
5src/android/app/src/main/res/values/styles_filepicker.xml
-
8src/common/CMakeLists.txt
-
38src/common/fs/file.cpp
-
98src/common/fs/fs_android.cpp
-
62src/common/fs/fs_android.h
-
31src/common/fs/path_util.cpp
-
8src/common/fs/path_util.h
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -0,0 +1,125 @@ |
|||
package org.yuzu.yuzu_emu.utils; |
|||
|
|||
import android.content.Context; |
|||
import android.net.Uri; |
|||
|
|||
import androidx.annotation.Nullable; |
|||
import androidx.documentfile.provider.DocumentFile; |
|||
|
|||
import org.yuzu.yuzu_emu.YuzuApplication; |
|||
import org.yuzu.yuzu_emu.model.MinimalDocumentFile; |
|||
|
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
import java.util.StringTokenizer; |
|||
|
|||
public class DocumentsTree { |
|||
private DocumentsNode root; |
|||
private final Context context; |
|||
public static final String DELIMITER = "/"; |
|||
|
|||
public DocumentsTree() { |
|||
context = YuzuApplication.getAppContext(); |
|||
} |
|||
|
|||
public void setRoot(Uri rootUri) { |
|||
root = null; |
|||
root = new DocumentsNode(); |
|||
root.uri = rootUri; |
|||
root.isDirectory = true; |
|||
} |
|||
|
|||
public int openContentUri(String filepath, String openmode) { |
|||
DocumentsNode node = resolvePath(filepath); |
|||
if (node == null) { |
|||
return -1; |
|||
} |
|||
return FileUtil.openContentUri(context, node.uri.toString(), openmode); |
|||
} |
|||
|
|||
public long getFileSize(String filepath) { |
|||
DocumentsNode node = resolvePath(filepath); |
|||
if (node == null || node.isDirectory) { |
|||
return 0; |
|||
} |
|||
return FileUtil.getFileSize(context, node.uri.toString()); |
|||
} |
|||
|
|||
public boolean Exists(String filepath) { |
|||
return resolvePath(filepath) != null; |
|||
} |
|||
|
|||
@Nullable |
|||
private DocumentsNode resolvePath(String filepath) { |
|||
StringTokenizer tokens = new StringTokenizer(filepath, DELIMITER, false); |
|||
DocumentsNode iterator = root; |
|||
while (tokens.hasMoreTokens()) { |
|||
String token = tokens.nextToken(); |
|||
if (token.isEmpty()) continue; |
|||
iterator = find(iterator, token); |
|||
if (iterator == null) return null; |
|||
} |
|||
return iterator; |
|||
} |
|||
|
|||
@Nullable |
|||
private DocumentsNode find(DocumentsNode parent, String filename) { |
|||
if (parent.isDirectory && !parent.loaded) { |
|||
structTree(parent); |
|||
} |
|||
return parent.children.get(filename); |
|||
} |
|||
|
|||
/** |
|||
* Construct current level directory tree |
|||
* @param parent parent node of this level |
|||
*/ |
|||
private void structTree(DocumentsNode parent) { |
|||
MinimalDocumentFile[] documents = FileUtil.listFiles(context, parent.uri); |
|||
for (MinimalDocumentFile document: documents) { |
|||
DocumentsNode node = new DocumentsNode(document); |
|||
node.parent = parent; |
|||
parent.children.put(node.name, node); |
|||
} |
|||
parent.loaded = true; |
|||
} |
|||
|
|||
public static boolean isNativePath(String path) { |
|||
if (path.length() > 0) { |
|||
return path.charAt(0) == '/'; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
private static class DocumentsNode { |
|||
private DocumentsNode parent; |
|||
private final Map<String, DocumentsNode> children = new HashMap<>(); |
|||
private String name; |
|||
private Uri uri; |
|||
private boolean loaded = false; |
|||
private boolean isDirectory = false; |
|||
|
|||
private DocumentsNode() {} |
|||
private DocumentsNode(MinimalDocumentFile document) { |
|||
name = document.getFilename(); |
|||
uri = document.getUri(); |
|||
isDirectory = document.isDirectory(); |
|||
loaded = !isDirectory; |
|||
} |
|||
private DocumentsNode(DocumentFile document, boolean isCreateDir) { |
|||
name = document.getName(); |
|||
uri = document.getUri(); |
|||
isDirectory = isCreateDir; |
|||
loaded = true; |
|||
} |
|||
|
|||
private void rename(String name) { |
|||
if (parent == null) { |
|||
return; |
|||
} |
|||
parent.children.remove(this.name); |
|||
this.name = name; |
|||
parent.children.put(name, this); |
|||
} |
|||
} |
|||
} |
|||
@ -1,73 +1,16 @@ |
|||
package org.yuzu.yuzu_emu.utils; |
|||
|
|||
import android.content.Intent; |
|||
import android.net.Uri; |
|||
import android.os.Environment; |
|||
|
|||
import androidx.annotation.Nullable; |
|||
import androidx.fragment.app.FragmentActivity; |
|||
|
|||
import com.nononsenseapps.filepicker.FilePickerActivity; |
|||
import com.nononsenseapps.filepicker.Utils; |
|||
|
|||
import org.yuzu.yuzu_emu.activities.CustomFilePickerActivity; |
|||
|
|||
import java.io.File; |
|||
import java.util.List; |
|||
|
|||
public final class FileBrowserHelper { |
|||
public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title, List<String> extensions) { |
|||
Intent i = new Intent(activity, CustomFilePickerActivity.class); |
|||
|
|||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); |
|||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); |
|||
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR); |
|||
i.putExtra(FilePickerActivity.EXTRA_START_PATH, |
|||
Environment.getExternalStorageDirectory().getPath()); |
|||
i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title); |
|||
i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions)); |
|||
|
|||
public static void openDirectoryPicker(FragmentActivity activity, int requestCode, int title) { |
|||
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); |
|||
i.putExtra(Intent.EXTRA_TITLE, title); |
|||
activity.startActivityForResult(i, requestCode); |
|||
} |
|||
|
|||
public static void openFilePicker(FragmentActivity activity, int requestCode, int title, |
|||
List<String> extensions, boolean allowMultiple) { |
|||
Intent i = new Intent(activity, CustomFilePickerActivity.class); |
|||
|
|||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, allowMultiple); |
|||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); |
|||
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); |
|||
i.putExtra(FilePickerActivity.EXTRA_START_PATH, |
|||
Environment.getExternalStorageDirectory().getPath()); |
|||
i.putExtra(CustomFilePickerActivity.EXTRA_TITLE, title); |
|||
i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, String.join(",", extensions)); |
|||
|
|||
activity.startActivityForResult(i, requestCode); |
|||
} |
|||
|
|||
@Nullable |
|||
public static String getSelectedDirectory(Intent result) { |
|||
// Use the provided utility method to parse the result |
|||
List<Uri> files = Utils.getSelectedFilesFromResult(result); |
|||
if (!files.isEmpty()) { |
|||
File file = Utils.getFileForUri(files.get(0)); |
|||
return file.getAbsolutePath(); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
@Nullable |
|||
public static String[] getSelectedFiles(Intent result) { |
|||
// Use the provided utility method to parse the result |
|||
List<Uri> files = Utils.getSelectedFilesFromResult(result); |
|||
if (!files.isEmpty()) { |
|||
String[] paths = new String[files.size()]; |
|||
for (int i = 0; i < files.size(); i++) |
|||
paths[i] = Utils.getFileForUri(files.get(i)).getAbsolutePath(); |
|||
return paths; |
|||
} |
|||
|
|||
return null; |
|||
return result.getDataString(); |
|||
} |
|||
} |
|||
@ -1,37 +1,261 @@ |
|||
package org.yuzu.yuzu_emu.utils; |
|||
|
|||
import java.io.File; |
|||
import java.io.FileInputStream; |
|||
import java.io.IOException; |
|||
import android.content.ContentResolver; |
|||
import android.content.Context; |
|||
import android.database.Cursor; |
|||
import android.net.Uri; |
|||
import android.os.ParcelFileDescriptor; |
|||
import android.provider.DocumentsContract; |
|||
|
|||
import androidx.annotation.Nullable; |
|||
import androidx.documentfile.provider.DocumentFile; |
|||
|
|||
import org.yuzu.yuzu_emu.model.MinimalDocumentFile; |
|||
|
|||
import java.io.InputStream; |
|||
import java.io.OutputStream; |
|||
import java.net.URLDecoder; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
public class FileUtil { |
|||
public static byte[] getBytesFromFile(File file) throws IOException { |
|||
final long length = file.length(); |
|||
static final String PATH_TREE = "tree"; |
|||
static final String DECODE_METHOD = "UTF-8"; |
|||
static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; |
|||
static final String TEXT_PLAIN = "text/plain"; |
|||
|
|||
// You cannot create an array using a long type. |
|||
if (length > Integer.MAX_VALUE) { |
|||
// File is too large |
|||
throw new IOException("File is too large!"); |
|||
/** |
|||
* Create a file from directory with filename. |
|||
* @param context Application context |
|||
* @param directory parent path for file. |
|||
* @param filename file display name. |
|||
* @return boolean |
|||
*/ |
|||
@Nullable |
|||
public static DocumentFile createFile(Context context, String directory, String filename) { |
|||
try { |
|||
Uri directoryUri = Uri.parse(directory); |
|||
DocumentFile parent = DocumentFile.fromTreeUri(context, directoryUri); |
|||
if (parent == null) return null; |
|||
filename = URLDecoder.decode(filename, DECODE_METHOD); |
|||
String mimeType = APPLICATION_OCTET_STREAM; |
|||
if (filename.endsWith(".txt")) { |
|||
mimeType = TEXT_PLAIN; |
|||
} |
|||
DocumentFile exists = parent.findFile(filename); |
|||
if (exists != null) return exists; |
|||
return parent.createFile(mimeType, filename); |
|||
} catch (Exception e) { |
|||
Log.error("[FileUtil]: Cannot create file, error: " + e.getMessage()); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
byte[] bytes = new byte[(int) length]; |
|||
/** |
|||
* Create a directory from directory with filename. |
|||
* @param context Application context |
|||
* @param directory parent path for directory. |
|||
* @param directoryName directory display name. |
|||
* @return boolean |
|||
*/ |
|||
@Nullable |
|||
public static DocumentFile createDir(Context context, String directory, String directoryName) { |
|||
try { |
|||
Uri directoryUri = Uri.parse(directory); |
|||
DocumentFile parent = DocumentFile.fromTreeUri(context, directoryUri); |
|||
if (parent == null) return null; |
|||
directoryName = URLDecoder.decode(directoryName, DECODE_METHOD); |
|||
DocumentFile isExist = parent.findFile(directoryName); |
|||
if (isExist != null) return isExist; |
|||
return parent.createDirectory(directoryName); |
|||
} catch (Exception e) { |
|||
Log.error("[FileUtil]: Cannot create file, error: " + e.getMessage()); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
int offset = 0; |
|||
int numRead; |
|||
/** |
|||
* Open content uri and return file descriptor to JNI. |
|||
* @param context Application context |
|||
* @param path Native content uri path |
|||
* @param openmode will be one of "r", "r", "rw", "wa", "rwa" |
|||
* @return file descriptor |
|||
*/ |
|||
public static int openContentUri(Context context, String path, String openmode) { |
|||
try { |
|||
Uri uri = Uri.parse(path); |
|||
ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, openmode); |
|||
if (parcelFileDescriptor == null) { |
|||
Log.error("[FileUtil]: Cannot get the file descriptor from uri: " + path); |
|||
return -1; |
|||
} |
|||
return parcelFileDescriptor.detachFd(); |
|||
} |
|||
catch (Exception e) { |
|||
Log.error("[FileUtil]: Cannot open content uri, error: " + e.getMessage()); |
|||
} |
|||
return -1; |
|||
} |
|||
|
|||
try (InputStream is = new FileInputStream(file)) { |
|||
while (offset < bytes.length |
|||
&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { |
|||
offset += numRead; |
|||
/** |
|||
* Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow |
|||
* This function will be faster than DoucmentFile.listFiles |
|||
* @param context Application context |
|||
* @param uri Directory uri. |
|||
* @return CheapDocument lists. |
|||
*/ |
|||
public static MinimalDocumentFile[] listFiles(Context context, Uri uri) { |
|||
final ContentResolver resolver = context.getContentResolver(); |
|||
final String[] columns = new String[]{ |
|||
DocumentsContract.Document.COLUMN_DOCUMENT_ID, |
|||
DocumentsContract.Document.COLUMN_DISPLAY_NAME, |
|||
DocumentsContract.Document.COLUMN_MIME_TYPE, |
|||
}; |
|||
Cursor c = null; |
|||
final List<MinimalDocumentFile> results = new ArrayList<>(); |
|||
try { |
|||
String docId; |
|||
if (isRootTreeUri(uri)) { |
|||
docId = DocumentsContract.getTreeDocumentId(uri); |
|||
} else { |
|||
docId = DocumentsContract.getDocumentId(uri); |
|||
} |
|||
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId); |
|||
c = resolver.query(childrenUri, columns, null, null, null); |
|||
while(c.moveToNext()) { |
|||
final String documentId = c.getString(0); |
|||
final String documentName = c.getString(1); |
|||
final String documentMimeType = c.getString(2); |
|||
final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId); |
|||
MinimalDocumentFile document = new MinimalDocumentFile(documentName, documentMimeType, documentUri); |
|||
results.add(document); |
|||
} |
|||
} catch (Exception e) { |
|||
Log.error("[FileUtil]: Cannot list file error: " + e.getMessage()); |
|||
} finally { |
|||
closeQuietly(c); |
|||
} |
|||
return results.toArray(new MinimalDocumentFile[0]); |
|||
} |
|||
|
|||
/** |
|||
* Check whether given path exists. |
|||
* @param path Native content uri path |
|||
* @return bool |
|||
*/ |
|||
public static boolean Exists(Context context, String path) { |
|||
Cursor c = null; |
|||
try { |
|||
Uri mUri = Uri.parse(path); |
|||
final String[] columns = new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID }; |
|||
c = context.getContentResolver().query(mUri, columns, null, null, null); |
|||
return c.getCount() > 0; |
|||
} catch (Exception e) { |
|||
Log.info("[FileUtil] Cannot find file from given path, error: " + e.getMessage()); |
|||
} finally { |
|||
closeQuietly(c); |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
/** |
|||
* Check whether given path is a directory |
|||
* @param path content uri path |
|||
* @return bool |
|||
*/ |
|||
public static boolean isDirectory(Context context, String path) { |
|||
final ContentResolver resolver = context.getContentResolver(); |
|||
final String[] columns = new String[] { |
|||
DocumentsContract.Document.COLUMN_MIME_TYPE |
|||
}; |
|||
boolean isDirectory = false; |
|||
Cursor c = null; |
|||
try { |
|||
Uri mUri = Uri.parse(path); |
|||
c = resolver.query(mUri, columns, null, null, null); |
|||
c.moveToNext(); |
|||
final String mimeType = c.getString(0); |
|||
isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR); |
|||
} catch (Exception e) { |
|||
Log.error("[FileUtil]: Cannot list files, error: " + e.getMessage()); |
|||
} finally { |
|||
closeQuietly(c); |
|||
} |
|||
return isDirectory; |
|||
} |
|||
|
|||
// Ensure all the bytes have been read in |
|||
if (offset < bytes.length) { |
|||
throw new IOException("Could not completely read file " + file.getName()); |
|||
/** |
|||
* 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]); |
|||
} |
|||
|
|||
return bytes; |
|||
/** |
|||
* Get file size from given path. |
|||
* @param path content uri path |
|||
* @return long file size |
|||
*/ |
|||
public static long getFileSize(Context context, String path) { |
|||
final ContentResolver resolver = context.getContentResolver(); |
|||
final String[] columns = new String[] { |
|||
DocumentsContract.Document.COLUMN_SIZE |
|||
}; |
|||
long size = 0; |
|||
Cursor c =null; |
|||
try { |
|||
Uri mUri = Uri.parse(path); |
|||
c = resolver.query(mUri, columns, null, null, null); |
|||
c.moveToNext(); |
|||
size = c.getLong(0); |
|||
} catch (Exception e) { |
|||
Log.error("[FileUtil]: Cannot get file size, error: " + e.getMessage()); |
|||
} finally { |
|||
closeQuietly(c); |
|||
} |
|||
return size; |
|||
} |
|||
|
|||
public static boolean isRootTreeUri(Uri uri) { |
|||
final List<String> paths = uri.getPathSegments(); |
|||
return paths.size() == 2 && PATH_TREE.equals(paths.get(0)); |
|||
} |
|||
|
|||
public static void closeQuietly(AutoCloseable closeable) { |
|||
if (closeable != null) { |
|||
try { |
|||
closeable.close(); |
|||
} catch (RuntimeException rethrown) { |
|||
throw rethrown; |
|||
} catch (Exception ignored) { |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,35 +0,0 @@ |
|||
package org.yuzu.yuzu_emu.utils; |
|||
|
|||
import android.annotation.TargetApi; |
|||
import android.content.Context; |
|||
import android.content.pm.PackageManager; |
|||
import android.os.Build; |
|||
|
|||
import androidx.core.content.ContextCompat; |
|||
import androidx.fragment.app.FragmentActivity; |
|||
|
|||
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; |
|||
|
|||
public class PermissionsHandler { |
|||
public static final int REQUEST_CODE_WRITE_PERMISSION = 500; |
|||
|
|||
// We use permissions acceptance as an indicator if this is a first boot for the user. |
|||
public static boolean isFirstBoot(final FragmentActivity activity) { |
|||
return ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED; |
|||
} |
|||
|
|||
@TargetApi(Build.VERSION_CODES.M) |
|||
public static boolean checkWritePermission(final FragmentActivity activity) { |
|||
if (isFirstBoot(activity)) { |
|||
activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, |
|||
REQUEST_CODE_WRITE_PERMISSION); |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
public static boolean hasWriteAccess(Context context) { |
|||
return ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; |
|||
} |
|||
} |
|||
@ -1,44 +1,38 @@ |
|||
package org.yuzu.yuzu_emu.utils; |
|||
|
|||
import android.content.Intent; |
|||
import android.os.Bundle; |
|||
import android.text.TextUtils; |
|||
|
|||
import android.content.SharedPreferences; |
|||
import android.preference.PreferenceManager; |
|||
import androidx.appcompat.app.AlertDialog; |
|||
import androidx.fragment.app.FragmentActivity; |
|||
|
|||
import org.yuzu.yuzu_emu.R; |
|||
import org.yuzu.yuzu_emu.activities.EmulationActivity; |
|||
import org.yuzu.yuzu_emu.YuzuApplication; |
|||
import org.yuzu.yuzu_emu.ui.main.MainActivity; |
|||
import org.yuzu.yuzu_emu.ui.main.MainPresenter; |
|||
|
|||
public final class StartupHandler { |
|||
private static void handlePermissionsCheck(FragmentActivity parent) { |
|||
// Ask the user to grant write permission if it's not already granted |
|||
PermissionsHandler.checkWritePermission(parent); |
|||
private static SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.getAppContext()); |
|||
|
|||
String start_file = ""; |
|||
Bundle extras = parent.getIntent().getExtras(); |
|||
if (extras != null) { |
|||
start_file = extras.getString("AutoStartFile"); |
|||
} |
|||
private static void handleStartupPromptDismiss(MainActivity parent) { |
|||
parent.launchFileListActivity(MainPresenter.REQUEST_ADD_DIRECTORY); |
|||
} |
|||
|
|||
if (!TextUtils.isEmpty(start_file)) { |
|||
// Start the emulation activity, send the ISO passed in and finish the main activity |
|||
Intent emulation_intent = new Intent(parent, EmulationActivity.class); |
|||
emulation_intent.putExtra("SelectedGame", start_file); |
|||
parent.startActivity(emulation_intent); |
|||
parent.finish(); |
|||
} |
|||
private static void markFirstBoot() { |
|||
final SharedPreferences.Editor editor = mPreferences.edit(); |
|||
editor.putBoolean("FirstApplicationLaunch", false); |
|||
editor.apply(); |
|||
} |
|||
|
|||
public static void HandleInit(FragmentActivity parent) { |
|||
if (PermissionsHandler.isFirstBoot(parent)) { |
|||
public static void handleInit(MainActivity parent) { |
|||
if (mPreferences.getBoolean("FirstApplicationLaunch", true)) { |
|||
markFirstBoot(); |
|||
|
|||
// Prompt user with standard first boot disclaimer |
|||
new AlertDialog.Builder(parent) |
|||
.setTitle(R.string.app_name) |
|||
.setIcon(R.mipmap.ic_launcher) |
|||
.setMessage(parent.getResources().getString(R.string.app_disclaimer)) |
|||
.setPositiveButton(android.R.string.ok, null) |
|||
.setOnDismissListener(dialogInterface -> handlePermissionsCheck(parent)) |
|||
.setOnDismissListener(dialogInterface -> handleStartupPromptDismiss(parent)) |
|||
.show(); |
|||
} |
|||
} |
|||
|
|||
@ -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> |
|||
@ -1,5 +1,4 @@ |
|||
<resources> |
|||
<!-- Example customization of dimensions originally defined in res/values/dimens.xml |
|||
(such as screen margins) for screens with more than 820dp of available width. --> |
|||
<dimen name="activity_horizontal_margin">64dp</dimen> |
|||
</resources> |
|||
@ -1,5 +0,0 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<resources> |
|||
|
|||
<style name="FilePickerBaseTheme" parent="NNF_BaseTheme.Light" /> |
|||
</resources> |
|||
@ -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 |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue