Merge pull request #20973 from hrydgard/resize-improvements

Assorted UI improvements
This commit is contained in:
Henrik Rydgård
2025-11-07 01:13:47 +01:00
committed by GitHub
16 changed files with 524 additions and 465 deletions
+20 -20
View File
@@ -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) {
+5 -5
View File
@@ -100,12 +100,12 @@ inline bool CompareByArea(const Data& lhs, const Data& rhs) {
return lhs.w * lhs.h > rhs.w * rhs.h;
}
std::vector<Data> Bucket::Resolve(int image_width, Image &dest) {
std::vector<Data> Bucket::Resolve(int image_width, Image *dest) {
// Place all the little images - whatever they are.
// Uses greedy fill algorithm. Slow but works surprisingly well, CPUs are fast.
ImageU8 masq;
masq.resize(image_width, 1);
dest.resize(image_width, 1);
dest->resize(image_width, 1);
std::sort(data.begin(), data.end(), CompareByArea);
for (int i = 0; i < (int)data.size(); i++) {
if ((i + 1) % 2000 == 0) {
@@ -116,10 +116,10 @@ std::vector<Data> Bucket::Resolve(int image_width, Image &dest) {
if (idx > 1 && idy > 1) {
assert(idx <= image_width);
for (int ty = 0; ty < 2047; ty++) {
if (ty + idy + 1 > (int)dest.height()) {
if (ty + idy + 1 > (int)dest->height()) {
// Every 16 lines of new space needed, grow the image.
masq.resize(image_width, ty + idy + 16);
dest.resize(image_width, ty + idy + 16);
dest->resize(image_width, ty + idy + 16);
}
// Brute force packing.
int sz = (int)data[i].w;
@@ -160,7 +160,7 @@ std::vector<Data> Bucket::Resolve(int image_width, Image &dest) {
// Actually copy the image data in place, after doing the layout.
for (int i = 0; i < (int)data.size(); i++) {
dest.copyfrom(images[i], data[i].sx, data[i].sy, data[i].redToWhiteAlpha);
dest->copyfrom(images[i], data[i].sx, data[i].sy, data[i].redToWhiteAlpha);
}
return data;
+7 -1
View File
@@ -46,6 +46,12 @@ struct Image {
Image(Image &&) = default;
Image &operator=(Image &&) = default;
void clear() {
dat.clear();
w = 0;
h = 0;
}
float scale = 1.0f;
int w = 0;
@@ -118,7 +124,7 @@ struct Bucket {
data.emplace_back(dat);
}
void AddImage(Image &&img, int id);
std::vector<Data> Resolve(int image_width, Image &dest);
std::vector<Data> Resolve(int image_width, Image *dest);
};
AtlasImage ToAtlasImage(int id, std::string_view name, float tw, float th, const std::vector<Data> &results);
+2 -2
View File
@@ -46,7 +46,7 @@ void UIContext::BeginFrame() {
if (uitexture_) {
uitexture_->Release();
}
AtlasData data = atlasProvider_(draw_, AtlasChoice::General, 1.0f / g_display.dpi_scale_x);
AtlasData data = atlasProvider_(draw_, AtlasChoice::General, 1.0f / g_display.dpi_scale_x, atlasInvalid_);
uitexture_ = data.texture;
_dbg_assert_(uitexture_);
ui_draw2d.SetAtlas(data.atlas);
@@ -54,7 +54,7 @@ void UIContext::BeginFrame() {
}
if (!fontTexture_) {
AtlasData data = atlasProvider_(draw_, AtlasChoice::Font, 1.0f / g_display.dpi_scale_x);
AtlasData data = atlasProvider_(draw_, AtlasChoice::Font, 1.0f / g_display.dpi_scale_x, false);
fontTexture_ = data.texture;
_dbg_assert_(fontTexture_);
ui_draw2d.SetFontAtlas(data.atlas);
+1 -1
View File
@@ -54,7 +54,7 @@ struct AtlasData {
Draw::Texture *texture;
};
typedef std::function<AtlasData(Draw::DrawContext *, AtlasChoice, float dpiScale)> UIAtlasProviderFunc;
typedef std::function<AtlasData(Draw::DrawContext *, AtlasChoice, float dpiScale, bool invalidate)> UIAtlasProviderFunc;
class UIContext {
public:
+1
View File
@@ -331,6 +331,7 @@ static const ConfigSetting generalSettings[] = {
ConfigSetting("WindowY", SETTING(g_Config, iWindowY), -1, CfgFlag::DEFAULT),
ConfigSetting("WindowWidth", SETTING(g_Config, iWindowWidth), 0, CfgFlag::DEFAULT), // 0 will be automatically reset later (need to do the AdjustWindowRect dance).
ConfigSetting("WindowHeight", SETTING(g_Config, iWindowHeight), 0, CfgFlag::DEFAULT),
ConfigSetting("ShrinkIfWindowSmall", SETTING(g_Config, bShrinkIfWindowSmall), false, CfgFlag::DEFAULT),
#endif
ConfigSetting("PauseWhenMinimized", SETTING(g_Config, bPauseWhenMinimized), false, CfgFlag::PER_GAME),
+1
View File
@@ -194,6 +194,7 @@ public:
bool bDisableHTTPS;
bool bShrinkIfWindowSmall;
bool bSeparateSASThread;
int iIOTimingMethod;
int iLockedCPUSpeed;
+2 -2
View File
@@ -237,11 +237,11 @@ void GameSettingsScreen::CreateTabs() {
CreateNetworkingSettings(parent);
});
AddTab("GameSettingsTools", ms->T("Tools"), ImageID("I_DEVMENU"), [this](UI::LinearLayout *parent) {
AddTab("GameSettingsTools", ms->T("Tools"), ImageID("I_TOOLS"), [this](UI::LinearLayout *parent) {
CreateToolsSettings(parent);
});
AddTab("GameSettingsSystem", ms->T("System"), ImageID("I_GEAR"), [this](UI::LinearLayout *parent) {
AddTab("GameSettingsSystem", ms->T("System"), ImageID("I_PSP"), [this](UI::LinearLayout *parent) {
parent->SetSpacing(0);
CreateSystemSettings(parent);
});
+4
View File
@@ -1645,6 +1645,10 @@ bool Native_IsWindowHidden() {
}
static bool IsWindowSmall(int pixelWidth, int pixelHeight) {
if (!g_Config.bShrinkIfWindowSmall) {
return false;
}
// Can't take this from config as it will not be set if windows is maximized.
int w = (int)(pixelWidth * g_display.dpi_scale_real_x);
int h = (int)(pixelHeight * g_display.dpi_scale_real_y);
+28 -12
View File
@@ -153,6 +153,8 @@ static const ImageMeta imageIDs[] = {
{"I_DEVMENU", false},
{"I_CONTROLLER", false},
{"I_DEBUGGER", false},
{"I_TOOLS", false},
{"I_PSP", false},
};
static std::string PNGNameFromID(std::string_view id) {
@@ -178,7 +180,7 @@ static bool IsImageID(std::string_view id) {
return GetImageIndex(id) != -1;
}
Draw::Texture *GenerateUIAtlas(Draw::DrawContext *draw, Atlas *atlas, float dpiScale) {
static bool GenerateUIAtlasImage(Atlas *atlas, float dpiScale, Image *dest) {
Bucket bucket;
// Script fully read, now read images and rasterize the fonts.
@@ -368,7 +370,6 @@ Draw::Texture *GenerateUIAtlas(Draw::DrawContext *draw, Atlas *atlas, float dpiS
INFO_LOG(Log::G3D, " - Added %zu images to bucket in %.2f ms", bucket.data.size(), addStart.ElapsedMs());
int image_width = 512;
Image dest;
Instant bucketStart = Instant::Now();
std::vector<Data> results = bucket.Resolve(image_width, dest);
@@ -379,7 +380,7 @@ Draw::Texture *GenerateUIAtlas(Draw::DrawContext *draw, Atlas *atlas, float dpiS
std::vector<AtlasImage> genAtlasImages;
genAtlasImages.reserve(ARRAY_SIZE(imageIDs));
for (int i = 0; i < ARRAY_SIZE(imageIDs); i++) {
genAtlasImages.push_back(ToAtlasImage(resultIds[i], imageIDs[i].id, (float)dest.width(), (float)dest.height(), results));
genAtlasImages.push_back(ToAtlasImage(resultIds[i], imageIDs[i].id, (float)dest->width(), (float)dest->height(), results));
}
atlas->Clear();
@@ -389,21 +390,36 @@ Draw::Texture *GenerateUIAtlas(Draw::DrawContext *draw, Atlas *atlas, float dpiS
// For debug, write out the atlas.
if (SAVE_DEBUG_IMAGES) {
dest.SavePNG("../ui_atlas_gen.png");
dest->SavePNG("../ui_atlas_gen.png");
}
INFO_LOG(Log::G3D, "UI atlas generated in %.2f ms, size %dx%d with %zu images\n", svgStart.ElapsedMs(), dest->width(), dest->height(), genAtlasImages.size());
return true;
}
// Then, create the texture too.
static Image g_cachedUIAtlasImage;
static float g_cachedDpiScale = 0.0f;
// The caller must cache the Atlas.
Draw::Texture *GenerateUIAtlas(Draw::DrawContext *draw, Atlas *atlas, float dpiScale, bool invalidate) {
if (g_cachedUIAtlasImage.IsEmpty() || dpiScale != g_cachedDpiScale || invalidate) {
g_cachedUIAtlasImage.clear();
if (!GenerateUIAtlasImage(atlas, dpiScale, &g_cachedUIAtlasImage)) {
ERROR_LOG(Log::G3D, "Failed to generate UI atlas!");
return nullptr;
}
}
g_cachedDpiScale = dpiScale;
// Create the texture.
Draw::TextureDesc desc{};
desc.width = image_width;
desc.height = dest.height();
desc.width = g_cachedUIAtlasImage.width();
desc.height = g_cachedUIAtlasImage.height();
desc.depth = 1;
desc.mipLevels = 1;
desc.format = Draw::DataFormat::R8G8B8A8_UNORM;
desc.type = Draw::TextureType::LINEAR2D;
desc.initData.push_back((const u8 *)dest.data());
desc.initData.push_back((const u8 *)g_cachedUIAtlasImage.data());
desc.tag = "UIAtlas";
INFO_LOG(Log::G3D, "UI atlas generated in %.2f ms, size %dx%d with %zu images\n", svgStart.ElapsedMs(), desc.width, desc.height, genAtlasImages.size());
return draw->CreateTexture(desc);
}
@@ -418,7 +434,7 @@ static void LoadAtlasMetadata(Atlas &metadata, const char *filename) {
delete[] atlas_data;
}
AtlasData AtlasProvider(Draw::DrawContext *draw, AtlasChoice atlas, float dpiScale) {
AtlasData AtlasProvider(Draw::DrawContext *draw, AtlasChoice atlas, float dpiScale, bool invalidate) {
// Clamp the dpiScale to sane values. Might increase the range later.
dpiScale = std::clamp(dpiScale, 0.5f, 4.0f);
@@ -426,7 +442,7 @@ AtlasData AtlasProvider(Draw::DrawContext *draw, AtlasChoice atlas, float dpiSca
case AtlasChoice::General:
{
// Generate the atlas from scratch.
Draw::Texture *tex = GenerateUIAtlas(draw, &ui_atlas, dpiScale);
Draw::Texture *tex = GenerateUIAtlas(draw, &ui_atlas, dpiScale, invalidate);
return {&ui_atlas, tex};
}
case AtlasChoice::Font:
+1 -1
View File
@@ -5,4 +5,4 @@
const Atlas *GetFontAtlas();
Atlas *GetUIAtlas();
AtlasData AtlasProvider(Draw::DrawContext *draw, AtlasChoice atlas, float dpiScale);
AtlasData AtlasProvider(Draw::DrawContext *draw, AtlasChoice atlas, float dpiScale, bool invalidate);
@@ -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;
}
}
}
@@ -29,7 +29,6 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.PowerManager;
import android.os.Vibrator;
import android.provider.MediaStore;
import androidx.activity.OnBackPressedCallback;
@@ -104,8 +103,6 @@ public abstract class NativeActivity extends AppCompatActivity implements Sensor
private AudioFocusChangeListener audioFocusChangeListener;
private AudioManager audioManager;
private Vibrator vibrator;
// This is to avoid losing the game/menu state etc when we are just
// switched-away from or rotated etc.
private boolean shuttingDown;
@@ -484,11 +481,6 @@ public abstract class NativeActivity extends AppCompatActivity implements Sensor
}
}
vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
if (vibrator != null && !vibrator.hasVibrator()) {
vibrator = null;
}
mLocationHelper = new LocationHelper(this);
try {
mInfraredHelper = new InfraredHelper(this);
@@ -1549,7 +1541,6 @@ public abstract class NativeActivity extends AppCompatActivity implements Sensor
break;
default:
// Requires the vibrate permission, which we don't have, so disabled.
// vibrator.vibrate(milliseconds);
break;
}
} else {
@@ -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;
}
}
}
+20 -6
View File
@@ -24,15 +24,15 @@
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="4.0000002"
inkscape:cx="95.374995"
inkscape:cy="277.37499"
inkscape:zoom="2.0000001"
inkscape:cx="433.74998"
inkscape:cy="317.24998"
inkscape:window-width="3379"
inkscape:window-height="1941"
inkscape:window-x="-9"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="I_FOLDER_PINNED"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showgrid="false" /><defs
id="defs1"><inkscape:path-effect
effect="fillet_chamfer"
@@ -4278,7 +4278,21 @@
d="m 18,14 c 0,0.5523 -0.4477,1 -1,1 -0.5523,0 -1,-0.4477 -1,-1 0,-0.5523 0.4477,-1 1,-1 0.5523,0 1,0.4477 1,1 z"
fill="#1c274c"
id="path5-9"
style="fill:#ffffff;fill-opacity:1;stroke:none" /></g></g></g></g><path
style="fill:#ffffff;fill-opacity:1;stroke:none" /></g></g></g><g
style="fill:#ffffff;stroke:none;fill-opacity:1"
id="I_TOOLS"
transform="matrix(0.40312873,0,0,0.40312873,94.498775,96.901751)"><path
style="fill:#ffffff;fill-opacity:1;stroke:none"
d="m 22.053129,4.0028661 c -1.68654,-0.04273 -3.38675,0.5794244 -4.673749,1.8664231 -2.255997,2.2559978 -2.447584,5.7721538 -0.616586,8.2681518 L 14.889962,15.705185 8.4523405,9.2419259 8.4267028,7.274234 4.601561,4.9335139 2.7300104,6.8076283 l 2.3266194,3.7405377 2.0099941,0.0013 6.3632721,6.379936 -9.5820827,8.025876 c -0.7669993,0.765999 -0.7669993,2.008279 0,2.775278 l 0.6947811,0.694782 c 0.7659992,0.766999 2.0082795,0.766999 2.7752788,0 l 8.4117228,-9.189827 1.195998,1.199844 -0.685808,0.684526 c -0.384,0.382999 -0.384,1.005281 0,1.38828 l 6.082539,6.14407 c 0.383,0.384 1.005281,0.384 1.38828,0 l 2.775279,-2.776561 c 0.384,-0.382999 0.384,-1.00528 0,-1.38828 l -6.081257,-6.142788 c -0.383,-0.384 -1.005281,-0.384 -1.388281,0 l -0.749902,0.749902 -1.254964,-1.260092 1.758745,-1.921544 c 2.432997,1.372999 5.562621,1.049337 7.633619,-1.021661 1.880998,-1.880999 2.338306,-4.642505 1.379307,-6.9555028 L 22.995314,12.74916 19.523972,9.2778186 24.304118,4.4733176 C 23.583181,4.1792554 22.819739,4.0222887 22.053129,4.0028661 Z M 5.4565776,25.646195 c 0.2511248,0 0.5019995,0.09692 0.6934993,0.288424 0.3839996,0.383 0.3839996,1.003281 0,1.38828 -0.3829996,0.383 -1.0042808,0.383 -1.3882804,0 -0.3839996,-0.383999 -0.3839996,-1.00428 0,-1.38828 0.1919998,-0.1915 0.4436564,-0.288424 0.6947811,-0.288424 z"
sodipodi:nodetypes="ssccccccccccscccccccccccccccsccccssccccs"
id="path60" /></g><g
style="fill:#ffffff;fill-opacity:1"
id="I_PSP"
transform="matrix(0.02755588,0,0,0.02755588,109.17829,97.017535)"><path
d="m 394.50099,118.40641 -313.625408,0 C 19.303582,118.40641 0,177.316 0,238.889 0,300.462 19.303582,358.17138 80.875582,358.17138 l 313.625408,0 c 61.573,0 83.27701,-57.71038 83.27701,-119.28238 0,-61.573 -21.70401,-120.48259 -83.27701,-120.48259 z m -46.97508,203.70597 c 5.624,0 -4.558,10.18 -10.18,10.18 l -216.71733,0 c -5.622,0 -10.179,-4.556 -10.179,-10.18 V 154.46541 c 0,-5.623 4.557,-10.18 10.179,-10.18 l 216.71633,0 c 5.622,0 10.18,4.556 10.18,10.18 v 167.64697 z m 28.9719,-62.74938 c -0.132,0 -0.232,0.07 -0.365,0.07 -9.877,0 -17.886,-8.017 -17.886,-17.901 0,-9.868 8.009,-17.885 17.886,-17.885 0.132,0 0.233,0.07 0.365,0.07 9.713,0.209 17.536,8.079 17.536,17.815 0,9.752 -7.822,17.623 -17.536,17.831 z m 36.527,36.961 c -9.877,0 -17.886,-8.009 -17.886,-17.893 0,-9.884 8.009,-17.893 17.886,-17.893 9.892,0 17.901,8.009 17.901,17.893 -0.001,9.884 -8.01,17.893 -17.901,17.893 z m 0,-73.781 c -9.877,0 -17.886,-8.009 -17.886,-17.893 0,-9.884 8.009,-17.893 17.886,-17.893 9.892,0 17.901,8.009 17.901,17.893 -0.001,9.883 -8.01,17.893 -17.901,17.893 z m 36.89,36.89 c -9.877,0 -17.886,-8.017 -17.886,-17.901 0,-9.868 8.009,-17.885 17.886,-17.885 9.892,0 17.901,8.017 17.901,17.885 0,9.884 -8.009,17.901 -17.901,17.901 z"
style="fill:#ffffff;fill-opacity:1"
sodipodi:nodetypes="ssssssssssssssscscssscscsssssssssssssss"
id="path113" /></g></g><path
id="I_GEAR"
style="fill:#ececec;fill-opacity:1;stroke-width:0.019487"
class="st0"

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 210 KiB

+1 -1
View File
@@ -654,7 +654,7 @@ int GenerateFromScript(const char *script_file, const char *atlas_name, bool hig
// Place things on the bitmap.
printf("Resolving...\n");
std::vector<Data> results = bucket.Resolve(image_width, dest);
std::vector<Data> results = bucket.Resolve(image_width, &dest);
if (highcolor) {
printf("Writing .ZIM %ix%i RGBA8888...\n", dest.width(), dest.height());
dest.SaveZIM(image_name.c_str(), ZIM_RGBA8888 | ZIM_ZSTD_COMPRESSED);