mirror of https://git.suyu.dev/suyu/suyu
android: Implement SAF support & migrate to SDK 31. (#4)
parent
39ab81a098
commit
ef605f7d8f
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,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
|
Loading…
Reference in New Issue