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;
|
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.
|
||||||
int offset = 0;
|
* @param context Application context
|
||||||
int numRead;
|
* @param directory parent path for directory.
|
||||||
|
* @param directoryName directory display name.
|
||||||
try (InputStream is = new FileInputStream(file)) {
|
* @return boolean
|
||||||
while (offset < bytes.length
|
*/
|
||||||
&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
|
@Nullable
|
||||||
offset += numRead;
|
public static DocumentFile createDir(Context context, String directory, String directoryName) {
|
||||||
|
try {
|
||||||
|
Uri directoryUri = Uri.parse(directory);
|
||||||
|
DocumentFile parent = DocumentFile.fromTreeUri(context, directoryUri);
|
||||||
|
if (parent == null) return null;
|
||||||
|
directoryName = URLDecoder.decode(directoryName, DECODE_METHOD);
|
||||||
|
DocumentFile isExist = parent.findFile(directoryName);
|
||||||
|
if (isExist != null) return isExist;
|
||||||
|
return parent.createDirectory(directoryName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot create file, error: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure all the bytes have been read in
|
/**
|
||||||
if (offset < bytes.length) {
|
* Open content uri and return file descriptor to JNI.
|
||||||
throw new IOException("Could not completely read file " + file.getName());
|
* @param context Application context
|
||||||
|
* @param path Native content uri path
|
||||||
|
* @param openmode will be one of "r", "r", "rw", "wa", "rwa"
|
||||||
|
* @return file descriptor
|
||||||
|
*/
|
||||||
|
public static int openContentUri(Context context, String path, String openmode) {
|
||||||
|
try {
|
||||||
|
Uri uri = Uri.parse(path);
|
||||||
|
ParcelFileDescriptor parcelFileDescriptor = context.getContentResolver().openFileDescriptor(uri, openmode);
|
||||||
|
if (parcelFileDescriptor == null) {
|
||||||
|
Log.error("[FileUtil]: Cannot get the file descriptor from uri: " + path);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return parcelFileDescriptor.detachFd();
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot open content uri, error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytes;
|
/**
|
||||||
|
* Reference: https://stackoverflow.com/questions/42186820/documentfile-is-very-slow
|
||||||
|
* This function will be faster than DoucmentFile.listFiles
|
||||||
|
* @param context Application context
|
||||||
|
* @param uri Directory uri.
|
||||||
|
* @return CheapDocument lists.
|
||||||
|
*/
|
||||||
|
public static MinimalDocumentFile[] listFiles(Context context, Uri uri) {
|
||||||
|
final ContentResolver resolver = context.getContentResolver();
|
||||||
|
final String[] columns = new String[]{
|
||||||
|
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||||
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||||
|
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||||
|
};
|
||||||
|
Cursor c = null;
|
||||||
|
final List<MinimalDocumentFile> results = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
String docId;
|
||||||
|
if (isRootTreeUri(uri)) {
|
||||||
|
docId = DocumentsContract.getTreeDocumentId(uri);
|
||||||
|
} else {
|
||||||
|
docId = DocumentsContract.getDocumentId(uri);
|
||||||
|
}
|
||||||
|
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId);
|
||||||
|
c = resolver.query(childrenUri, columns, null, null, null);
|
||||||
|
while(c.moveToNext()) {
|
||||||
|
final String documentId = c.getString(0);
|
||||||
|
final String documentName = c.getString(1);
|
||||||
|
final String documentMimeType = c.getString(2);
|
||||||
|
final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, documentId);
|
||||||
|
MinimalDocumentFile document = new MinimalDocumentFile(documentName, documentMimeType, documentUri);
|
||||||
|
results.add(document);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot list file error: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
closeQuietly(c);
|
||||||
|
}
|
||||||
|
return results.toArray(new MinimalDocumentFile[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether given path exists.
|
||||||
|
* @param path Native content uri path
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static boolean Exists(Context context, String path) {
|
||||||
|
Cursor c = null;
|
||||||
|
try {
|
||||||
|
Uri mUri = Uri.parse(path);
|
||||||
|
final String[] columns = new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID };
|
||||||
|
c = context.getContentResolver().query(mUri, columns, null, null, null);
|
||||||
|
return c.getCount() > 0;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.info("[FileUtil] Cannot find file from given path, error: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
closeQuietly(c);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether given path is a directory
|
||||||
|
* @param path content uri path
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static boolean isDirectory(Context context, String path) {
|
||||||
|
final ContentResolver resolver = context.getContentResolver();
|
||||||
|
final String[] columns = new String[] {
|
||||||
|
DocumentsContract.Document.COLUMN_MIME_TYPE
|
||||||
|
};
|
||||||
|
boolean isDirectory = false;
|
||||||
|
Cursor c = null;
|
||||||
|
try {
|
||||||
|
Uri mUri = Uri.parse(path);
|
||||||
|
c = resolver.query(mUri, columns, null, null, null);
|
||||||
|
c.moveToNext();
|
||||||
|
final String mimeType = c.getString(0);
|
||||||
|
isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot list files, error: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
closeQuietly(c);
|
||||||
|
}
|
||||||
|
return isDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file display name from given path
|
||||||
|
* @param path content uri path
|
||||||
|
* @return String display name
|
||||||
|
*/
|
||||||
|
public static String getFilename(Context context, String path) {
|
||||||
|
final ContentResolver resolver = context.getContentResolver();
|
||||||
|
final String[] columns = new String[] {
|
||||||
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME
|
||||||
|
};
|
||||||
|
String filename = "";
|
||||||
|
Cursor c = null;
|
||||||
|
try {
|
||||||
|
Uri mUri = Uri.parse(path);
|
||||||
|
c = resolver.query(mUri, columns, null, null, null);
|
||||||
|
c.moveToNext();
|
||||||
|
filename = c.getString(0);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot get file size, error: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
closeQuietly(c);
|
||||||
|
}
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String[] getFilesName(Context context, String path) {
|
||||||
|
Uri uri = Uri.parse(path);
|
||||||
|
List<String> files = new ArrayList<>();
|
||||||
|
for (MinimalDocumentFile file: FileUtil.listFiles(context, uri)) {
|
||||||
|
files.add(file.getFilename());
|
||||||
|
}
|
||||||
|
return files.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file size from given path.
|
||||||
|
* @param path content uri path
|
||||||
|
* @return long file size
|
||||||
|
*/
|
||||||
|
public static long getFileSize(Context context, String path) {
|
||||||
|
final ContentResolver resolver = context.getContentResolver();
|
||||||
|
final String[] columns = new String[] {
|
||||||
|
DocumentsContract.Document.COLUMN_SIZE
|
||||||
|
};
|
||||||
|
long size = 0;
|
||||||
|
Cursor c =null;
|
||||||
|
try {
|
||||||
|
Uri mUri = Uri.parse(path);
|
||||||
|
c = resolver.query(mUri, columns, null, null, null);
|
||||||
|
c.moveToNext();
|
||||||
|
size = c.getLong(0);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.error("[FileUtil]: Cannot get file size, error: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
closeQuietly(c);
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isRootTreeUri(Uri uri) {
|
||||||
|
final List<String> paths = uri.getPathSegments();
|
||||||
|
return paths.size() == 2 && PATH_TREE.equals(paths.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void closeQuietly(AutoCloseable closeable) {
|
||||||
|
if (closeable != null) {
|
||||||
|
try {
|
||||||
|
closeable.close();
|
||||||
|
} catch (RuntimeException rethrown) {
|
||||||
|
throw rethrown;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
package org.yuzu.yuzu_emu.utils;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.fragment.app.FragmentActivity;
|
|
||||||
|
|
||||||
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
|
||||||
|
|
||||||
public class PermissionsHandler {
|
|
||||||
public static final int REQUEST_CODE_WRITE_PERMISSION = 500;
|
|
||||||
|
|
||||||
// We use permissions acceptance as an indicator if this is a first boot for the user.
|
|
||||||
public static boolean isFirstBoot(final FragmentActivity activity) {
|
|
||||||
return ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.M)
|
|
||||||
public static boolean checkWritePermission(final FragmentActivity activity) {
|
|
||||||
if (isFirstBoot(activity)) {
|
|
||||||
activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE},
|
|
||||||
REQUEST_CODE_WRITE_PERMISSION);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean hasWriteAccess(Context context) {
|
|
||||||
return ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,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>
|
<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>
|
||||||
|
@ -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