Frame dumps: Offer Share, Save or Continue where folders can't be shown

On Android, Save picks a place with the system's save picker, through
a new BROWSE_FOR_FILE_SAVE implementation (ACTION_CREATE_DOCUMENT).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-11-11 12:43:03 +01:00
1 parent 9cc2d2d378
commit ad58640cdb
4 files changed
+122 -3

No files matched your search

+91 -2
View File
@@ -33,6 +33,7 @@
#include "Common/Audio/AudioBackend.h"
#include "Common/File/AndroidStorage.h"
#include "Common/File/DirListing.h"
#include "Common/Data/Text/I18n.h"
#include "Common/Data/Text/Parsers.h"
#include "Common/Data/Encoding/Utf8.h"
@@ -134,13 +135,101 @@ static void ShareFrameDump(const Path &dumpPath) {
}
}
static bool CopyFileContents(const Path &src, const Path &dest) {
FILE *in = File::OpenCFile(src, "rb");
if (!in) {
return false;
}
// Content URIs can be written but not created this way, so this works on a document the picker made.
FILE *out = File::OpenCFile(dest, "wb");
if (!out) {
fclose(in);
return false;
}
bool success = true;
char buffer[65536];
size_t count;
while ((count = fread(buffer, 1, sizeof(buffer), in)) > 0) {
if (fwrite(buffer, 1, count, out) != count) {
success = false;
break;
}
}
if (ferror(in)) {
success = false;
}
fclose(in);
if (fclose(out) != 0) {
success = false;
}
return success;
}
static bool CanSaveFileAs() {
#if PPSSPP_PLATFORM(ANDROID)
return System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER);
#else
return false;
#endif
}
// Where a dump can't be shown in its folder: share it, save it somewhere the user picks, or neither.
class FrameDumpPopupScreen : public UI::PopupScreen {
public:
FrameDumpPopupScreen(const Path &dumpPath) : PopupScreen(dumpPath.GetFilename()), dumpPath_(dumpPath) {}
const char *tag() const override { return "FrameDumpPopup"; }
protected:
bool ShowButtons() const override { return false; }
void CreatePopupContents(UI::ViewGroup *parent) override {
using namespace UI;
auto di = GetI18NCategory(I18NCat::DIALOG);
auto pa = GetI18NCategory(I18NCat::PAUSE);
if (CanShareFiles()) {
parent->Add(new Choice(di->T("Share"), ImageID("I_SHARE")))->OnClick.Add([this](UI::EventParams &e) {
ShareFrameDump(dumpPath_);
TriggerFinish(DR_OK);
});
}
if (CanSaveFileAs()) {
parent->Add(new Choice(di->T("Save"), ImageID("I_FILE_SAVE")))->OnClick.Add([this, di](UI::EventParams &e) {
// Stays open until the picker answers, since the answer is dropped once the screen is gone.
System_BrowseForFileSave(GetRequesterToken(), di->T("Save"), dumpPath_.GetFilename(), BrowseFileType::ANY, [this](std::string_view value, int) {
auto sy = GetI18NCategory(I18NCat::SYSTEM);
const Path dest(value);
if (CopyFileContents(dumpPath_, dest)) {
// A picked document's URI ends in an ID, so ask for its name.
File::FileInfo info;
std::string name = dumpPath_.GetFilename();
if (File::GetFileInfo(dest, &info) && !info.name.empty()) {
name = info.name;
}
g_OSD.Show(OSDType::MESSAGE_SUCCESS, name);
} else {
g_OSD.Show(OSDType::MESSAGE_ERROR, sy->T("Failed"));
}
TriggerFinish(DR_OK);
});
});
}
parent->Add(new Choice(pa->T("Continue")))->OnClick.Add([this](UI::EventParams &e) {
TriggerFinish(DR_CANCEL);
});
}
private:
Path dumpPath_;
};
extern ScreenManager *g_screenManager;
void OnFrameDumpCreated(const Path &dumpPath, bool openNow) {
NOTICE_LOG(Log::System, "Frame dump created at '%s'", dumpPath.c_str());
std::function<void()> open;
if (System_GetPropertyBool(SYSPROP_CAN_SHOW_FILE)) {
open = [dumpPath]() { System_ShowFileInFolder(dumpPath); };
} else if (CanShareFiles()) {
open = [dumpPath]() { ShareFrameDump(dumpPath); };
} else if (CanShareFiles() || CanSaveFileAs()) {
open = [dumpPath]() { g_screenManager->push(new FrameDumpPopupScreen(dumpPath)); };
}
g_OSD.Show(OSDType::MESSAGE_SUCCESS, GetFriendlyPath(dumpPath), 7.0f, "framedump");
if (!open) {
+4
View File
@@ -1197,6 +1197,10 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string
}
return true;
}
case SystemRequestType::BROWSE_FOR_FILE_SAVE:
// The type isn't passed on, the picker keeps the suggested name's extension.
PushCommand("browse_file_save", StringFromFormat("%d:@:%s", requestId, param2.c_str()), requestId);
return true;
case SystemRequestType::BROWSE_FOR_FOLDER:
PushCommand("browse_folder", StringFromFormat("%d", requestId), requestId);
return true;
@@ -53,8 +53,10 @@ public class DocumentResultProxyActivity extends AppCompatActivity {
if (uri != null) {
Log.i(TAG, "DocumentResultProxy: Selected URI: " + uri);
final boolean isTree = pickerIntent != null && Intent.ACTION_OPEN_DOCUMENT_TREE.equals(pickerIntent.getAction());
// A created document gets written to after the picker is gone.
final boolean isCreate = pickerIntent != null && Intent.ACTION_CREATE_DOCUMENT.equals(pickerIntent.getAction());
try {
if (isTree) {
if (isTree || isCreate) {
getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} else {
getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
@@ -1808,6 +1808,30 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
}
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
Intent proxy = new Intent(this, DocumentResultProxyActivity.class);
proxy.putExtra("picker_intent", intent);
proxy.putExtra("request_id", requestId);
startActivity(proxy);
return true;
} catch (Exception e) {
NativeApp.reportException(e, params);
Log.e(TAG, e.toString());
return false;
}
} else if (command.equals("browse_file_save")) {
try {
// requestId:@:suggested filename
String[] param = params.split(":@:", 2);
int requestId = Integer.parseInt(param[0]);
Log.i(TAG, "browse_file_save request ID: " + requestId);
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
// The generic type keeps the extension of the suggested name.
intent.setType("application/octet-stream");
if (param.length > 1) {
intent.putExtra(Intent.EXTRA_TITLE, param[1]);
}
Intent proxy = new Intent(this, DocumentResultProxyActivity.class);
proxy.putExtra("picker_intent", intent);
proxy.putExtra("request_id", requestId);