mirror of
https://github.com/RetroDECK/Duckstation.git
synced 2024-11-23 14:25:37 +00:00
Android: Use SAF paths for scanning
This commit is contained in:
parent
d6d8d21eff
commit
c3f914565f
|
@ -305,19 +305,15 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
|
||||||
applySettings();
|
applySettings();
|
||||||
}
|
}
|
||||||
} else if (requestCode == REQUEST_IMPORT_PATCH_CODES) {
|
} else if (requestCode == REQUEST_IMPORT_PATCH_CODES) {
|
||||||
if (data == null)
|
if (data == null || data.getData() == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
importPatchesFromFile(data.getData());
|
importPatchesFromFile(data.getData());
|
||||||
} else if (requestCode == REQUEST_CHANGE_DISC_FILE) {
|
} else if (requestCode == REQUEST_CHANGE_DISC_FILE) {
|
||||||
if (data == null)
|
if (data == null || data.getData() == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
String path = GameDirectoriesActivity.getPathFromUri(this, data.getData());
|
AndroidHostInterface.getInstance().setMediaFilename(data.getDataString());
|
||||||
if (path == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
AndroidHostInterface.getInstance().setMediaFilename(path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -687,7 +683,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
|
||||||
}
|
}
|
||||||
|
|
||||||
private void importPatchesFromFile(Uri uri) {
|
private void importPatchesFromFile(Uri uri) {
|
||||||
String str = FileUtil.readFileFromUri(this, uri, 512 * 1024);
|
String str = FileHelper.readStringFromUri(this, uri, 512 * 1024);
|
||||||
if (str == null || !AndroidHostInterface.getInstance().importPatchCodesFromString(str)) {
|
if (str == null || !AndroidHostInterface.getInstance().importPatchCodesFromString(str)) {
|
||||||
reportErrorOnUIThread(getString(R.string.emulation_activity_failed_to_import_patch_codes));
|
reportErrorOnUIThread(getString(R.string.emulation_activity_failed_to_import_patch_codes));
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,22 @@ package com.github.stenzek.duckstation;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.ImageDecoder;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.provider.DocumentsContract;
|
import android.provider.DocumentsContract;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,6 +58,7 @@ public class FileHelper {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* File helper class - used to bridge native code to Java storage access framework APIs.
|
* File helper class - used to bridge native code to Java storage access framework APIs.
|
||||||
|
*
|
||||||
* @param context Context in which to perform file actions as.
|
* @param context Context in which to perform file actions as.
|
||||||
*/
|
*/
|
||||||
public FileHelper(Context context) {
|
public FileHelper(Context context) {
|
||||||
|
@ -53,10 +66,187 @@ public class FileHelper {
|
||||||
this.contentResolver = context.getContentResolver();
|
this.contentResolver = context.getContentResolver();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the specified file as a string, under the specified context.
|
||||||
|
*
|
||||||
|
* @param context context to access file under
|
||||||
|
* @param uri uri to write data to
|
||||||
|
* @param maxSize maximum file size to read
|
||||||
|
* @return String containing the file data, otherwise null
|
||||||
|
*/
|
||||||
|
public static String readStringFromUri(final Context context, final Uri uri, int maxSize) {
|
||||||
|
InputStream stream = null;
|
||||||
|
try {
|
||||||
|
stream = context.getContentResolver().openInputStream(uri);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder os = new StringBuilder();
|
||||||
|
try {
|
||||||
|
char[] buffer = new char[1024];
|
||||||
|
InputStreamReader reader = new InputStreamReader(stream, Charset.forName(StandardCharsets.UTF_8.name()));
|
||||||
|
int len;
|
||||||
|
while ((len = reader.read(buffer)) > 0) {
|
||||||
|
os.append(buffer, 0, len);
|
||||||
|
if (os.length() > maxSize)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (os.length() == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return os.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the specified file as a byte array, under the specified context.
|
||||||
|
*
|
||||||
|
* @param context context to access file under
|
||||||
|
* @param uri uri to write data to
|
||||||
|
* @param maxSize maximum file size to read
|
||||||
|
* @return byte array containing the file data, otherwise null
|
||||||
|
*/
|
||||||
|
public static byte[] readBytesFromUri(final Context context, final Uri uri, int maxSize) {
|
||||||
|
InputStream stream = null;
|
||||||
|
try {
|
||||||
|
stream = context.getContentResolver().openInputStream(uri);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
|
try {
|
||||||
|
byte[] buffer = new byte[512 * 1024];
|
||||||
|
int len;
|
||||||
|
while ((len = stream.read(buffer)) > 0) {
|
||||||
|
os.write(buffer, 0, len);
|
||||||
|
if (maxSize > 0 && os.size() > maxSize) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (os.size() == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return os.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the specified data to a file referenced by the URI, as the specified context.
|
||||||
|
*
|
||||||
|
* @param context context to access file under
|
||||||
|
* @param uri uri to write data to
|
||||||
|
* @param bytes data to write file to
|
||||||
|
* @return true if write was succesful, otherwise false
|
||||||
|
*/
|
||||||
|
public static boolean writeBytesToUri(final Context context, final Uri uri, final byte[] bytes) {
|
||||||
|
OutputStream stream = null;
|
||||||
|
try {
|
||||||
|
stream = context.getContentResolver().openOutputStream(uri);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes != null && bytes.length > 0) {
|
||||||
|
try {
|
||||||
|
stream.write(bytes);
|
||||||
|
stream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the file referenced by the URI, under the specified context.
|
||||||
|
*
|
||||||
|
* @param context context to delete file under
|
||||||
|
* @param uri uri to delete
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static boolean deleteFileAtUri(final Context context, final Uri uri) {
|
||||||
|
try {
|
||||||
|
if (uri.getScheme() == "file") {
|
||||||
|
final File file = new File(uri.getPath());
|
||||||
|
if (!file.isFile())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return file.delete();
|
||||||
|
}
|
||||||
|
return (context.getContentResolver().delete(uri, null, null) > 0);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of the file pointed at by a SAF URI.
|
||||||
|
*
|
||||||
|
* @param context context to access file under
|
||||||
|
* @param uri uri to retrieve file name for
|
||||||
|
* @return the name of the file, or null
|
||||||
|
*/
|
||||||
|
public static String getDocumentNameFromUri(final Context context, final Uri uri) {
|
||||||
|
Cursor cursor = null;
|
||||||
|
try {
|
||||||
|
final String[] proj = {DocumentsContract.Document.COLUMN_DISPLAY_NAME};
|
||||||
|
cursor = context.getContentResolver().query(uri, proj, null, null, null);
|
||||||
|
final int columnIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME);
|
||||||
|
cursor.moveToFirst();
|
||||||
|
return cursor.getString(columnIndex);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (cursor != null)
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a bitmap from the provided SAF URI.
|
||||||
|
*
|
||||||
|
* @param context context to access file under
|
||||||
|
* @param uri uri to retrieve file name for
|
||||||
|
* @return a decoded bitmap for the file, or null
|
||||||
|
*/
|
||||||
|
public static Bitmap loadBitmapFromUri(final Context context, final Uri uri) {
|
||||||
|
InputStream stream = null;
|
||||||
|
try {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
||||||
|
final ImageDecoder.Source source = ImageDecoder.createSource(context.getContentResolver(), uri);
|
||||||
|
return ImageDecoder.decodeBitmap(source);
|
||||||
|
} else {
|
||||||
|
return MediaStore.Images.Media.getBitmap(context.getContentResolver(), uri);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a file descriptor for a content URI string. Called by native code.
|
* Retrieves a file descriptor for a content URI string. Called by native code.
|
||||||
|
*
|
||||||
* @param uriString string of the URI to open
|
* @param uriString string of the URI to open
|
||||||
* @param mode Java open mode
|
* @param mode Java open mode
|
||||||
* @return file descriptor for URI, or -1
|
* @return file descriptor for URI, or -1
|
||||||
*/
|
*/
|
||||||
public int openURIAsFileDescriptor(String uriString, String mode) {
|
public int openURIAsFileDescriptor(String uriString, String mode) {
|
||||||
|
@ -73,10 +263,11 @@ public class FileHelper {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively iterates documents in the specified tree, searching for files.
|
* Recursively iterates documents in the specified tree, searching for files.
|
||||||
* @param treeUri Root tree in which to search for documents.
|
*
|
||||||
|
* @param treeUri Root tree in which to search for documents.
|
||||||
* @param documentId Document ID representing the directory to start searching.
|
* @param documentId Document ID representing the directory to start searching.
|
||||||
* @param flags Native search flags.
|
* @param flags Native search flags.
|
||||||
* @param results Cumulative result array.
|
* @param results Cumulative result array.
|
||||||
*/
|
*/
|
||||||
private void doFindFiles(Uri treeUri, String documentId, int flags, ArrayList<FindResult> results) {
|
private void doFindFiles(Uri treeUri, String documentId, int flags, ArrayList<FindResult> results) {
|
||||||
try {
|
try {
|
||||||
|
@ -116,8 +307,9 @@ public class FileHelper {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively iterates documents in the specified URI, searching for files.
|
* Recursively iterates documents in the specified URI, searching for files.
|
||||||
|
*
|
||||||
* @param uriString URI containing directory to search.
|
* @param uriString URI containing directory to search.
|
||||||
* @param flags Native filter flags.
|
* @param flags Native filter flags.
|
||||||
* @return Array of find results.
|
* @return Array of find results.
|
||||||
*/
|
*/
|
||||||
public FindResult[] findFiles(String uriString, int flags) {
|
public FindResult[] findFiles(String uriString, int flags) {
|
||||||
|
|
|
@ -1,293 +0,0 @@
|
||||||
package com.github.stenzek.duckstation;
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/34927748/android-5-0-documentfile-from-tree-uri
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.graphics.ImageDecoder;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.storage.StorageManager;
|
|
||||||
import android.provider.DocumentsContract;
|
|
||||||
import android.provider.MediaStore;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.lang.reflect.Array;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
|
|
||||||
public final class FileUtil {
|
|
||||||
static String TAG = "TAG";
|
|
||||||
private static final String PRIMARY_VOLUME_NAME = "primary";
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) {
|
|
||||||
if (treeUri == null) return null;
|
|
||||||
String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri), con);
|
|
||||||
if (volumePath == null) return File.separator;
|
|
||||||
if (volumePath.endsWith(File.separator))
|
|
||||||
volumePath = volumePath.substring(0, volumePath.length() - 1);
|
|
||||||
|
|
||||||
String documentPath = getDocumentPathFromTreeUri(treeUri);
|
|
||||||
if (documentPath.endsWith(File.separator))
|
|
||||||
documentPath = documentPath.substring(0, documentPath.length() - 1);
|
|
||||||
|
|
||||||
if (documentPath.length() > 0) {
|
|
||||||
if (documentPath.startsWith(File.separator))
|
|
||||||
return volumePath + documentPath;
|
|
||||||
else
|
|
||||||
return volumePath + File.separator + documentPath;
|
|
||||||
} else return volumePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ObsoleteSdkInt")
|
|
||||||
private static String getVolumePath(final String volumeId, Context context) {
|
|
||||||
if (volumeId == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null;
|
|
||||||
try {
|
|
||||||
StorageManager mStorageManager =
|
|
||||||
(StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
|
|
||||||
Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
|
|
||||||
Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
|
|
||||||
Method getUuid = storageVolumeClazz.getMethod("getUuid");
|
|
||||||
Method getPath = storageVolumeClazz.getMethod("getPath");
|
|
||||||
Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
|
|
||||||
Object result = getVolumeList.invoke(mStorageManager);
|
|
||||||
|
|
||||||
final int length = Array.getLength(result);
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
Object storageVolumeElement = Array.get(result, i);
|
|
||||||
String uuid = (String) getUuid.invoke(storageVolumeElement);
|
|
||||||
Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);
|
|
||||||
|
|
||||||
// primary volume?
|
|
||||||
if (primary && PRIMARY_VOLUME_NAME.equals(volumeId))
|
|
||||||
return (String) getPath.invoke(storageVolumeElement);
|
|
||||||
|
|
||||||
// other volumes?
|
|
||||||
if (uuid != null && uuid.equals(volumeId))
|
|
||||||
return (String) getPath.invoke(storageVolumeElement);
|
|
||||||
}
|
|
||||||
// not found.
|
|
||||||
return null;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
private static String getVolumeIdFromTreeUri(final Uri treeUri) {
|
|
||||||
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
|
|
||||||
final String[] split = docId.split(":");
|
|
||||||
if (split.length > 0) return split[0];
|
|
||||||
else return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
private static String getDocumentPathFromTreeUri(final Uri treeUri) {
|
|
||||||
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
|
|
||||||
final String[] split = docId.split(":");
|
|
||||||
if ((split.length >= 2) && (split[1] != null)) return split[1];
|
|
||||||
else return File.separator;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static String getFullPathFromUri(@Nullable final Uri treeUri, Context con) {
|
|
||||||
if (treeUri == null) return null;
|
|
||||||
String volumePath = getVolumePath(getVolumeIdFromUri(treeUri), con);
|
|
||||||
if (volumePath == null) return File.separator;
|
|
||||||
if (volumePath.endsWith(File.separator))
|
|
||||||
volumePath = volumePath.substring(0, volumePath.length() - 1);
|
|
||||||
|
|
||||||
String documentPath = getDocumentPathFromUri(treeUri);
|
|
||||||
if (documentPath.endsWith(File.separator))
|
|
||||||
documentPath = documentPath.substring(0, documentPath.length() - 1);
|
|
||||||
|
|
||||||
if (documentPath.length() > 0) {
|
|
||||||
if (documentPath.startsWith(File.separator))
|
|
||||||
return volumePath + documentPath;
|
|
||||||
else
|
|
||||||
return volumePath + File.separator + documentPath;
|
|
||||||
} else return volumePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
private static String getVolumeIdFromUri(final Uri treeUri) {
|
|
||||||
try {
|
|
||||||
final String docId = DocumentsContract.getDocumentId(treeUri);
|
|
||||||
final String[] split = docId.split(":");
|
|
||||||
if (split.length > 0) return split[0];
|
|
||||||
else return null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
private static String getDocumentPathFromUri(final Uri treeUri) {
|
|
||||||
try {
|
|
||||||
final String docId = DocumentsContract.getDocumentId(treeUri);
|
|
||||||
final String[] split = docId.split(":");
|
|
||||||
if ((split.length >= 2) && (split[1] != null)) return split[1];
|
|
||||||
else return File.separator;
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String readFileFromUri(final Context context, final Uri uri, int maxSize) {
|
|
||||||
InputStream stream = null;
|
|
||||||
try {
|
|
||||||
stream = context.getContentResolver().openInputStream(uri);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder os = new StringBuilder();
|
|
||||||
try {
|
|
||||||
char[] buffer = new char[1024];
|
|
||||||
InputStreamReader reader = new InputStreamReader(stream, Charset.forName(StandardCharsets.UTF_8.name()));
|
|
||||||
int len;
|
|
||||||
while ((len = reader.read(buffer)) > 0) {
|
|
||||||
os.append(buffer, 0, len);
|
|
||||||
if (os.length() > maxSize)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (os.length() == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return os.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] readBytesFromUri(final Context context, final Uri uri, int maxSize) {
|
|
||||||
InputStream stream = null;
|
|
||||||
try {
|
|
||||||
stream = context.getContentResolver().openInputStream(uri);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
|
||||||
try {
|
|
||||||
byte[] buffer = new byte[512 * 1024];
|
|
||||||
int len;
|
|
||||||
while ((len = stream.read(buffer)) > 0) {
|
|
||||||
os.write(buffer, 0, len);
|
|
||||||
if (maxSize > 0 && os.size() > maxSize) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (os.size() == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return os.toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean writeBytesToUri(final Context context, final Uri uri, final byte[] bytes) {
|
|
||||||
OutputStream stream = null;
|
|
||||||
try {
|
|
||||||
stream = context.getContentResolver().openOutputStream(uri);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bytes != null && bytes.length > 0) {
|
|
||||||
try {
|
|
||||||
stream.write(bytes);
|
|
||||||
stream.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean deleteFileAtUri(final Context context, final Uri uri) {
|
|
||||||
try {
|
|
||||||
if (uri.getScheme() == "file") {
|
|
||||||
final File file = new File(uri.getPath());
|
|
||||||
if (!file.isFile())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return file.delete();
|
|
||||||
}
|
|
||||||
return (context.getContentResolver().delete(uri, null, null) > 0);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the name of the file pointed at by a SAF URI.
|
|
||||||
* @param context context to access file under
|
|
||||||
* @param uri uri to retrieve file name for
|
|
||||||
* @return the name of the file, or null
|
|
||||||
*/
|
|
||||||
public static String getDocumentNameFromUri(final Context context, final Uri uri) {
|
|
||||||
Cursor cursor = null;
|
|
||||||
try {
|
|
||||||
final String[] proj = {DocumentsContract.Document.COLUMN_DISPLAY_NAME};
|
|
||||||
cursor = context.getContentResolver().query(uri, proj, null, null, null);
|
|
||||||
final int columnIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME);
|
|
||||||
cursor.moveToFirst();
|
|
||||||
return cursor.getString(columnIndex);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
if (cursor != null)
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Bitmap loadBitmapFromUri(final Context context, final Uri uri) {
|
|
||||||
InputStream stream = null;
|
|
||||||
try {
|
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
|
||||||
final ImageDecoder.Source source =ImageDecoder.createSource(context.getContentResolver(), uri);
|
|
||||||
return ImageDecoder.decodeBitmap(source);
|
|
||||||
} else {
|
|
||||||
return MediaStore.Images.Media.getBitmap(context.getContentResolver(), uri);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -239,38 +239,6 @@ public class GameDirectoriesActivity extends AppCompatActivity {
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getPathFromTreeUri(Context context, Uri treeUri) {
|
|
||||||
String path = FileUtil.getFullPathFromTreeUri(treeUri, context);
|
|
||||||
if (path.length() < 5) {
|
|
||||||
new AlertDialog.Builder(context)
|
|
||||||
.setTitle(R.string.main_activity_error)
|
|
||||||
.setMessage(R.string.main_activity_get_path_from_directory_error)
|
|
||||||
.setPositiveButton(R.string.main_activity_ok, (dialog, button) -> {
|
|
||||||
})
|
|
||||||
.create()
|
|
||||||
.show();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getPathFromUri(Context context, Uri uri) {
|
|
||||||
String path = FileUtil.getFullPathFromUri(uri, context);
|
|
||||||
if (path.length() < 5) {
|
|
||||||
new AlertDialog.Builder(context)
|
|
||||||
.setTitle(R.string.main_activity_error)
|
|
||||||
.setMessage(R.string.main_activity_get_path_from_file_error)
|
|
||||||
.setPositiveButton(R.string.main_activity_ok, (dialog, button) -> {
|
|
||||||
})
|
|
||||||
.create()
|
|
||||||
.show();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void addSearchDirectory(Context context, String path, boolean recursive) {
|
public static void addSearchDirectory(Context context, String path, boolean recursive) {
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
final String key = recursive ? "GameList/RecursivePaths" : "GameList/Paths";
|
final String key = recursive ? "GameList/RecursivePaths" : "GameList/Paths";
|
||||||
|
@ -289,6 +257,8 @@ public class GameDirectoriesActivity extends AppCompatActivity {
|
||||||
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
i.addCategory(Intent.CATEGORY_DEFAULT);
|
i.addCategory(Intent.CATEGORY_DEFAULT);
|
||||||
i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
|
i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
|
||||||
|
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
i.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||||
startActivityForResult(Intent.createChooser(i, getString(R.string.main_activity_choose_directory)),
|
startActivityForResult(Intent.createChooser(i, getString(R.string.main_activity_choose_directory)),
|
||||||
REQUEST_ADD_DIRECTORY_TO_GAME_LIST);
|
REQUEST_ADD_DIRECTORY_TO_GAME_LIST);
|
||||||
}
|
}
|
||||||
|
@ -318,14 +288,18 @@ public class GameDirectoriesActivity extends AppCompatActivity {
|
||||||
|
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
case REQUEST_ADD_DIRECTORY_TO_GAME_LIST: {
|
case REQUEST_ADD_DIRECTORY_TO_GAME_LIST: {
|
||||||
if (resultCode != RESULT_OK)
|
if (resultCode != RESULT_OK || data.getData() == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
String path = getPathFromTreeUri(this, data.getData());
|
try {
|
||||||
if (path == null)
|
getContentResolver().takePersistableUriPermission(data.getData(),
|
||||||
return;
|
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Toast.makeText(this, "Failed to take persistable permission.", Toast.LENGTH_LONG);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
addSearchDirectory(this, path, true);
|
addSearchDirectory(this, data.getDataString(), true);
|
||||||
mDirectoryListAdapter.reload();
|
mDirectoryListAdapter.reload();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -9,7 +9,6 @@ import android.content.pm.PackageManager;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -18,12 +17,10 @@ import android.view.Gravity;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.appcompat.app.AppCompatDelegate;
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
|
@ -195,6 +192,8 @@ public class MainActivity extends AppCompatActivity {
|
||||||
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
i.addCategory(Intent.CATEGORY_DEFAULT);
|
i.addCategory(Intent.CATEGORY_DEFAULT);
|
||||||
i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
|
i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
|
||||||
|
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
i.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||||
startActivityForResult(Intent.createChooser(i, getString(R.string.main_activity_choose_directory)),
|
startActivityForResult(Intent.createChooser(i, getString(R.string.main_activity_choose_directory)),
|
||||||
REQUEST_ADD_DIRECTORY_TO_GAME_LIST);
|
REQUEST_ADD_DIRECTORY_TO_GAME_LIST);
|
||||||
}
|
}
|
||||||
|
@ -270,14 +269,18 @@ public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
case REQUEST_ADD_DIRECTORY_TO_GAME_LIST: {
|
case REQUEST_ADD_DIRECTORY_TO_GAME_LIST: {
|
||||||
if (resultCode != RESULT_OK)
|
if (resultCode != RESULT_OK || data.getData() == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
String path = GameDirectoriesActivity.getPathFromTreeUri(this, data.getData());
|
try {
|
||||||
if (path == null)
|
getContentResolver().takePersistableUriPermission(data.getData(),
|
||||||
return;
|
Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Toast.makeText(this, "Failed to take persistable permission.", Toast.LENGTH_LONG);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
GameDirectoriesActivity.addSearchDirectory(this, path, true);
|
GameDirectoriesActivity.addSearchDirectory(this, data.getDataString(), true);
|
||||||
mGameList.refresh(false, false, this);
|
mGameList.refresh(false, false, this);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -291,14 +294,10 @@ public class MainActivity extends AppCompatActivity {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case REQUEST_START_FILE: {
|
case REQUEST_START_FILE: {
|
||||||
if (resultCode != RESULT_OK)
|
if (resultCode != RESULT_OK || data.getData() == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
String path = GameDirectoriesActivity.getPathFromUri(this, data.getData());
|
startEmulation(data.getDataString(), shouldResumeStateByDefault());
|
||||||
if (path == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
startEmulation(path, shouldResumeStateByDefault());
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
@ -428,7 +427,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
if (gameListEntry == null)
|
if (gameListEntry == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
final Bitmap bitmap = FileUtil.loadBitmapFromUri(this, uri);
|
final Bitmap bitmap = FileHelper.loadBitmapFromUri(this, uri);
|
||||||
if (bitmap == null) {
|
if (bitmap == null) {
|
||||||
Toast.makeText(this, "Failed to open/decode image.", Toast.LENGTH_LONG).show();
|
Toast.makeText(this, "Failed to open/decode image.", Toast.LENGTH_LONG).show();
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -286,13 +286,13 @@ public class MemoryCardEditorActivity extends AppCompatActivity {
|
||||||
if (card == null)
|
if (card == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
final byte[] data = FileUtil.readBytesFromUri(this, uri, 16 * 1024 * 1024);
|
final byte[] data = FileHelper.readBytesFromUri(this, uri, 16 * 1024 * 1024);
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
displayError(getString(R.string.memory_card_editor_import_card_read_failed, uri.toString()));
|
displayError(getString(R.string.memory_card_editor_import_card_read_failed, uri.toString()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String importFileName = FileUtil.getDocumentNameFromUri(this, uri);
|
String importFileName = FileHelper.getDocumentNameFromUri(this, uri);
|
||||||
if (importFileName == null) {
|
if (importFileName == null) {
|
||||||
importFileName = uri.getPath();
|
importFileName = uri.getPath();
|
||||||
if (importFileName == null || importFileName.isEmpty())
|
if (importFileName == null || importFileName.isEmpty())
|
||||||
|
|
|
@ -41,7 +41,7 @@ public class MemoryCardImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MemoryCardImage open(Context context, Uri uri) {
|
public static MemoryCardImage open(Context context, Uri uri) {
|
||||||
byte[] data = FileUtil.readBytesFromUri(context, uri, DATA_SIZE);
|
byte[] data = FileHelper.readBytesFromUri(context, uri, DATA_SIZE);
|
||||||
if (data == null)
|
if (data == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
@ -145,11 +145,11 @@ public class MemoryCardImage {
|
||||||
private static native boolean importCard(byte[] data, String filename, byte[] importData);
|
private static native boolean importCard(byte[] data, String filename, byte[] importData);
|
||||||
|
|
||||||
public boolean save() {
|
public boolean save() {
|
||||||
return FileUtil.writeBytesToUri(context, uri, data);
|
return FileHelper.writeBytesToUri(context, uri, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean delete() {
|
public boolean delete() {
|
||||||
return FileUtil.deleteFileAtUri(context, uri);
|
return FileHelper.deleteFileAtUri(context, uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean format() {
|
public boolean format() {
|
||||||
|
|
Loading…
Reference in a new issue