mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-07-11 01:25:07 +02:00
Android refactor: Move all contentUri utilities to the new class ContentUri
This commit is contained in:
@@ -27,14 +27,14 @@ static jmethodID isExternalStoragePreservedLegacy;
|
||||
static jmethodID computeRecursiveDirectorySize;
|
||||
|
||||
static jobject g_nativeActivity;
|
||||
static jclass g_classActivity;
|
||||
static jclass g_classContentUri;
|
||||
|
||||
void Android_StorageSetActivity(jobject nativeActivity) {
|
||||
g_nativeActivity = nativeActivity;
|
||||
}
|
||||
|
||||
void Android_RegisterStorageCallbacks(JNIEnv * env, jobject obj) {
|
||||
jclass localClass = env->FindClass("org/ppsspp/ppsspp/PpssppActivity");
|
||||
jclass localClass = env->FindClass("org/ppsspp/ppsspp/ContentUri");
|
||||
_dbg_assert_(localClass);
|
||||
|
||||
openContentUri = env->GetStaticMethodID(localClass, "openContentUri", "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)I");
|
||||
@@ -66,14 +66,14 @@ void Android_RegisterStorageCallbacks(JNIEnv * env, jobject obj) {
|
||||
computeRecursiveDirectorySize = env->GetStaticMethodID(localClass, "computeRecursiveDirectorySize", "(Landroid/app/Activity;Ljava/lang/String;)J");
|
||||
_dbg_assert_(computeRecursiveDirectorySize);
|
||||
|
||||
g_classActivity = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
|
||||
g_classContentUri = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
|
||||
env->DeleteLocalRef(localClass); // cleanup local ref
|
||||
}
|
||||
|
||||
void Android_UnregisterStorageCallbacks(JNIEnv * env) {
|
||||
if (g_classActivity) {
|
||||
env->DeleteGlobalRef(g_classActivity);
|
||||
g_classActivity = nullptr;
|
||||
if (g_classContentUri) {
|
||||
env->DeleteGlobalRef(g_classContentUri);
|
||||
g_classContentUri = nullptr;
|
||||
}
|
||||
g_nativeActivity = nullptr;
|
||||
openContentUri = nullptr;
|
||||
@@ -125,7 +125,7 @@ int Android_OpenContentUriFd(std::string_view filename, Android_OpenContentUriMo
|
||||
}
|
||||
jstring j_filename = env->NewStringUTF(fname.c_str());
|
||||
jstring j_mode = env->NewStringUTF(modeStr);
|
||||
int fd = env->CallStaticIntMethod(g_classActivity, openContentUri, g_nativeActivity, j_filename, j_mode);
|
||||
int fd = env->CallStaticIntMethod(g_classContentUri, openContentUri, g_nativeActivity, j_filename, j_mode);
|
||||
return fd;
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ StorageError Android_CreateDirectory(const std::string &rootTreeUri, const std::
|
||||
auto env = getEnv();
|
||||
jstring paramRoot = env->NewStringUTF(rootTreeUri.c_str());
|
||||
jstring paramDirName = env->NewStringUTF(dirName.c_str());
|
||||
return StorageErrorFromInt(env->CallStaticIntMethod(g_classActivity, contentUriCreateDirectory, g_nativeActivity, paramRoot, paramDirName));
|
||||
return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriCreateDirectory, g_nativeActivity, paramRoot, paramDirName));
|
||||
}
|
||||
|
||||
StorageError Android_CreateFile(const std::string &parentTreeUri, const std::string &fileName) {
|
||||
@@ -146,7 +146,7 @@ StorageError Android_CreateFile(const std::string &parentTreeUri, const std::str
|
||||
auto env = getEnv();
|
||||
jstring paramRoot = env->NewStringUTF(parentTreeUri.c_str());
|
||||
jstring paramFileName = env->NewStringUTF(fileName.c_str());
|
||||
return StorageErrorFromInt(env->CallStaticIntMethod(g_classActivity, contentUriCreateFile, g_nativeActivity, paramRoot, paramFileName));
|
||||
return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriCreateFile, g_nativeActivity, paramRoot, paramFileName));
|
||||
}
|
||||
|
||||
StorageError Android_CopyFile(const std::string &fileUri, const std::string &destParentUri) {
|
||||
@@ -156,7 +156,7 @@ StorageError Android_CopyFile(const std::string &fileUri, const std::string &des
|
||||
auto env = getEnv();
|
||||
jstring paramFileName = env->NewStringUTF(fileUri.c_str());
|
||||
jstring paramDestParentUri = env->NewStringUTF(destParentUri.c_str());
|
||||
return StorageErrorFromInt(env->CallStaticIntMethod(g_classActivity, contentUriCopyFile, g_nativeActivity, paramFileName, paramDestParentUri));
|
||||
return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriCopyFile, g_nativeActivity, paramFileName, paramDestParentUri));
|
||||
}
|
||||
|
||||
StorageError Android_MoveFile(const std::string &fileUri, const std::string &srcParentUri, const std::string &destParentUri) {
|
||||
@@ -167,7 +167,7 @@ StorageError Android_MoveFile(const std::string &fileUri, const std::string &src
|
||||
jstring paramFileName = env->NewStringUTF(fileUri.c_str());
|
||||
jstring paramSrcParentUri = env->NewStringUTF(srcParentUri.c_str());
|
||||
jstring paramDestParentUri = env->NewStringUTF(destParentUri.c_str());
|
||||
return StorageErrorFromInt(env->CallStaticIntMethod(g_classActivity, contentUriMoveFile, g_nativeActivity, paramFileName, paramSrcParentUri, paramDestParentUri));
|
||||
return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriMoveFile, g_nativeActivity, paramFileName, paramSrcParentUri, paramDestParentUri));
|
||||
}
|
||||
|
||||
StorageError Android_RemoveFile(const std::string &fileUri) {
|
||||
@@ -176,7 +176,7 @@ StorageError Android_RemoveFile(const std::string &fileUri) {
|
||||
}
|
||||
auto env = getEnv();
|
||||
jstring paramFileName = env->NewStringUTF(fileUri.c_str());
|
||||
return StorageErrorFromInt(env->CallStaticIntMethod(g_classActivity, contentUriRemoveFile, g_nativeActivity, paramFileName));
|
||||
return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriRemoveFile, g_nativeActivity, paramFileName));
|
||||
}
|
||||
|
||||
StorageError Android_RenameFileTo(const std::string &fileUri, const std::string &newName) {
|
||||
@@ -186,7 +186,7 @@ StorageError Android_RenameFileTo(const std::string &fileUri, const std::string
|
||||
auto env = getEnv();
|
||||
jstring paramFileUri = env->NewStringUTF(fileUri.c_str());
|
||||
jstring paramNewName = env->NewStringUTF(newName.c_str());
|
||||
return StorageErrorFromInt(env->CallStaticIntMethod(g_classActivity, contentUriRenameFileTo, g_nativeActivity, paramFileUri, paramNewName));
|
||||
return StorageErrorFromInt(env->CallStaticIntMethod(g_classContentUri, contentUriRenameFileTo, g_nativeActivity, paramFileUri, paramNewName));
|
||||
}
|
||||
|
||||
// NOTE: Does not set fullName - you're supposed to already know it.
|
||||
@@ -232,7 +232,7 @@ bool Android_GetFileInfo(const std::string &fileUri, File::FileInfo *fileInfo) {
|
||||
auto env = getEnv();
|
||||
jstring paramFileUri = env->NewStringUTF(fileUri.c_str());
|
||||
|
||||
jstring str = (jstring)env->CallStaticObjectMethod(g_classActivity, contentUriGetFileInfo, g_nativeActivity, paramFileUri);
|
||||
jstring str = (jstring)env->CallStaticObjectMethod(g_classContentUri, contentUriGetFileInfo, g_nativeActivity, paramFileUri);
|
||||
if (!str) {
|
||||
return false;
|
||||
}
|
||||
@@ -250,7 +250,7 @@ bool Android_FileExists(const std::string &fileUri) {
|
||||
}
|
||||
auto env = getEnv();
|
||||
jstring paramFileUri = env->NewStringUTF(fileUri.c_str());
|
||||
bool exists = env->CallStaticBooleanMethod(g_classActivity, contentUriFileExists, g_nativeActivity, paramFileUri);
|
||||
bool exists = env->CallStaticBooleanMethod(g_classContentUri, contentUriFileExists, g_nativeActivity, paramFileUri);
|
||||
return exists;
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ std::vector<File::FileInfo> Android_ListContentUri(const std::string &uri, const
|
||||
double start = time_now_d();
|
||||
|
||||
jstring param = env->NewStringUTF(uri.c_str());
|
||||
jobject retval = env->CallStaticObjectMethod(g_classActivity, listContentUriDir, g_nativeActivity, param);
|
||||
jobject retval = env->CallStaticObjectMethod(g_classContentUri, listContentUriDir, g_nativeActivity, param);
|
||||
|
||||
jobjectArray fileList = (jobjectArray)retval;
|
||||
std::vector<File::FileInfo> items;
|
||||
@@ -306,7 +306,7 @@ int64_t Android_GetFreeSpaceByContentUri(const std::string &uri) {
|
||||
auto env = getEnv();
|
||||
|
||||
jstring param = env->NewStringUTF(uri.c_str());
|
||||
return env->CallStaticLongMethod(g_classActivity, contentUriGetFreeStorageSpace, g_nativeActivity, param);
|
||||
return env->CallStaticLongMethod(g_classContentUri, contentUriGetFreeStorageSpace, g_nativeActivity, param);
|
||||
}
|
||||
|
||||
// Hm, this is never used? We use statvfs instead.
|
||||
@@ -322,7 +322,7 @@ int64_t Android_GetFreeSpaceByFilePath(const std::string &filePath) {
|
||||
}
|
||||
|
||||
jstring param = env->NewStringUTF(filePath.c_str());
|
||||
return env->CallStaticLongMethod(g_classActivity, filePathGetFreeStorageSpace, g_nativeActivity, param);
|
||||
return env->CallStaticLongMethod(g_classContentUri, filePathGetFreeStorageSpace, g_nativeActivity, param);
|
||||
}
|
||||
|
||||
int64_t Android_ComputeRecursiveDirectorySize(const std::string &uri) {
|
||||
@@ -334,7 +334,7 @@ int64_t Android_ComputeRecursiveDirectorySize(const std::string &uri) {
|
||||
jstring param = env->NewStringUTF(uri.c_str());
|
||||
|
||||
double start = time_now_d();
|
||||
int64_t size = env->CallStaticLongMethod(g_classActivity, computeRecursiveDirectorySize, g_nativeActivity, param);
|
||||
int64_t size = env->CallStaticLongMethod(g_classContentUri, computeRecursiveDirectorySize, g_nativeActivity, param);
|
||||
double elapsed = time_now_d() - start;
|
||||
|
||||
INFO_LOG(Log::IO, "ComputeRecursiveDirectorySize(%s) in %0.3f s", uri.c_str(), elapsed);
|
||||
@@ -347,7 +347,7 @@ bool Android_IsExternalStoragePreservedLegacy() {
|
||||
}
|
||||
auto env = getEnv();
|
||||
// Note: No activity param
|
||||
return env->CallStaticBooleanMethod(g_classActivity, isExternalStoragePreservedLegacy);
|
||||
return env->CallStaticBooleanMethod(g_classContentUri, isExternalStoragePreservedLegacy);
|
||||
}
|
||||
|
||||
const char *Android_ErrorToString(StorageError error) {
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
package org.ppsspp.ppsspp;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.storage.StorageManager;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.system.Os;
|
||||
import android.system.StructStatVfs;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ContentUri {
|
||||
private static final String TAG = "PpssppActivity"; // don't want to add yet another tag
|
||||
|
||||
// Matches the enum in AndroidStorage.h.
|
||||
private static final int STORAGE_ERROR_SUCCESS = 0;
|
||||
private static final int STORAGE_ERROR_UNKNOWN = -1;
|
||||
private static final int STORAGE_ERROR_NOT_FOUND = -2;
|
||||
private static final int STORAGE_ERROR_DISK_FULL = -3;
|
||||
private static final int STORAGE_ERROR_ALREADY_EXISTS = -4;
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static int openContentUri(Activity activity, String uriString, String mode) {
|
||||
try {
|
||||
Uri uri = Uri.parse(uriString);
|
||||
try (ParcelFileDescriptor filePfd = activity.getContentResolver().openFileDescriptor(uri, mode)) {
|
||||
if (filePfd == null) {
|
||||
// I'd expect an exception to happen before we get here, so this is probably
|
||||
// never reached.
|
||||
Log.e(TAG, "Failed to get file descriptor for " + uriString);
|
||||
return -1;
|
||||
}
|
||||
return filePfd.detachFd(); // Take ownership of the fd.
|
||||
}
|
||||
} catch (java.lang.IllegalArgumentException e) {
|
||||
// This exception is long and ugly and really just means file not found.
|
||||
// We don't log anything (the caller can log).
|
||||
return -1;
|
||||
} catch (Exception e) {
|
||||
// Don't know when this might happen. Let's log. Still, the result is just a
|
||||
// failure that the caller may additionally log.
|
||||
Log.e(TAG, "Unexpected openContentUri exception: " + e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String[] columns = new String[] {
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_SIZE,
|
||||
DocumentsContract.Document.COLUMN_FLAGS,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE, // check for MIME_TYPE_DIR
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED
|
||||
};
|
||||
|
||||
private static String cursorToString(Cursor c) {
|
||||
final int flags = c.getInt(2);
|
||||
// Filter out any virtual or partial nonsense.
|
||||
// There's a bunch of potentially-interesting flags here btw,
|
||||
// to figure out how to set access flags better, etc.
|
||||
// Like FLAG_SUPPORTS_WRITE etc.
|
||||
if ((flags & (DocumentsContract.Document.FLAG_PARTIAL | DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT)) != 0) {
|
||||
return null;
|
||||
}
|
||||
final String mimeType = c.getString(3);
|
||||
final boolean isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
|
||||
final String documentName = c.getString(0);
|
||||
final long size = isDirectory ? 0 : c.getLong(1);
|
||||
final long lastModified = c.getLong(4);
|
||||
|
||||
String str = "F|";
|
||||
if (isDirectory) {
|
||||
str = "D|";
|
||||
}
|
||||
return str + size + "|" + documentName + "|" + lastModified;
|
||||
}
|
||||
|
||||
private static long directorySizeRecursion(Activity activity, Uri uri) {
|
||||
Cursor c = null;
|
||||
try {
|
||||
// Log.i(TAG, "recursing into " + uri.toString());
|
||||
final String[] columns = new String[]{
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_SIZE,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE, // check for MIME_TYPE_DIR
|
||||
};
|
||||
final ContentResolver resolver = activity.getContentResolver();
|
||||
final String documentId = DocumentsContract.getDocumentId(uri);
|
||||
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, documentId);
|
||||
c = resolver.query(childrenUri, columns, null, null, null);
|
||||
long sizeSum = 0;
|
||||
|
||||
// Buffer the URIs so we only have one cursor active at once. I don't trust the storage framework
|
||||
// to handle more than one...
|
||||
ArrayList<Uri> childDirs = new ArrayList<>();
|
||||
|
||||
if (c != null) {
|
||||
while (c.moveToNext()) {
|
||||
final String mimeType = c.getString(2);
|
||||
final boolean isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
|
||||
if (isDirectory) {
|
||||
final String childDocumentId = c.getString(0);
|
||||
final Uri childUri = DocumentsContract.buildDocumentUriUsingTree(uri, childDocumentId);
|
||||
childDirs.add(childUri);
|
||||
} else {
|
||||
final long fileSize = c.getLong(1);
|
||||
sizeSum += fileSize;
|
||||
}
|
||||
}
|
||||
c.close();
|
||||
}
|
||||
c = null;
|
||||
|
||||
for (Uri childUri : childDirs) {
|
||||
long dirSize = directorySizeRecursion(activity, childUri);
|
||||
if (dirSize >= 0) {
|
||||
sizeSum += dirSize;
|
||||
} else {
|
||||
return dirSize;
|
||||
}
|
||||
}
|
||||
return sizeSum;
|
||||
} catch (Exception e) {
|
||||
return -1;
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static long computeRecursiveDirectorySize(Activity activity, String uriString) {
|
||||
try {
|
||||
Uri uri = Uri.parse(uriString);
|
||||
return directorySizeRecursion(activity, uri);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.e(TAG, "computeRecursiveSize exception: " + e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Maybe add a cheaper version that doesn't extract all the file information?
|
||||
// TODO: Replace with a proper query:
|
||||
// * https://stackoverflow.com/q
|
||||
// uestions/42186820/documentfile-is-very-slow
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static String[] listContentUriDir(Activity activity, String uriString) {
|
||||
try {
|
||||
Uri uri = Uri.parse(uriString);
|
||||
final ContentResolver resolver = activity.getContentResolver();
|
||||
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
|
||||
uri, DocumentsContract.getDocumentId(uri));
|
||||
final ArrayList<String> listing = new ArrayList<>();
|
||||
|
||||
String[] projection = {
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME, // index 0
|
||||
DocumentsContract.Document.COLUMN_SIZE, // index 1
|
||||
DocumentsContract.Document.COLUMN_FLAGS, // index 2
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE, // index 3
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED // index 4
|
||||
};
|
||||
|
||||
try (Cursor c = resolver.query(childrenUri, projection, null, null, null)) {
|
||||
if (c == null) {
|
||||
return new String[]{"X"};
|
||||
}
|
||||
while (c.moveToNext()) {
|
||||
String str = cursorToString(c);
|
||||
if (str != null) {
|
||||
listing.add(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return listing.toArray(new String[0]);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return new String[]{"X"};
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "listContentUriDir exception: " + e);
|
||||
return new String[]{"X"};
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static int contentUriCreateDirectory(Activity activity, String rootTreeUri, String dirName) {
|
||||
try {
|
||||
Uri uri = Uri.parse(rootTreeUri);
|
||||
DocumentFile documentFile = DocumentFile.fromTreeUri(activity, uri);
|
||||
if (documentFile != null) {
|
||||
DocumentFile createdDir = documentFile.createDirectory(dirName);
|
||||
return createdDir != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN;
|
||||
} else {
|
||||
Log.e(TAG, "contentUriCreateDirectory: fromTreeUri returned null");
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "contentUriCreateDirectory exception: " + e);
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static int contentUriCreateFile(Activity activity, String rootTreeUri, String fileName) {
|
||||
try {
|
||||
Uri uri = Uri.parse(rootTreeUri);
|
||||
DocumentFile documentFile = DocumentFile.fromTreeUri(activity, uri);
|
||||
if (documentFile != null) {
|
||||
// TODO: Check the file extension and choose MIME type appropriately.
|
||||
// Or actually, let's not bother.
|
||||
DocumentFile createdFile = documentFile.createFile("application/octet-stream", fileName);
|
||||
return createdFile != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN;
|
||||
} else {
|
||||
Log.e(TAG, "contentUriCreateFile: fromTreeUrisv returned null");
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "contentUriCreateFile exception: " + e);
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
public static int contentUriRemoveFile(Activity activity, String fileName) {
|
||||
try {
|
||||
Uri uri = Uri.parse(fileName);
|
||||
DocumentFile documentFile = DocumentFile.fromSingleUri(activity, uri);
|
||||
if (documentFile != null) {
|
||||
return documentFile.delete() ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN;
|
||||
} else {
|
||||
// This can return null on old Android versions (that we no longer supports).
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "contentUriRemoveFile exception: " + e);
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: The destination is the parent directory! This means that contentUriCopyFile
|
||||
// cannot rename things as part of the operation.
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static int contentUriCopyFile(Activity activity, String srcFileUri, String dstParentDirUri) {
|
||||
try {
|
||||
Uri srcUri = Uri.parse(srcFileUri);
|
||||
Uri dstParentUri = Uri.parse(dstParentDirUri);
|
||||
return DocumentsContract.copyDocument(activity.getContentResolver(), srcUri, dstParentUri) != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "contentUriCopyFile exception: " + e);
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: The destination is the parent directory! This means that contentUriCopyFile
|
||||
// cannot rename things as part of the operation.
|
||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static int contentUriMoveFile(Activity activity, String srcFileUri, String srcParentDirUri, String dstParentDirUri) {
|
||||
try {
|
||||
Uri srcUri = Uri.parse(srcFileUri);
|
||||
Uri srcParentUri = Uri.parse(srcParentDirUri);
|
||||
Uri dstParentUri = Uri.parse(dstParentDirUri);
|
||||
Log.i(TAG, "DocumentsContract.moveDocument");
|
||||
int result = DocumentsContract.moveDocument(activity.getContentResolver(), srcUri, srcParentUri, dstParentUri) != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN;
|
||||
Log.i(TAG, "DocumentsContract.moveDocument done");
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "contentUriMoveFile exception: " + e);
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static int contentUriRenameFileTo(Activity activity, String fileUri, String newName) {
|
||||
try {
|
||||
Uri uri = Uri.parse(fileUri);
|
||||
// Due to a design flaw, we can't use DocumentFile.renameTo().
|
||||
// Instead we use the DocumentsContract API directly.
|
||||
// See https://stackoverflow.com/questions/37168200/android-5-0-new-sd-card-access-api-documentfile-renameto-unsupportedoperation.
|
||||
Uri newUri = DocumentsContract.renameDocument(activity.getContentResolver(), uri, newName);
|
||||
// NOTE: we don't use the returned newUri for anything right now.
|
||||
return STORAGE_ERROR_SUCCESS;
|
||||
} catch (Exception e) {
|
||||
// TODO: More detailed exception processing.
|
||||
Log.e(TAG, "contentUriRenameFile exception: " + e);
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
private static void closeQuietly(AutoCloseable closeable) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (RuntimeException rethrown) {
|
||||
throw rethrown;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Probably slightly faster than contentUriGetFileInfo.
|
||||
// Smaller difference now than before I changed that one to a query...
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static boolean contentUriFileExists(Activity activity, String fileUri) {
|
||||
Cursor c = null;
|
||||
try {
|
||||
Uri uri = Uri.parse(fileUri);
|
||||
c = activity.getContentResolver().query(uri, new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null);
|
||||
if (c != null) {
|
||||
return c.getCount() > 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Log.w(TAG, "Failed query: " + e);
|
||||
return false;
|
||||
} finally {
|
||||
closeQuietly(c);
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static String contentUriGetFileInfo(Activity activity, String fileName) {
|
||||
String[] projection = {
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME, // index 0
|
||||
DocumentsContract.Document.COLUMN_SIZE, // index 1
|
||||
DocumentsContract.Document.COLUMN_FLAGS, // index 2
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE, // index 3
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED // index 4
|
||||
};
|
||||
|
||||
try (Cursor c = activity.getContentResolver().query(Uri.parse(fileName), projection, null, null, null)) {
|
||||
if (c != null && c.moveToFirst()) {
|
||||
return cursorToString(c);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "contentUriGetFileInfo exception: " + e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// The example in Android documentation uses this.getFilesDir for path.
|
||||
// There's also a way to beg the OS for more space, which might clear caches, but
|
||||
// let's just not bother with that for now.
|
||||
// NOTE: This is really super slow!
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static long contentUriGetFreeStorageSpaceSlow(Activity activity, Uri uri) {
|
||||
try {
|
||||
ParcelFileDescriptor pfd = activity.getContentResolver().openFileDescriptor(uri, "r");
|
||||
if (pfd == null) {
|
||||
Log.w(TAG, "Failed to get free storage space from URI: " + uri);
|
||||
return -1;
|
||||
}
|
||||
StructStatVfs stats = Os.fstatvfs(pfd.getFileDescriptor());
|
||||
long freeSpace = stats.f_bavail * stats.f_bsize;
|
||||
pfd.close();
|
||||
return freeSpace;
|
||||
} catch (Exception e) {
|
||||
// FileNotFoundException | ErrnoException e
|
||||
// Log.getStackTraceString(e)
|
||||
Log.e(TAG, "contentUriGetFreeStorageSpace exception: " + e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static long contentUriGetFreeStorageSpace(Activity activity, String str) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Uri uri = Uri.parse(str);
|
||||
return contentUriGetFreeStorageSpaceSlow(activity, uri);
|
||||
}
|
||||
|
||||
// Too early Android version
|
||||
return -1;
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static long filePathGetFreeStorageSpace(Activity activity, String filePath) {
|
||||
try {
|
||||
StorageManager storageManager = activity.getApplicationContext().getSystemService(StorageManager.class);
|
||||
File file = new File(filePath);
|
||||
UUID volumeUUID = storageManager.getUuidForPath(file);
|
||||
return storageManager.getAllocatableBytes(volumeUUID);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "filePathGetFreeStorageSpace exception: " + e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static boolean isExternalStoragePreservedLegacy() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// In 29 and later, we can check whether we got preserved storage legacy.
|
||||
return Environment.isExternalStorageLegacy();
|
||||
} else {
|
||||
// In 28 and earlier, we won't call this - we'll still request an exception.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,13 +37,6 @@ public class PpssppActivity extends NativeActivity {
|
||||
|
||||
public static boolean libraryLoaded = false;
|
||||
|
||||
// Matches the enum in AndroidStorage.h.
|
||||
private static final int STORAGE_ERROR_SUCCESS = 0;
|
||||
private static final int STORAGE_ERROR_UNKNOWN = -1;
|
||||
private static final int STORAGE_ERROR_NOT_FOUND = -2;
|
||||
private static final int STORAGE_ERROR_DISK_FULL = -3;
|
||||
private static final int STORAGE_ERROR_ALREADY_EXISTS = -4;
|
||||
|
||||
public static void CheckABIAndLoadLibrary() {
|
||||
try {
|
||||
System.loadLibrary("ppsspp_jni");
|
||||
@@ -156,402 +149,4 @@ public class PpssppActivity extends NativeActivity {
|
||||
return "bad debug string: " + str;
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static int openContentUri(Activity activity, String uriString, String mode) {
|
||||
try {
|
||||
Uri uri = Uri.parse(uriString);
|
||||
try (ParcelFileDescriptor filePfd = activity.getContentResolver().openFileDescriptor(uri, mode)) {
|
||||
if (filePfd == null) {
|
||||
// I'd expect an exception to happen before we get here, so this is probably
|
||||
// never reached.
|
||||
Log.e(TAG, "Failed to get file descriptor for " + uriString);
|
||||
return -1;
|
||||
}
|
||||
return filePfd.detachFd(); // Take ownership of the fd.
|
||||
}
|
||||
} catch (java.lang.IllegalArgumentException e) {
|
||||
// This exception is long and ugly and really just means file not found.
|
||||
// We don't log anything (the caller can log).
|
||||
return -1;
|
||||
} catch (Exception e) {
|
||||
// Don't know when this might happen. Let's log. Still, the result is just a
|
||||
// failure that the caller may additionally log.
|
||||
Log.e(TAG, "Unexpected openContentUri exception: " + e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String[] columns = new String[] {
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_SIZE,
|
||||
DocumentsContract.Document.COLUMN_FLAGS,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE, // check for MIME_TYPE_DIR
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED
|
||||
};
|
||||
|
||||
private static String cursorToString(Cursor c) {
|
||||
final int flags = c.getInt(2);
|
||||
// Filter out any virtual or partial nonsense.
|
||||
// There's a bunch of potentially-interesting flags here btw,
|
||||
// to figure out how to set access flags better, etc.
|
||||
// Like FLAG_SUPPORTS_WRITE etc.
|
||||
if ((flags & (DocumentsContract.Document.FLAG_PARTIAL | DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT)) != 0) {
|
||||
return null;
|
||||
}
|
||||
final String mimeType = c.getString(3);
|
||||
final boolean isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
|
||||
final String documentName = c.getString(0);
|
||||
final long size = isDirectory ? 0 : c.getLong(1);
|
||||
final long lastModified = c.getLong(4);
|
||||
|
||||
String str = "F|";
|
||||
if (isDirectory) {
|
||||
str = "D|";
|
||||
}
|
||||
return str + size + "|" + documentName + "|" + lastModified;
|
||||
}
|
||||
|
||||
private static long directorySizeRecursion(Activity activity, Uri uri) {
|
||||
Cursor c = null;
|
||||
try {
|
||||
// Log.i(TAG, "recursing into " + uri.toString());
|
||||
final String[] columns = new String[]{
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_SIZE,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE, // check for MIME_TYPE_DIR
|
||||
};
|
||||
final ContentResolver resolver = activity.getContentResolver();
|
||||
final String documentId = DocumentsContract.getDocumentId(uri);
|
||||
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, documentId);
|
||||
c = resolver.query(childrenUri, columns, null, null, null);
|
||||
long sizeSum = 0;
|
||||
|
||||
// Buffer the URIs so we only have one cursor active at once. I don't trust the storage framework
|
||||
// to handle more than one...
|
||||
ArrayList<Uri> childDirs = new ArrayList<>();
|
||||
|
||||
if (c != null) {
|
||||
while (c.moveToNext()) {
|
||||
final String mimeType = c.getString(2);
|
||||
final boolean isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
|
||||
if (isDirectory) {
|
||||
final String childDocumentId = c.getString(0);
|
||||
final Uri childUri = DocumentsContract.buildDocumentUriUsingTree(uri, childDocumentId);
|
||||
childDirs.add(childUri);
|
||||
} else {
|
||||
final long fileSize = c.getLong(1);
|
||||
sizeSum += fileSize;
|
||||
}
|
||||
}
|
||||
c.close();
|
||||
}
|
||||
c = null;
|
||||
|
||||
for (Uri childUri : childDirs) {
|
||||
long dirSize = directorySizeRecursion(activity, childUri);
|
||||
if (dirSize >= 0) {
|
||||
sizeSum += dirSize;
|
||||
} else {
|
||||
return dirSize;
|
||||
}
|
||||
}
|
||||
return sizeSum;
|
||||
} catch (Exception e) {
|
||||
return -1;
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static long computeRecursiveDirectorySize(Activity activity, String uriString) {
|
||||
try {
|
||||
Uri uri = Uri.parse(uriString);
|
||||
return directorySizeRecursion(activity, uri);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.e(TAG, "computeRecursiveSize exception: " + e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Maybe add a cheaper version that doesn't extract all the file information?
|
||||
// TODO: Replace with a proper query:
|
||||
// * https://stackoverflow.com/q
|
||||
// uestions/42186820/documentfile-is-very-slow
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static String[] listContentUriDir(Activity activity, String uriString) {
|
||||
try {
|
||||
Uri uri = Uri.parse(uriString);
|
||||
final ContentResolver resolver = activity.getContentResolver();
|
||||
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
|
||||
uri, DocumentsContract.getDocumentId(uri));
|
||||
final ArrayList<String> listing = new ArrayList<>();
|
||||
|
||||
String[] projection = {
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME, // index 0
|
||||
DocumentsContract.Document.COLUMN_SIZE, // index 1
|
||||
DocumentsContract.Document.COLUMN_FLAGS, // index 2
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE, // index 3
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED // index 4
|
||||
};
|
||||
|
||||
try (Cursor c = resolver.query(childrenUri, projection, null, null, null)) {
|
||||
if (c == null) {
|
||||
return new String[]{"X"};
|
||||
}
|
||||
while (c.moveToNext()) {
|
||||
String str = cursorToString(c);
|
||||
if (str != null) {
|
||||
listing.add(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return listing.toArray(new String[0]);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return new String[]{"X"};
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "listContentUriDir exception: " + e);
|
||||
return new String[]{"X"};
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static int contentUriCreateDirectory(Activity activity, String rootTreeUri, String dirName) {
|
||||
try {
|
||||
Uri uri = Uri.parse(rootTreeUri);
|
||||
DocumentFile documentFile = DocumentFile.fromTreeUri(activity, uri);
|
||||
if (documentFile != null) {
|
||||
DocumentFile createdDir = documentFile.createDirectory(dirName);
|
||||
return createdDir != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN;
|
||||
} else {
|
||||
Log.e(TAG, "contentUriCreateDirectory: fromTreeUri returned null");
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "contentUriCreateDirectory exception: " + e);
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static int contentUriCreateFile(Activity activity, String rootTreeUri, String fileName) {
|
||||
try {
|
||||
Uri uri = Uri.parse(rootTreeUri);
|
||||
DocumentFile documentFile = DocumentFile.fromTreeUri(activity, uri);
|
||||
if (documentFile != null) {
|
||||
// TODO: Check the file extension and choose MIME type appropriately.
|
||||
// Or actually, let's not bother.
|
||||
DocumentFile createdFile = documentFile.createFile("application/octet-stream", fileName);
|
||||
return createdFile != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN;
|
||||
} else {
|
||||
Log.e(TAG, "contentUriCreateFile: fromTreeUrisv returned null");
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "contentUriCreateFile exception: " + e);
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
public static int contentUriRemoveFile(Activity activity, String fileName) {
|
||||
try {
|
||||
Uri uri = Uri.parse(fileName);
|
||||
DocumentFile documentFile = DocumentFile.fromSingleUri(activity, uri);
|
||||
if (documentFile != null) {
|
||||
return documentFile.delete() ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN;
|
||||
} else {
|
||||
// This can return null on old Android versions (that we no longer supports).
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "contentUriRemoveFile exception: " + e);
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: The destination is the parent directory! This means that contentUriCopyFile
|
||||
// cannot rename things as part of the operation.
|
||||
@RequiresApi(Build.VERSION_CODES.N)
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static int contentUriCopyFile(Activity activity, String srcFileUri, String dstParentDirUri) {
|
||||
try {
|
||||
Uri srcUri = Uri.parse(srcFileUri);
|
||||
Uri dstParentUri = Uri.parse(dstParentDirUri);
|
||||
return DocumentsContract.copyDocument(activity.getContentResolver(), srcUri, dstParentUri) != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "contentUriCopyFile exception: " + e);
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: The destination is the parent directory! This means that contentUriCopyFile
|
||||
// cannot rename things as part of the operation.
|
||||
@RequiresApi(Build.VERSION_CODES.N_MR1)
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static int contentUriMoveFile(Activity activity, String srcFileUri, String srcParentDirUri, String dstParentDirUri) {
|
||||
try {
|
||||
Uri srcUri = Uri.parse(srcFileUri);
|
||||
Uri srcParentUri = Uri.parse(srcParentDirUri);
|
||||
Uri dstParentUri = Uri.parse(dstParentDirUri);
|
||||
Log.i(TAG, "DocumentsContract.moveDocument");
|
||||
int result = DocumentsContract.moveDocument(activity.getContentResolver(), srcUri, srcParentUri, dstParentUri) != null ? STORAGE_ERROR_SUCCESS : STORAGE_ERROR_UNKNOWN;
|
||||
Log.i(TAG, "DocumentsContract.moveDocument done");
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "contentUriMoveFile exception: " + e);
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static int contentUriRenameFileTo(Activity activity, String fileUri, String newName) {
|
||||
try {
|
||||
Uri uri = Uri.parse(fileUri);
|
||||
// Due to a design flaw, we can't use DocumentFile.renameTo().
|
||||
// Instead we use the DocumentsContract API directly.
|
||||
// See https://stackoverflow.com/questions/37168200/android-5-0-new-sd-card-access-api-documentfile-renameto-unsupportedoperation.
|
||||
Uri newUri = DocumentsContract.renameDocument(activity.getContentResolver(), uri, newName);
|
||||
// NOTE: we don't use the returned newUri for anything right now.
|
||||
return STORAGE_ERROR_SUCCESS;
|
||||
} catch (Exception e) {
|
||||
// TODO: More detailed exception processing.
|
||||
Log.e(TAG, "contentUriRenameFile exception: " + e);
|
||||
return STORAGE_ERROR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
private static void closeQuietly(AutoCloseable closeable) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (RuntimeException rethrown) {
|
||||
throw rethrown;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Probably slightly faster than contentUriGetFileInfo.
|
||||
// Smaller difference now than before I changed that one to a query...
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static boolean contentUriFileExists(Activity activity, String fileUri) {
|
||||
Cursor c = null;
|
||||
try {
|
||||
Uri uri = Uri.parse(fileUri);
|
||||
c = activity.getContentResolver().query(uri, new String[] { DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null);
|
||||
if (c != null) {
|
||||
return c.getCount() > 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Log.w(TAG, "Failed query: " + e);
|
||||
return false;
|
||||
} finally {
|
||||
closeQuietly(c);
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static String contentUriGetFileInfo(Activity activity, String fileName) {
|
||||
String[] projection = {
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME, // index 0
|
||||
DocumentsContract.Document.COLUMN_SIZE, // index 1
|
||||
DocumentsContract.Document.COLUMN_FLAGS, // index 2
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE, // index 3
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED // index 4
|
||||
};
|
||||
|
||||
try (Cursor c = activity.getContentResolver().query(Uri.parse(fileName), projection, null, null, null)) {
|
||||
if (c != null && c.moveToFirst()) {
|
||||
return cursorToString(c);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "contentUriGetFileInfo exception: " + e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// The example in Android documentation uses this.getFilesDir for path.
|
||||
// There's also a way to beg the OS for more space, which might clear caches, but
|
||||
// let's just not bother with that for now.
|
||||
// NOTE: This is really super slow!
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static long contentUriGetFreeStorageSpaceSlow(Activity activity, Uri uri) {
|
||||
try {
|
||||
ParcelFileDescriptor pfd = activity.getContentResolver().openFileDescriptor(uri, "r");
|
||||
if (pfd == null) {
|
||||
Log.w(TAG, "Failed to get free storage space from URI: " + uri);
|
||||
return -1;
|
||||
}
|
||||
StructStatVfs stats = Os.fstatvfs(pfd.getFileDescriptor());
|
||||
long freeSpace = stats.f_bavail * stats.f_bsize;
|
||||
pfd.close();
|
||||
return freeSpace;
|
||||
} catch (Exception e) {
|
||||
// FileNotFoundException | ErrnoException e
|
||||
// Log.getStackTraceString(e)
|
||||
Log.e(TAG, "contentUriGetFreeStorageSpace exception: " + e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static long contentUriGetFreeStorageSpace(Activity activity, String str) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Uri uri = Uri.parse(str);
|
||||
return contentUriGetFreeStorageSpaceSlow(activity, uri);
|
||||
}
|
||||
|
||||
// Too early Android version
|
||||
return -1;
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static long filePathGetFreeStorageSpace(Activity activity, String filePath) {
|
||||
try {
|
||||
StorageManager storageManager = activity.getApplicationContext().getSystemService(StorageManager.class);
|
||||
File file = new File(filePath);
|
||||
UUID volumeUUID = storageManager.getUuidForPath(file);
|
||||
return storageManager.getAllocatableBytes(volumeUUID);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "filePathGetFreeStorageSpace exception: " + e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@SuppressWarnings("unused")
|
||||
public static boolean isExternalStoragePreservedLegacy() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// In 29 and later, we can check whether we got preserved storage legacy.
|
||||
return Environment.isExternalStorageLegacy();
|
||||
} else {
|
||||
// In 28 and earlier, we won't call this - we'll still request an exception.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user