MemStickScreen: Readable folder names, say what the move is doing

- Folders show as "Internal storage / PSP" or "SD card (1234-ABCD) / PSP",
  and the app's own folder as "App private data", instead of content URIs
  and /storage/emulated/0/Android/data/... paths. Here and in the
  settings.
- The move's progress names the pass it's in (moving, finding files,
  copying, checking, deleting the originals, putting things back) with
  the count, on a line of its own above the file, across the whole width.
- A successful move shows "Done!" for three seconds before restarting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-11-06 14:53:37 +01:00
1 parent 86fcd95a4f
commit 1c0d8a2b96
5 files changed
+115 -24

No files matched your search

+31 -4
View File
@@ -72,6 +72,18 @@ static std::string LocationKey(const Path &path) {
return key;
}
bool SplitStorageLocation(const Path &path, std::string *volume, std::string *pathInVolume) {
const std::string key = LocationKey(path);
const size_t colon = key.find(':');
// A Windows drive letter ("C:/...") isn't a volume ID.
if (key.empty() || key[0] == '/' || colon == std::string::npos || colon < 2) {
return false;
}
*volume = key.substr(0, colon);
*pathInVolume = key.substr(colon + 1);
return true;
}
// True if child is strictly inside parent. Case-insensitive, like Android's shared storage.
static bool KeyIsInside(std::string_view child, std::string_view parent) {
if (parent.empty() || child.size() <= parent.size() || !startsWithNoCase(child, parent)) {
@@ -297,7 +309,17 @@ std::string MoveProgressReporter::Format() {
std::string str;
{
std::lock_guard<std::mutex> guard(mutex_);
if (max_ > 0) {
if (!phase_.empty()) {
str = phase_;
if (max_ > 0) {
str += StringFromFormat(" (%d/%d)", count_ + 1, max_);
} else if (max_ < 0) {
str += StringFromFormat(" (%d/?)", count_ + 1);
}
if (!progress_.empty()) {
str += "\n";
}
} else if (max_ > 0) {
str = StringFromFormat("(%d/%d) ", count_ + 1, max_);
} else if (max_ < 0) {
str = StringFromFormat("(%d/?) ", count_ + 1);
@@ -386,6 +408,7 @@ MoveResult *MoveDirectoryContentsSafe(Path moveSrc, Path moveDest, MoveProgressR
// Anything that fails from here on puts back what was changed.
auto fail = [&](const char *why, size_t failedFiles) {
ERROR_LOG(Log::System, "Move failed: %s. Putting things back.", why);
progressReporter.SetPhase(ms->T("Putting things back..."));
MoveResult *result = new MoveResult{ false, why, failedFiles };
result->partlyMoved = !UndoMove(moveSrc, moveDest, undo);
return result;
@@ -397,12 +420,13 @@ MoveResult *MoveDirectoryContentsSafe(Path moveSrc, Path moveDest, MoveProgressR
// First, we try the cheapest and safest way to move: Can we move files directly within the same device?
// We loop through the files/dirs in the source directory and just try to move them, it should work.
progressReporter.SetPhase(ms->T("Moving..."));
if (MoveChildrenFast(moveSrc, moveDest, progressReporter, undo)) {
INFO_LOG(Log::System, "Quick-move succeeded after %0.1f ms", moveStart.ElapsedMs());
if (beforeDeletingOriginals && !beforeDeletingOriginals()) {
return fail("Couldn't switch to the new folder", 0);
}
progressReporter.SetProgress(StringFromFormat("%s (%0.3f)", T_cstr(I18NCat::DIALOG, "Done!"), moveStart.ElapsedSeconds()));
progressReporter.SetPhase(T(I18NCat::DIALOG, "Done!"));
return new MoveResult{ true, "" };
} else {
INFO_LOG(Log::System, "Quick move denied after %0.1f ms, falling back to slow move.", moveStart.ElapsedMs());
@@ -416,14 +440,15 @@ MoveResult *MoveDirectoryContentsSafe(Path moveSrc, Path moveDest, MoveProgressR
std::vector<FileSuffix> fileSuffixesToMove;
std::vector<std::string> directorySuffixesToCreate;
progressReporter.SetPhase(ms->T("Finding files..."));
// NOTE: It's correct to pass moveSrc twice here, it's to keep the root in the recursion.
if (!ListFileSuffixesRecursively(moveSrc, moveSrc, directorySuffixesToCreate, fileSuffixesToMove, progressReporter)) {
progressReporter.SetProgress(ms->T("Failed to read old directory"));
return fail("Failed to read old directory", 0);
}
// Better not interrupt the app while this is happening!
progressReporter.SetPhase(ms->T("Copying..."));
// Create all the necessary directories.
for (size_t i = 0; i < directorySuffixesToCreate.size(); i++) {
const auto &dirSuffix = directorySuffixesToCreate[i];
@@ -463,9 +488,10 @@ MoveResult *MoveDirectoryContentsSafe(Path moveSrc, Path moveDest, MoveProgressR
// After the whole move, verify that all the files arrived correctly.
// If there's a single error, we do not delete the source data.
progressReporter.SetPhase(ms->T("Checking..."));
for (size_t i = 0; i < fileSuffixesToMove.size(); i++) {
const auto &fileSuffix = fileSuffixesToMove[i];
progressReporter.SetProgress(ms->T("Checking..."), i, fileSuffixesToMove.size());
progressReporter.SetProgress(fileSuffix.suffix, i, fileSuffixesToMove.size());
Path to = moveDest / fileSuffix.suffix;
@@ -492,6 +518,7 @@ MoveResult *MoveDirectoryContentsSafe(Path moveSrc, Path moveDest, MoveProgressR
// Delete the originals: the files, then the folders. On content URIs DeleteDir would take the files
// along anyway, but on native paths it only removes empty folders.
progressReporter.SetPhase(ms->T("Deleting from old folder..."));
for (size_t i = 0; i < fileSuffixesToMove.size(); i++) {
const Path from = moveSrc / fileSuffixesToMove[i].suffix;
progressReporter.SetProgress(fileSuffixesToMove[i].suffix, i, fileSuffixesToMove.size());
+16 -2
View File
@@ -16,14 +16,23 @@ public:
count_ = (int)count;
max_ = (int)maxVal;
}
// What the move is doing (like copying or checking), shown above the item it's on.
void SetPhase(std::string_view phase) {
std::lock_guard<std::mutex> guard(mutex_);
phase_ = phase;
progress_.clear();
count_ = 0;
max_ = 0;
}
std::string Format();
private:
std::string phase_;
std::string progress_;
std::mutex mutex_;
int count_;
int max_;
int count_ = 0;
int max_ = 0;
};
struct MoveResult {
@@ -53,6 +62,11 @@ enum class MemstickFolderCheck {
MemstickFolderCheck CheckNewMemstickFolder(const Path &newFolder, const Path &currentFolder);
bool FolderSeemsToBeUsed(const Path &newMemstickFolder);
// For showing a location to the user: splits Android shared storage, as a content URI or a native path, into
// its volume ("primary" for internal storage, otherwise an ID like "1234-ABCD") and the path inside it.
// False for anything else.
bool SplitStorageLocation(const Path &path, std::string *volume, std::string *pathInVolume);
// The checks SwitchMemstickFolderTo() starts with, to run before moving any data.
bool CanSwitchMemstickFolderTo(const Path &newMemstickFolder);
// Makes the next start use newMemstickFolder. SwitchMemstickFolderTo() does this and more.
+61 -17
View File
@@ -41,6 +41,7 @@
#include "Common/File/DiskFree.h"
#include "Common/Thread/ThreadManager.h"
#include "Common/TimeUtil.h"
#include "Common/UI/Notice.h"
#include "Core/Config.h"
@@ -60,6 +61,35 @@ static std::string FormatSpaceString(int64_t space) {
}
}
// The memory stick folder in the app's own storage.
static Path PrivateMemstickFolder() {
#if PPSSPP_PLATFORM(UWP)
return g_Config.internalDataDirectory;
#else
return Path(g_extFilesDir);
#endif
}
std::string GetMemstickFolderDisplayName(const Path &folder) {
auto ms = GetI18NCategory(I18NCat::MEMSTICK);
if (!folder.empty() && folder == PrivateMemstickFolder()) {
return std::string(ms->T("App private data"));
}
std::string volume;
std::string inVolume;
if (!SplitStorageLocation(folder, &volume, &inVolume)) {
return folder.ToVisualString();
}
std::string name;
if (volume == "primary") {
name = ms->T("Internal storage");
} else {
// Could also be a USB drive, which is rare enough. The ID tells them apart.
name = ApplySafeSubstitutions(ms->T("SD card (%1)"), volume);
}
return inVolume.empty() ? name : name + " / " + ReplaceAll(inVolume, "/", " / ");
}
MemStickScreen::MemStickScreen(bool initialSetup)
: UITwoPaneBaseDialogScreen(Path(),
TwoPaneFlags::SettingsToTheRight | TwoPaneFlags::ContentsCanScroll | (initialSetup ? TwoPaneFlags::NoBackButton : TwoPaneFlags::Default)),
@@ -231,7 +261,7 @@ void MemStickScreen::CreateContentViews(UI::ViewGroup *parent) {
privateFolder->SetHintText(GetExplanationString(CHOICE_PRIVATE_DIRECTORY));
if ((choice_ == CHOICE_BROWSE_FOLDER || choice_ == CHOICE_SET_MANUAL) && !g_Config.memStickDirectory.empty()) {
groups->Add(new TextView(StringFromFormat("%s: %s", ms->T_cstr("Current"), g_Config.memStickDirectory.ToVisualString().c_str()), ALIGN_LEFT, false));
groups->Add(new TextView(StringFromFormat("%s: %s", ms->T_cstr("Current"), GetMemstickFolderDisplayName(g_Config.memStickDirectory).c_str()), ALIGN_LEFT | FLAG_WRAP_TEXT, false));
}
INFO_LOG(Log::System, "MemStickScreen: initialSetup=%d", (int)initialSetup_);
@@ -362,11 +392,7 @@ void MemStickScreen::SetFolderManually(UI::EventParams &params) {
}
void MemStickScreen::UseInternalStorage(UI::EventParams &params) {
#if PPSSPP_PLATFORM(UWP)
Path pendingMemStickFolder = g_Config.internalDataDirectory;
#else
Path pendingMemStickFolder = Path(g_extFilesDir);
#endif
Path pendingMemStickFolder = PrivateMemstickFolder();
if (initialSetup_) {
// There's not gonna be any files here in this case since it's a fresh install.
@@ -502,22 +528,24 @@ void ConfirmMemstickMoveScreen::CreateViews() {
auto sy = GetI18NCategory(I18NCat::SYSTEM);
auto ms = GetI18NCategory(I18NCat::MEMSTICK);
root_ = new LinearLayout(ORIENT_HORIZONTAL);
// The columns, and below them the progress of a move, across the whole width so file names fit.
root_ = new LinearLayout(ORIENT_VERTICAL);
ViewGroup *columns = root_->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
const Path &oldMemstickFolder = g_Config.memStickDirectory;
Spacer *spacerColumn = new Spacer(new LinearLayoutParams(20.0, FILL_PARENT, 0.0f));
ViewGroup *leftColumn = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(1.0));
ViewGroup *rightColumn = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(1.0));
root_->Add(spacerColumn);
root_->Add(leftColumn);
root_->Add(rightColumn);
columns->Add(spacerColumn);
columns->Add(leftColumn);
columns->Add(rightColumn);
leftColumn->Add(new TextView(ms->T("Selected PSP Data Folder"), ALIGN_LEFT, false));
if (!initialSetup_) {
leftColumn->Add(new NoticeView(NoticeLevel::INFO, ms->T("PPSSPP will restart after the change"), ""));
}
leftColumn->Add(new TextView(newMemstickFolder_.ToVisualString(), ALIGN_LEFT, false));
leftColumn->Add(new TextView(GetMemstickFolderDisplayName(newMemstickFolder_), ALIGN_LEFT | FLAG_WRAP_TEXT, false));
// TODO: Add spinner
newFreeSpaceView_ = leftColumn->Add(new TextView(ApplySafeSubstitutions("%1: ...", ms->T("Free space")), ALIGN_LEFT, false));
@@ -555,11 +583,20 @@ void ConfirmMemstickMoveScreen::CreateViews() {
if (!oldMemstickFolder.empty()) {
rightColumn->Add(new TextView(std::string(ms->T("Current")) + ":", ALIGN_LEFT, false));
rightColumn->Add(new TextView(oldMemstickFolder.ToVisualString(), ALIGN_LEFT, false));
rightColumn->Add(new TextView(GetMemstickFolderDisplayName(oldMemstickFolder), ALIGN_LEFT | FLAG_WRAP_TEXT, false));
}
if (restartTime_ > 0.0) {
auto di = GetI18NCategory(I18NCat::DIALOG);
leftColumn->Add(new NoticeView(NoticeLevel::SUCCESS, di->T("Done!"), ""));
progressView_ = nullptr;
return;
}
if (moveDataTask_) {
progressView_ = leftColumn->Add(new TextView(progressReporter_.Format()));
// What it's doing and the count on the first line, the file it's on below.
progressView_ = root_->Add(new TextView(progressReporter_.Format(), ALIGN_LEFT | FLAG_WRAP_TEXT, false,
new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(20, 10))));
} else {
progressView_ = nullptr;
}
@@ -589,7 +626,7 @@ void ConfirmMemstickMoveScreen::OnMoveDataClick(UI::EventParams &params) {
bool ConfirmMemstickMoveScreen::key(const KeyInput &key) {
// The Back choice is hidden during a move, but the back key would still close the screen, blocking the
// UI thread in the destructor until the move is done, and skipping FinishFolderMove().
if (moveDataTask_ && UI::IsEscapeKey(key)) {
if ((moveDataTask_ || restartTime_ > 0.0) && UI::IsEscapeKey(key)) {
return true;
}
return UIBaseDialogScreen::key(key);
@@ -608,6 +645,12 @@ void ConfirmMemstickMoveScreen::update() {
UIBaseDialogScreen::update();
auto ms = GetI18NCategory(I18NCat::MEMSTICK);
if (restartTime_ > 0.0 && time_now_d() >= restartTime_) {
restartTime_ = 0.0;
System_RestartApp("");
return;
}
if (moveDataTask_) {
if (progressView_) {
progressView_->SetText(progressReporter_.Format());
@@ -727,9 +770,10 @@ void ConfirmMemstickMoveScreen::FinishFolderMove() {
PostLoadConfig();
if (!initialSetup_) {
// We restart the app here, to get the new settings.
INFO_LOG(Log::System, "Not initial setup. Restarting!");
System_RestartApp("");
// We restart the app to get the new settings, once the user has seen that it worked.
INFO_LOG(Log::System, "Not initial setup. Restarting in a moment.");
restartTime_ = time_now_d() + 3.0;
RecreateViews();
} else {
// This is initial setup, we now switch to the main screen, if we were successful
// (which we better have been...)
+6
View File
@@ -35,6 +35,10 @@
class NoticeView;
// A memory stick folder as shown to the user: "Internal storage / PSP" rather than a content URI or a
// /storage/emulated/0 path, and the app's own folder by name.
std::string GetMemstickFolderDisplayName(const Path &folder);
// MemStickScreen - let's you configure your memory stick directory.
// Currently only useful for Android.
class MemStickScreen : public UITwoPaneBaseDialogScreen {
@@ -125,6 +129,8 @@ private:
bool initialSetup_;
// The data is already in the new folder.
bool dataMoved_ = false;
// Set when everything worked: restart then (time_now_d()), after showing that.
double restartTime_ = 0.0;
MoveProgressReporter progressReporter_;
UI::TextView *progressView_ = nullptr;
+1 -1
View File
@@ -292,7 +292,7 @@ void BuildSystemSettings(SettingsPage *page, SystemPageState *state) {
auto addMemstickPath = [host, &page]() {
page->Custom("Memory Stick folder", [host](UI::ViewGroup *parent) -> UI::View * {
Choice *memstickPath = parent->Add(new ChoiceWithCallbackValueDisplay(T(I18NCat::SYSTEM, "Memory Stick folder"), []() {
return g_Config.memStickDirectory.ToVisualString();
return GetMemstickFolderDisplayName(g_Config.memStickDirectory);
}));
memstickPath->OnClick.Add([host](UI::EventParams &e) {
host->PageScreenManager()->push(new MemStickScreen(false));