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; |
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) { |
|
||||
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); |
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 |
|
||||
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; |
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.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 { |
|
||||
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; |
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.appcompat.app.AlertDialog; |
||||
import androidx.fragment.app.FragmentActivity; |
|
||||
|
|
||||
import org.yuzu.yuzu_emu.R; |
import org.yuzu.yuzu_emu.R; |
||||
import org.yuzu.yuzu_emu.activities.EmulationActivity; |
|
||||
|
import org.yuzu.yuzu_emu.YuzuApplication; |
||||
|
import org.yuzu.yuzu_emu.ui.main.MainActivity; |
||||
|
import org.yuzu.yuzu_emu.ui.main.MainPresenter; |
||||
|
|
||||
public final class StartupHandler { |
public final class StartupHandler { |
||||
private static void handlePermissionsCheck(FragmentActivity parent) { |
|
||||
// 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 |
// Prompt user with standard first boot disclaimer |
||||
new AlertDialog.Builder(parent) |
new AlertDialog.Builder(parent) |
||||
.setTitle(R.string.app_name) |
.setTitle(R.string.app_name) |
||||
.setIcon(R.mipmap.ic_launcher) |
.setIcon(R.mipmap.ic_launcher) |
||||
.setMessage(parent.getResources().getString(R.string.app_disclaimer)) |
.setMessage(parent.getResources().getString(R.string.app_disclaimer)) |
||||
.setPositiveButton(android.R.string.ok, null) |
.setPositiveButton(android.R.string.ok, null) |
||||
.setOnDismissListener(dialogInterface -> handlePermissionsCheck(parent)) |
|
||||
|
.setOnDismissListener(dialogInterface -> handleStartupPromptDismiss(parent)) |
||||
.show(); |
.show(); |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -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 |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue