diff --git a/Core/Util/MemStick.cpp b/Core/Util/MemStick.cpp index 55ef606a5a..b8d47f50ac 100644 --- a/Core/Util/MemStick.cpp +++ b/Core/Util/MemStick.cpp @@ -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 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 fileSuffixesToMove; std::vector 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()); diff --git a/Core/Util/MemStick.h b/Core/Util/MemStick.h index 5949d3dd8c..fad82b463d 100644 --- a/Core/Util/MemStick.h +++ b/Core/Util/MemStick.h @@ -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 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 ¤tFolder); 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. diff --git a/UI/MemStickScreen.cpp b/UI/MemStickScreen.cpp index 041804c6a6..abba10877d 100644 --- a/UI/MemStickScreen.cpp +++ b/UI/MemStickScreen.cpp @@ -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 ¶ms) { } void MemStickScreen::UseInternalStorage(UI::EventParams ¶ms) { -#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 ¶ms) { 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...) diff --git a/UI/MemStickScreen.h b/UI/MemStickScreen.h index 4457f661a0..b670049b68 100644 --- a/UI/MemStickScreen.h +++ b/UI/MemStickScreen.h @@ -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; diff --git a/UI/Settings/SystemPage.cpp b/UI/Settings/SystemPage.cpp index 6614595ef1..7b3609485b 100644 --- a/UI/Settings/SystemPage.cpp +++ b/UI/Settings/SystemPage.cpp @@ -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));