ppsspp/UI/MemStickScreen.cpp
Henrik Rydgård 1304d04161 Fix a particular type of race condition in file dialog requests
It seems to be possible for a user to back out of a screen before
receiving the "dialog completed" callback on Android, in which case
things pointed to by the callback might be gone.

In this case, it's better to simply not call the callback, rather than
crashing.

This is accomplished by assigning "Tokens" to screens that cause
requests, and in ~Screen, invalidate any pending requests belonging to
that token.
2024-01-18 12:25:55 +01:00

810 lines
29 KiB
C++

// Copyright (c) 2013- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include "ppsspp_config.h"
#include "android/jni/app-android.h"
#include "Common/Log.h"
#include "Common/UI/UI.h"
#include "Common/UI/View.h"
#include "Common/UI/ViewGroup.h"
#include "Common/StringUtils.h"
#include "Common/System/System.h"
#include "Common/System/Request.h"
#include "Common/System/NativeApp.h"
#include "Common/System/Display.h"
#include "Common/System/OSD.h"
#include "Common/Data/Text/I18n.h"
#include "Common/Data/Text/Parsers.h"
#include "Common/File/AndroidStorage.h"
#include "Common/File/FileUtil.h"
#include "Common/File/Path.h"
#include "Common/File/DiskFree.h"
#include "Common/Thread/ThreadManager.h"
#include "Core/Config.h"
#include "Core/Reporting.h"
#include "Core/System.h"
#include "Core/Util/GameManager.h"
#include "UI/MemStickScreen.h"
#include "UI/MainScreen.h"
#include "UI/MiscScreens.h"
#include "UI/OnScreenDisplay.h"
static bool FolderSeemsToBeUsed(const Path &newMemstickFolder) {
// Inspect the potential new folder, quickly.
if (File::Exists(newMemstickFolder / "PSP/SAVEDATA") || File::Exists(newMemstickFolder / "SAVEDATA")) {
// Does seem likely. We could add more criteria like checking for actual savegames or something.
return true;
} else {
return false;
}
}
static bool SwitchMemstickFolderTo(Path newMemstickFolder) {
// Doesn't already exist, create.
// Should this ever happen?
if (newMemstickFolder.Type() == PathType::NATIVE) {
if (!File::Exists(newMemstickFolder)) {
File::CreateFullPath(newMemstickFolder);
}
Path testWriteFile = newMemstickFolder / ".write_verify_file";
if (!File::WriteDataToFile(true, "1", 1, testWriteFile)) {
return false;
}
File::Delete(testWriteFile);
} else {
// TODO: Do the same but with scoped storage? Not really necessary, right? If it came from a browse
// for folder, we can assume it exists and is writable, barring wacky race conditions like the user
// being connected by USB and deleting it.
}
Path memStickDirFile = g_Config.internalDataDirectory / "memstick_dir.txt";
#if PPSSPP_PLATFORM(UWP)
File::Delete(memStickDirFile);
if (newMemstickFolder != g_Config.internalDataDirectory) {
#endif
std::string str = newMemstickFolder.ToString();
if (!File::WriteDataToFile(true, str.c_str(), (unsigned int)str.size(), memStickDirFile)) {
ERROR_LOG(SYSTEM, "Failed to write memstick path '%s' to '%s'", newMemstickFolder.c_str(), memStickDirFile.c_str());
// Not sure what to do if this file can't be written. Disk full?
}
#if PPSSPP_PLATFORM(UWP)
}
#endif
// Save so the settings, at least, are transferred.
g_Config.memStickDirectory = newMemstickFolder;
g_Config.SetSearchPath(GetSysDirectory(DIRECTORY_SYSTEM));
g_Config.UpdateIniLocation();
return true;
}
static std::string FormatSpaceString(int64_t space) {
if (space >= 0) {
char buffer[50];
NiceSizeFormat(space, buffer, sizeof(buffer));
return buffer;
} else {
return "N/A";
}
}
MemStickScreen::MemStickScreen(bool initialSetup)
: initialSetup_(initialSetup) {
#if PPSSPP_PLATFORM(ANDROID)
// Let's only offer the browse-for-folder choice on Android 10 or later.
// Earlier versions often don't really have working folder browsers.
storageBrowserWorking_ = System_GetPropertyInt(SYSPROP_SYSTEMVERSION) >= 29;
#else
// For testing UI only
storageBrowserWorking_ = true;
#endif
if (initialSetup_) {
// Preselect current choice.
if (System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE)) {
choice_ = CHOICE_BROWSE_FOLDER;
} else {
WARN_LOG_REPORT(SYSTEM, "Scoped storage not enabled - shouldn't be in MemStickScreen at initial setup");
choice_ = CHOICE_STORAGE_ROOT;
// Shouldn't really be here in initial setup.
}
} else {
// Detect the current choice, so it's preselected in the UI.
#if PPSSPP_PLATFORM(UWP)
if (g_Config.memStickDirectory == g_Config.internalDataDirectory) {
#else
if (g_Config.memStickDirectory == Path(g_extFilesDir)) {
#endif
choice_ = CHOICE_PRIVATE_DIRECTORY;
} else if (g_Config.memStickDirectory == Path(g_externalDir)) {
choice_ = CHOICE_STORAGE_ROOT;
} else if (storageBrowserWorking_) {
choice_ = CHOICE_BROWSE_FOLDER;
} else {
choice_ = CHOICE_SET_MANUAL;
}
}
}
static void AddExplanation(UI::ViewGroup *viewGroup, MemStickScreen::Choice choice, UI::View *extraView = nullptr) {
auto iz = GetI18NCategory(I18NCat::MEMSTICK);
using namespace UI;
int flags = FLAG_WRAP_TEXT;
UI::ViewGroup *holder = new UI::LinearLayout(ORIENT_VERTICAL);
UI::ViewGroup *indentHolder = new UI::LinearLayout(ORIENT_HORIZONTAL);
indentHolder->Add(new Spacer(20.0));
indentHolder->Add(holder);
viewGroup->Add(indentHolder);
if (extraView) {
holder->Add(extraView);
}
switch (choice) {
case MemStickScreen::CHOICE_STORAGE_ROOT:
// Old school choice
holder->Add(new TextView(iz->T("DataWillStay", "Data will stay even if you uninstall PPSSPP"), flags, false))->SetBullet(true);
holder->Add(new TextView(iz->T("DataCanBeShared", "Data can be shared between PPSSPP regular/Gold"), flags, false))->SetBullet(true);
holder->Add(new TextView(iz->T("EasyUSBAccess", "Easy USB access"), flags, false))->SetBullet(true);
break;
case MemStickScreen::CHOICE_BROWSE_FOLDER:
holder->Add(new TextView(iz->T("DataWillStay", "Data will stay even if you uninstall PPSSPP"), flags, false))->SetBullet(true);
holder->Add(new TextView(iz->T("DataCanBeShared", "Data can be shared between PPSSPP regular/Gold"), flags, false))->SetBullet(true);
#if !PPSSPP_PLATFORM(UWP)
holder->Add(new TextView(iz->T("EasyUSBAccess", "Easy USB access"), flags, false))->SetBullet(true);
#endif
break;
case MemStickScreen::CHOICE_PRIVATE_DIRECTORY:
// Consider https://www.compart.com/en/unicode/U+26A0 (unicode warning sign?)? or a graphic?
holder->Add(new TextView(iz->T("DataWillBeLostOnUninstall", "Warning! Data will be lost when you uninstall PPSSPP!"), flags, false))->SetBullet(true);
holder->Add(new TextView(iz->T("DataCannotBeShared", "Data CANNOT be shared between PPSSPP regular/Gold!"), flags, false))->SetBullet(true);
#if !PPSSPP_PLATFORM(UWP)
#if GOLD
holder->Add(new TextView(iz->T("USBAccessThroughGold", "USB access through Android/data/org.ppsspp.ppssppgold/files"), flags, false))->SetBullet(true);
#else
holder->Add(new TextView(iz->T("USBAccessThrough", "USB access through Android/data/org.ppsspp.ppsspp/files"), flags, false))->SetBullet(true);
#endif
#endif
break;
case MemStickScreen::CHOICE_SET_MANUAL:
default:
holder->Add(new TextView(iz->T("EasyUSBAccess", "Easy USB access"), flags, false))->SetBullet(true);
// What more?
// Should we have a special text here? It'll popup a text window for editing.
break;
}
}
void MemStickScreen::CreateViews() {
using namespace UI;
auto di = GetI18NCategory(I18NCat::DIALOG);
auto iz = GetI18NCategory(I18NCat::MEMSTICK);
Margins actionMenuMargins(15, 0, 15, 0);
root_ = new LinearLayout(ORIENT_HORIZONTAL);
Spacer *spacerColumn = new Spacer(new LinearLayoutParams(20.0, FILL_PARENT, 0.0f));
ScrollView *mainColumnScroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(1.0));
ViewGroup *mainColumn = new LinearLayoutList(ORIENT_VERTICAL);
mainColumnScroll->Add(mainColumn);
root_->Add(spacerColumn);
root_->Add(mainColumnScroll);
if (initialSetup_) {
mainColumn->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 12.0f, 0.0f)));
mainColumn->Add(new TextView(iz->T("Welcome to PPSSPP!"), ALIGN_LEFT, false));
}
mainColumn->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 18.0f, 0.0f)));
mainColumn->Add(new TextView(iz->T("MemoryStickDescription", "Choose where to keep PSP data (Memory Stick)"), ALIGN_LEFT, false));
mainColumn->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 18.0f, 0.0f)));
ViewGroup *subColumns = new LinearLayoutList(ORIENT_HORIZONTAL);
mainColumn->Add(subColumns);
ViewGroup *leftColumn = new LinearLayoutList(ORIENT_VERTICAL, new LinearLayoutParams(1.0));
subColumns->Add(leftColumn);
ViewGroup *rightColumnItems = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(220, FILL_PARENT, actionMenuMargins));
subColumns->Add(rightColumnItems);
// For legacy Android systems, so you can switch back to the old ways if you move to SD or something.
// Trying to avoid needing a scroll view, so only showing the explanation for one option at a time.
#if !PPSSPP_PLATFORM(UWP)
if (!System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE)) {
leftColumn->Add(new RadioButton(&choice_, CHOICE_STORAGE_ROOT, iz->T("Use PSP folder at root of storage")))->OnClick.Handle(this, &MemStickScreen::OnChoiceClick);
if (choice_ == CHOICE_STORAGE_ROOT) {
AddExplanation(leftColumn, (MemStickScreen::Choice)choice_);
}
}
#endif
if (storageBrowserWorking_) {
leftColumn->Add(new RadioButton(&choice_, CHOICE_BROWSE_FOLDER, iz->T("Create or Choose a PSP folder")))->OnClick.Handle(this, &MemStickScreen::OnChoiceClick);
// TODO: Show current folder here if we have one set.
} else {
leftColumn->Add(new RadioButton(&choice_, CHOICE_SET_MANUAL, iz->T("Manually specify PSP folder")))->OnClick.Handle(this, &MemStickScreen::OnChoiceClick);
leftColumn->Add(new TextView(iz->T("DataWillStay", "Data will stay even if you uninstall PPSSPP.")))->SetBullet(true);
leftColumn->Add(new TextView(iz->T("DataCanBeShared", "Data can be shared between PPSSPP regular/Gold.")))->SetBullet(true);
// TODO: Show current folder here if we have one set.
}
errorNoticeView_ = leftColumn->Add(new NoticeView(NoticeLevel::WARN, iz->T("Cancelled - try again"), ""));
errorNoticeView_->SetVisibility(UI::V_GONE);
if (choice_ == CHOICE_BROWSE_FOLDER || choice_ == CHOICE_SET_MANUAL) {
UI::View *extraView = nullptr;
if (!g_Config.memStickDirectory.empty()) {
extraView = new TextView(StringFromFormat(" %s: %s", iz->T("Current"), g_Config.memStickDirectory.ToVisualString().c_str()), ALIGN_LEFT, false);
}
AddExplanation(leftColumn, (MemStickScreen::Choice)choice_, extraView);
}
std::string privateString = iz->T("Use App Private Data");
if (initialSetup_) {
privateString = StringFromFormat("%s (%s)", iz->T("Skip for now"), privateString.c_str());
}
leftColumn->Add(new RadioButton(&choice_, CHOICE_PRIVATE_DIRECTORY, privateString))->OnClick.Handle(this, &MemStickScreen::OnChoiceClick);
if (choice_ == CHOICE_PRIVATE_DIRECTORY) {
AddExplanation(leftColumn, (MemStickScreen::Choice)choice_);
}
leftColumn->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 12.0f, 0.0f)));
const char *confirmButtonText = nullptr;
ImageID confirmButtonImage = ImageID::invalid();
switch (choice_) {
case CHOICE_BROWSE_FOLDER:
confirmButtonText = di->T("OK");
confirmButtonImage = ImageID("I_FOLDER_OPEN");
break;
case CHOICE_PRIVATE_DIRECTORY:
if (initialSetup_) {
confirmButtonText = di->T("Skip");
confirmButtonImage = ImageID("I_WARNING");
} else {
confirmButtonText = di->T("OK");
}
break;
case CHOICE_STORAGE_ROOT:
case CHOICE_SET_MANUAL:
default:
confirmButtonText = di->T("OK");
break;
}
rightColumnItems->Add(new UI::Choice(confirmButtonText, confirmButtonImage))->OnClick.Handle<MemStickScreen>(this, &MemStickScreen::OnConfirmClick);
rightColumnItems->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 12.0f, 0.0f)));
if (!initialSetup_) {
rightColumnItems->Add(new UI::Choice(di->T("Back")))->OnClick.Handle<UIScreen>(this, &UIScreen::OnBack);
}
if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) != DEVICE_TYPE_TV) {
rightColumnItems->Add(new UI::Choice(iz->T("WhatsThis", "What's this?")))->OnClick.Handle<MemStickScreen>(this, &MemStickScreen::OnHelp);
}
INFO_LOG(SYSTEM, "MemStickScreen: initialSetup=%d", (int)initialSetup_);
}
UI::EventReturn MemStickScreen::OnHelp(UI::EventParams &params) {
// I'm letting the old redirect handle this one, as the target is within /docs on the website,
// and that structure may change a bit.
System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.ppsspp.org/guide_storage.html");
return UI::EVENT_DONE;
}
UI::EventReturn MemStickScreen::OnChoiceClick(UI::EventParams &params) {
// Change the confirm button to match the choice,
// and change the text that we show.
RecreateViews();
return UI::EVENT_DONE;
}
UI::EventReturn MemStickScreen::OnConfirmClick(UI::EventParams &params) {
switch (choice_) {
case CHOICE_SET_MANUAL:
return SetFolderManually(params);
case CHOICE_STORAGE_ROOT:
return UseStorageRoot(params);
case CHOICE_PRIVATE_DIRECTORY:
return UseInternalStorage(params);
case CHOICE_BROWSE_FOLDER:
return Browse(params);
}
return UI::EVENT_DONE;
}
UI::EventReturn MemStickScreen::SetFolderManually(UI::EventParams &params) {
// The old way, from before scoped storage.
#if PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(SWITCH)
auto sy = GetI18NCategory(I18NCat::SYSTEM);
System_InputBoxGetString(GetRequesterToken(), sy->T("Memory Stick Folder"), g_Config.memStickDirectory.ToString(), [&](const std::string &value, int) {
auto sy = GetI18NCategory(I18NCat::SYSTEM);
auto di = GetI18NCategory(I18NCat::DIALOG);
std::string newPath = value;
size_t pos = newPath.find_last_not_of("/");
// Gotta have at least something but a /, and also needs to start with a /.
if (newPath.empty() || pos == newPath.npos || newPath[0] != '/') {
settingInfo_->Show(sy->T("ChangingMemstickPathInvalid", "That path couldn't be used to save Memory Stick files."), nullptr);
return;
}
if (pos != newPath.size() - 1) {
newPath = newPath.substr(0, pos + 1);
}
if (newPath.empty()) {
// Reuse below message instead of adding yet another string.
System_Toast(sy->T("Path does not exist!"));
return;
}
Path pendingMemStickFolder(newPath);
if (!File::Exists(pendingMemStickFolder)) {
// Try to fix the path string, apparently some users got used to leaving out the /.
if (newPath[0] != '/') {
newPath = "/" + newPath;
}
pendingMemStickFolder = Path(newPath);
}
if (!File::Exists(pendingMemStickFolder) && pendingMemStickFolder.Type() == PathType::NATIVE) {
// Still no path? Try to automatically fix the case.
std::string oldNewPath = newPath;
FixPathCase(Path(""), newPath, FixPathCaseBehavior::FPC_FILE_MUST_EXIST);
if (oldNewPath != newPath) {
NOTICE_LOG(IO, "Fixed path case: %s -> %s", oldNewPath.c_str(), newPath.c_str());
pendingMemStickFolder = Path(newPath);
} else {
NOTICE_LOG(IO, "Failed to fix case of path %s (result: %s)", newPath.c_str(), oldNewPath.c_str());
}
}
if (pendingMemStickFolder == g_Config.memStickDirectory) {
// Same directory as before - all good. Nothing to do.
TriggerFinish(DialogResult::DR_OK);
return;
}
if (!File::Exists(pendingMemStickFolder)) {
System_Toast(sy->T("Path does not exist!"));
return;
}
screenManager()->push(new ConfirmMemstickMoveScreen(pendingMemStickFolder, false));
});
#endif
return UI::EVENT_DONE;
}
UI::EventReturn MemStickScreen::UseInternalStorage(UI::EventParams &params) {
#if PPSSPP_PLATFORM(UWP)
Path pendingMemStickFolder = g_Config.internalDataDirectory;
#else
Path pendingMemStickFolder = Path(g_extFilesDir);
#endif
if (initialSetup_) {
// There's not gonna be any files here in this case since it's a fresh install.
// Let's just accept it and move on. No need to move files either.
if (SwitchMemstickFolderTo(pendingMemStickFolder)) {
TriggerFinish(DialogResult::DR_OK);
} else {
// This can't really happen?? Not worth making an error message.
ERROR_LOG_REPORT(SYSTEM, "Could not switch memstick path in setup (internal)");
}
// Don't have a confirmation dialog that would otherwise do it for us, need to just switch directly to the main screen.
screenManager()->switchScreen(new MainScreen());
} else if (pendingMemStickFolder != g_Config.memStickDirectory) {
// Always ask for confirmation when called from the UI. Likely there's already some data.
screenManager()->push(new ConfirmMemstickMoveScreen(pendingMemStickFolder, false));
} else {
// User chose the same directory it's already in. Let's just bail.
TriggerFinish(DialogResult::DR_OK);
}
return UI::EVENT_DONE;
}
UI::EventReturn MemStickScreen::UseStorageRoot(UI::EventParams &params) {
Path pendingMemStickFolder = Path(g_externalDir);
if (initialSetup_) {
// There's not gonna be any files here in this case since it's a fresh install.
// Let's just accept it and move on. No need to move files either.
if (SwitchMemstickFolderTo(pendingMemStickFolder)) {
TriggerFinish(DialogResult::DR_OK);
} else {
// This can't really happen?? Not worth making an error message.
ERROR_LOG_REPORT(SYSTEM, "Could not switch memstick path in setup");
}
} else if (pendingMemStickFolder != g_Config.memStickDirectory) {
// Always ask for confirmation when called from the UI. Likely there's already some data.
screenManager()->push(new ConfirmMemstickMoveScreen(pendingMemStickFolder, false));
} else {
// User chose the same directory it's already in. Let's just bail.
TriggerFinish(DialogResult::DR_OK);
}
return UI::EVENT_DONE;
}
UI::EventReturn MemStickScreen::Browse(UI::EventParams &params) {
auto mm = GetI18NCategory(I18NCat::MAINMENU);
System_BrowseForFolder(GetRequesterToken(), mm->T("Choose folder"), [=](const std::string &value, int) {
Path pendingMemStickFolder = Path(value);
INFO_LOG(SYSTEM, "Got folder: '%s'", pendingMemStickFolder.c_str());
// Browse finished. Let's pop up the confirmation dialog.
if (!pendingMemStickFolder.empty() && pendingMemStickFolder == g_Config.memStickDirectory && File::IsDirectory(pendingMemStickFolder)) {
auto iz = GetI18NCategory(I18NCat::MEMSTICK);
// Not sure how this could happen, but let's go with it.
g_OSD.Show(OSDType::MESSAGE_SUCCESS, iz->T("Done!"));
done_ = true;
return;
}
errorNoticeView_->SetVisibility(UI::V_GONE);
screenManager()->push(new ConfirmMemstickMoveScreen(pendingMemStickFolder, initialSetup_));
}, [=]() {
errorNoticeView_->SetVisibility(UI::V_VISIBLE);
});
return UI::EVENT_DONE;
}
void MemStickScreen::dialogFinished(const Screen *dialog, DialogResult result) {
if (result == DialogResult::DR_OK) {
INFO_LOG(SYSTEM, "Confirmation screen done - moving on.");
// There's a screen manager bug if we call TriggerFinish directly.
// Can't be bothered right now, so we pick this up in update().
done_ = true;
}
// otherwise, we just keep going.
}
void MemStickScreen::update() {
UIDialogScreenWithBackground::update();
if (done_) {
TriggerFinish(DialogResult::DR_OK);
done_ = false;
}
}
// Keep the size with the file, so we can skip overly large ones in the move.
// The user will have to take care of them afterwards, it'll just take too long probably.
struct FileSuffix {
std::string suffix;
u64 fileSize;
};
static bool ListFileSuffixesRecursively(const Path &root, const Path &folder, std::vector<std::string> &dirSuffixes, std::vector<FileSuffix> &fileSuffixes) {
std::vector<File::FileInfo> files;
if (!File::GetFilesInDir(folder, &files)) {
return false;
}
for (auto &file : files) {
if (file.isDirectory) {
std::string dirSuffix;
if (root.ComputePathTo(file.fullName, dirSuffix)) {
if (!dirSuffix.empty()) {
dirSuffixes.push_back(dirSuffix);
ListFileSuffixesRecursively(root, folder / file.name, dirSuffixes, fileSuffixes);
}
} else {
ERROR_LOG_REPORT(SYSTEM, "Failed to compute PathTo from '%s' to '%s'", root.c_str(), folder.c_str());
}
} else {
std::string fileSuffix;
if (root.ComputePathTo(file.fullName, fileSuffix)) {
if (!fileSuffix.empty()) {
fileSuffixes.push_back(FileSuffix{ fileSuffix, file.size });
}
}
}
}
return true;
}
ConfirmMemstickMoveScreen::ConfirmMemstickMoveScreen(Path newMemstickFolder, bool initialSetup)
: newMemstickFolder_(newMemstickFolder), initialSetup_(initialSetup) {
existingFilesInNewFolder_ = FolderSeemsToBeUsed(newMemstickFolder);
if (initialSetup_) {
moveData_ = false;
}
}
ConfirmMemstickMoveScreen::~ConfirmMemstickMoveScreen() {
if (moveDataTask_) {
INFO_LOG(SYSTEM, "Move Data task still running, blocking on it");
moveDataTask_->BlockUntilReady();
delete moveDataTask_;
}
}
void ConfirmMemstickMoveScreen::CreateViews() {
using namespace UI;
auto sy = GetI18NCategory(I18NCat::SYSTEM);
auto iz = GetI18NCategory(I18NCat::MEMSTICK);
root_ = new LinearLayout(ORIENT_HORIZONTAL);
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);
int64_t freeSpaceNew;
int64_t freeSpaceOld;
free_disk_space(newMemstickFolder_, freeSpaceNew);
free_disk_space(oldMemstickFolder, freeSpaceOld);
leftColumn->Add(new TextView(iz->T("Selected PSP Data Folder"), ALIGN_LEFT, false));
if (!initialSetup_) {
leftColumn->Add(new NoticeView(NoticeLevel::WARN, iz->T("PPSSPP will restart after the change"), ""));
}
leftColumn->Add(new TextView(newMemstickFolder_.ToVisualString(), ALIGN_LEFT, false));
std::string newFreeSpaceText = std::string(iz->T("Free space")) + ": " + FormatSpaceString(freeSpaceNew);
leftColumn->Add(new TextView(newFreeSpaceText, ALIGN_LEFT, false));
if (existingFilesInNewFolder_) {
leftColumn->Add(new NoticeView(NoticeLevel::SUCCESS, iz->T("Already contains PSP data"), ""));
if (!moveData_) {
leftColumn->Add(new NoticeView(NoticeLevel::INFO, iz->T("No data will be changed"), ""));
}
}
if (!error_.empty()) {
leftColumn->Add(new TextView(error_, ALIGN_LEFT, false));
}
if (!oldMemstickFolder.empty()) {
std::string oldFreeSpaceText = std::string(iz->T("Free space")) + ": " + FormatSpaceString(freeSpaceOld);
rightColumn->Add(new TextView(std::string(iz->T("Current")) + ":", ALIGN_LEFT, false));
rightColumn->Add(new TextView(oldMemstickFolder.ToVisualString(), ALIGN_LEFT, false));
rightColumn->Add(new TextView(oldFreeSpaceText, ALIGN_LEFT, false));
}
if (moveDataTask_) {
progressView_ = leftColumn->Add(new TextView(progressReporter_.Get()));
} else {
progressView_ = nullptr;
}
if (!moveDataTask_) {
if (!initialSetup_) {
leftColumn->Add(new CheckBox(&moveData_, iz->T("Move Data")))->OnClick.Handle(this, &ConfirmMemstickMoveScreen::OnMoveDataClick);
}
auto di = GetI18NCategory(I18NCat::DIALOG);
leftColumn->Add(new Choice(di->T("OK")))->OnClick.Handle(this, &ConfirmMemstickMoveScreen::OnConfirm);
leftColumn->Add(new Choice(di->T("Back")))->OnClick.Handle<UIScreen>(this, &UIScreen::OnBack);
}
}
UI::EventReturn ConfirmMemstickMoveScreen::OnMoveDataClick(UI::EventParams &params) {
RecreateViews();
return UI::EVENT_DONE;
}
void ConfirmMemstickMoveScreen::update() {
UIDialogScreenWithBackground::update();
auto iz = GetI18NCategory(I18NCat::MEMSTICK);
if (moveDataTask_) {
if (progressView_) {
progressView_->SetText(progressReporter_.Get());
}
MoveResult *result = moveDataTask_->Poll();
if (result) {
if (result->success) {
progressReporter_.Set(iz->T("Done!"));
INFO_LOG(SYSTEM, "Move data task finished successfully!");
// Succeeded!
FinishFolderMove();
} else {
progressReporter_.Set(iz->T("Failed to move some files!"));
INFO_LOG(SYSTEM, "Move data task failed!");
// What do we do here? We might be in the middle of a move... Bad.
RecreateViews();
}
delete moveDataTask_;
moveDataTask_ = nullptr;
}
}
}
UI::EventReturn ConfirmMemstickMoveScreen::OnConfirm(UI::EventParams &params) {
// Transfer all the files in /PSP from the original directory.
// Should probably be done on a background thread so we can show some UI.
// So we probably need another screen for this with a progress bar..
// If the directory itself is called PSP, don't go below.
if (moveData_) {
progressReporter_.Set(T(I18NCat::MEMSTICK, "Starting move..."));
moveDataTask_ = Promise<MoveResult *>::Spawn(&g_threadManager, [&]() -> MoveResult * {
auto ms = GetI18NCategory(I18NCat::MEMSTICK);
Path moveSrc = g_Config.memStickDirectory;
Path moveDest = newMemstickFolder_;
if (moveSrc.GetFilename() != "PSP") {
moveSrc = moveSrc / "PSP";
}
if (moveDest.GetFilename() != "PSP") {
moveDest = moveDest / "PSP";
File::CreateDir(moveDest);
}
INFO_LOG(SYSTEM, "About to move PSP data from '%s' to '%s'", moveSrc.c_str(), moveDest.c_str());
// Search through recursively, listing the files to move and also summing their sizes.
std::vector<FileSuffix> fileSuffixesToMove;
std::vector<std::string> directorySuffixesToCreate;
// NOTE: It's correct to pass moveSrc twice here, it's to keep the root in the recursion.
if (!ListFileSuffixesRecursively(moveSrc, moveSrc, directorySuffixesToCreate, fileSuffixesToMove)) {
// TODO: Handle failure listing files.
std::string error = "Failed to read old directory";
INFO_LOG(SYSTEM, "%s", error.c_str());
progressReporter_.Set(ms->T(error.c_str()));
return new MoveResult{ false, error };
}
bool dryRun = false; // Useful for debugging.
size_t failedFiles = 0;
size_t skippedFiles = 0;
// We're not moving huge files like ISOs during this process, unless
// they can be directly moved, without rewriting the file.
const uint64_t BIG_FILE_THRESHOLD = 24 * 1024 * 1024;
if (!moveSrc.empty()) {
// Better not interrupt the app while this is happening!
// Create all the necessary directories.
for (auto &dirSuffix : directorySuffixesToCreate) {
Path dir = moveDest / dirSuffix;
if (dryRun) {
INFO_LOG(SYSTEM, "dry run: Would have created dir '%s'", dir.c_str());
} else {
INFO_LOG(SYSTEM, "Creating dir '%s'", dir.c_str());
progressReporter_.Set(dirSuffix);
// Just ignore already-exists errors.
File::CreateDir(dir);
}
}
for (auto &fileSuffix : fileSuffixesToMove) {
progressReporter_.Set(StringFromFormat("%s (%s)", fileSuffix.suffix.c_str(), NiceSizeFormat(fileSuffix.fileSize).c_str()));
Path from = moveSrc / fileSuffix.suffix;
Path to = moveDest / fileSuffix.suffix;
if (fileSuffix.fileSize > BIG_FILE_THRESHOLD) {
// We only move big files if it's fast to do so.
if (dryRun) {
INFO_LOG(SYSTEM, "dry run: Would have moved '%s' to '%s' (%d bytes) if fast", from.c_str(), to.c_str(), (int)fileSuffix.fileSize);
} else {
if (!File::MoveIfFast(from, to)) {
INFO_LOG(SYSTEM, "Skipped moving file '%s' to '%s' (%s)", from.c_str(), to.c_str(), NiceSizeFormat(fileSuffix.fileSize).c_str());
skippedFiles++;
} else {
INFO_LOG(SYSTEM, "Moved file '%s' to '%s'", from.c_str(), to.c_str());
}
}
} else {
if (dryRun) {
INFO_LOG(SYSTEM, "dry run: Would have moved '%s' to '%s' (%d bytes)", from.c_str(), to.c_str(), (int)fileSuffix.fileSize);
} else {
// Remove the "from" prefix from the path.
// We have to drop down to string operations for this.
if (!File::Move(from, to)) {
ERROR_LOG(SYSTEM, "Failed to move file '%s' to '%s'", from.c_str(), to.c_str());
failedFiles++;
// Should probably just bail?
} else {
INFO_LOG(SYSTEM, "Moved file '%s' to '%s'", from.c_str(), to.c_str());
}
}
}
}
// Delete all the old, now hopefully empty, directories.
for (auto &dirSuffix : directorySuffixesToCreate) {
Path dir = moveSrc / dirSuffix;
if (dryRun) {
INFO_LOG(SYSTEM, "dry run: Would have deleted dir '%s'", dir.c_str());
} else {
INFO_LOG(SYSTEM, "Deleting dir '%s'", dir.c_str());
progressReporter_.Set(dirSuffix);
if (File::Exists(dir)) {
File::DeleteDir(dir);
}
}
}
}
return new MoveResult{ true, "", failedFiles };
}, TaskType::IO_BLOCKING, TaskPriority::HIGH);
RecreateViews();
} else {
FinishFolderMove();
}
return UI::EVENT_DONE;
}
void ConfirmMemstickMoveScreen::FinishFolderMove() {
auto ms = GetI18NCategory(I18NCat::MEMSTICK);
// Successful so far, switch the memstick folder.
if (!SwitchMemstickFolderTo(newMemstickFolder_)) {
// TODO: More precise errors.
error_ = ms->T("That folder doesn't work as a memstick folder.");
return;
}
// If the chosen folder already had a config, reload it!
g_Config.Load();
PostLoadConfig();
if (!initialSetup_) {
// We restart the app here, to get the new settings.
System_RestartApp("");
} else {
// This is initial setup, we now switch to the main screen, if we were successful
// (which we better have been...)
if (g_Config.Save("MemstickPathChanged")) {
// TriggerFinish(DialogResult::DR_OK);
screenManager()->switchScreen(new MainScreen());
} else {
error_ = ms->T("Failed to save config");
RecreateViews();
}
}
}