mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-13 17:49:29 +01:00
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:
1 parent
9cc2d2d378
commit
ad58640cdb
4 files changed
+122
-3
No files matched your search
+91
-2
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user