// 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/NativeApp.h" #include "Common/System/Display.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/Util/GameManager.h" #include "Core/System.h" #include "Core/Config.h" #include "UI/MemStickScreen.h" #include "UI/MainScreen.h" #include "UI/MiscScreens.h" static bool FolderSeemsToBeUsed(Path newMemstickFolder) { // Inspect the potential new folder. 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) { Path testWriteFile = newMemstickFolder / ".write_verify_file"; // Doesn't already exist, create. // Should this ever happen? if (newMemstickFolder.Type() == PathType::NATIVE) { if (!File::Exists(newMemstickFolder)) { File::CreateFullPath(newMemstickFolder); } 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"; 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. } // 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) { } void MemStickScreen::CreateViews() { using namespace UI; auto di = GetI18NCategory("Dialog"); auto iz = GetI18NCategory("MemStick"); Margins actionMenuMargins(15, 15, 15, 0); root_ = new LinearLayout(ORIENT_HORIZONTAL); Spacer *spacerColumn = new Spacer(new LinearLayoutParams(20.0, FILL_PARENT, 0.0f)); ScrollView *leftColumnScroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(1.0)); ViewGroup *leftColumn = new LinearLayoutList(ORIENT_VERTICAL); ViewGroup *rightColumnItems = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(300, FILL_PARENT, actionMenuMargins)); root_->Add(spacerColumn); root_->Add(leftColumnScroll); root_->Add(rightColumnItems); leftColumnScroll->Add(leftColumn); if (initialSetup_) { leftColumn->Add(new TextView(iz->T("Welcome to PPSSPP!"), ALIGN_LEFT, false)); leftColumn->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 12.0f, 0.0f))); } if (System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE)) { leftColumn->Add(new TextView(iz->T("ScopedStorageWarning", "WARNING: BETA ANDROID SCOPED STORAGE SUPPORT\nMAY EAT YOUR DATA"), ALIGN_LEFT, false)); } leftColumn->Add(new TextView(iz->T("MemoryStickDescription", "Choose PSP data storage (Memory Stick):"), ALIGN_LEFT, false)); // For legacy Android systems, so you can switch back to the old ways if you move to SD or something. // TODO: Gonna need a scroll view. #if PPSSPP_PLATFORM(ANDROID) if (!System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE)) { leftColumn->Add(new Choice(iz->T("Use PSP folder at root of storage")))->OnClick.Handle(this, &MemStickScreen::OnUseStorageRoot); 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); leftColumn->Add(new TextView(iz->T("EasyUSBAccess", "Easy USB access")))->SetBullet(true); } #endif leftColumn->Add(new Choice(iz->T("Create or Choose a PSP folder")))->OnClick.Handle(this, &MemStickScreen::OnBrowse); 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); leftColumn->Add(new TextView(iz->T("EasyUSBAccess", "Easy USB access")))->SetBullet(true); leftColumn->Add(new Choice(iz->T("Use App Private Directory")))->OnClick.Handle(this, &MemStickScreen::OnUseInternalStorage); // Consider https://www.compart.com/en/unicode/U+26A0 (unicode warning sign?)? or a graphic? leftColumn->Add(new TextView(iz->T("DataWillBeLostOnUninstall", "Warning! Data will be lost when you uninstall PPSSPP!")))->SetBullet(true); leftColumn->Add(new TextView(iz->T("DataCannotBeShared", "Data CANNOT be shared between PPSSPP regular/Gold!")))->SetBullet(true); #if GOLD leftColumn->Add(new TextView(iz->T("USBAccessThroughGold", "USB access through Android/data/org.ppsspp.ppssppgold/files")))->SetBullet(true); #else leftColumn->Add(new TextView(iz->T("USBAccessThrough", "USB access through Android/data/org.ppsspp.ppsspp/files")))->SetBullet(true); #endif leftColumn->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 12.0f, 0.0f))); if (!initialSetup_) { rightColumnItems->Add(new Choice(di->T("Back")))->OnClick.Handle(this, &UIScreen::OnBack); } INFO_LOG(SYSTEM, "MemStickScreen: initialSetup=%d", (int)initialSetup_); } UI::EventReturn MemStickScreen::OnUseInternalStorage(UI::EventParams ¶ms) { Path pendingMemStickFolder = Path(g_extFilesDir); 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. } } else { // Always ask for confirmation when called from the UI. Likely there's already some data. screenManager()->push(new ConfirmMemstickMoveScreen(pendingMemStickFolder, false)); } return UI::EVENT_DONE; } UI::EventReturn MemStickScreen::OnUseStorageRoot(UI::EventParams ¶ms) { 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. } } else { // Always ask for confirmation when called from the UI. Likely there's already some data. screenManager()->push(new ConfirmMemstickMoveScreen(pendingMemStickFolder, false)); } return UI::EVENT_DONE; } UI::EventReturn MemStickScreen::OnBrowse(UI::EventParams ¶ms) { System_SendMessage("browse_folder", ""); return UI::EVENT_DONE; } void MemStickScreen::sendMessage(const char *message, const char *value) { // Always call the base class method first to handle the most common messages. UIDialogScreenWithBackground::sendMessage(message, value); if (screenManager()->topScreen() == this) { if (!strcmp(message, "browse_folderSelect")) { std::string filename; filename = value; INFO_LOG(SYSTEM, "Got folder: '%s'", filename.c_str()); // Browse finished. Let's pop up the confirmation dialog. Path pendingMemStickFolder = Path(filename); if (pendingMemStickFolder == g_Config.memStickDirectory) { auto iz = GetI18NCategory("MemStick"); #if PPSSPP_PLATFORM(ANDROID) SystemToast(iz->T("That's the folder being used!")); #endif return; } bool existingFiles = FolderSeemsToBeUsed(pendingMemStickFolder); screenManager()->push(new ConfirmMemstickMoveScreen(pendingMemStickFolder, initialSetup_)); } } } 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; } } static bool ListFileSuffixesRecursively(const Path &root, Path folder, std::vector &dirSuffixes, std::vector &fileSuffixes) { std::vector files; if (!File::GetFilesInDir(folder, &files)) { return false; } for (auto &file : files) { if (file.isDirectory) { std::string dirSuffix; if (root.ComputePathTo(file.fullName, dirSuffix)) { dirSuffixes.push_back(dirSuffix); ListFileSuffixesRecursively(root, folder / file.name, dirSuffixes, fileSuffixes); } else { ERROR_LOG(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)) { fileSuffixes.push_back(fileSuffix); } } } 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 di = GetI18NCategory("Dialog"); auto sy = GetI18NCategory("System"); auto iz = GetI18NCategory("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("New PSP Data Folder"), ALIGN_LEFT, false)); if (!initialSetup_) { leftColumn->Add(new TextView(iz->T("PPSSPP will restart after the change."), ALIGN_LEFT, false)); } 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 TextView(iz->T("Already contains data."), ALIGN_LEFT, false)); if (!moveData_) { leftColumn->Add(new TextView(iz->T("No data will be changed."), ALIGN_LEFT, false)); } } 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(iz->T("Old PSP Data Folder"), 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); } leftColumn->Add(new Choice(di->T("OK")))->OnClick.Handle(this, &ConfirmMemstickMoveScreen::OnConfirm); leftColumn->Add(new Choice(di->T("Back")))->OnClick.Handle(this, &UIScreen::OnBack); } } UI::EventReturn ConfirmMemstickMoveScreen::OnMoveDataClick(UI::EventParams ¶ms) { RecreateViews(); return UI::EVENT_DONE; } void ConfirmMemstickMoveScreen::update() { UIDialogScreenWithBackground::update(); if (moveDataTask_) { if (progressView_) { progressView_->SetText(progressReporter_.Get()); } bool *result = moveDataTask_->Poll(); if (result) { if (*result) { progressReporter_.Set("Done!"); INFO_LOG(SYSTEM, "Move data task finished successfully!"); // Succeeded! FinishFolderMove(); } else { 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 ¶ms) { auto sy = GetI18NCategory("System"); auto iz = GetI18NCategory("MemStick"); // 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("Starting move..."); moveDataTask_ = Promise::Spawn(&g_threadManager, [&]() -> bool * { 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 fileSuffixesToMove; std::vector 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(error); return new bool(false); } bool dryRun = false; // Useful for debugging. size_t moveFailures = 0; 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()); if (!File::Exists(dir)) { File::CreateDir(dir); } } } for (auto &fileSuffix : fileSuffixesToMove) { progressReporter_.Set(fileSuffix); Path from = moveSrc / fileSuffix; Path to = moveDest / fileSuffix; if (dryRun) { INFO_LOG(SYSTEM, "dry run: Would have moved '%s' to '%s'", from.c_str(), to.c_str()); } 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()); moveFailures++; // 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()); if (!File::Exists(dir)) { File::DeleteDir(dir); } } } } if (moveFailures > 0) { std::string error = "Failed to move some files!"; progressReporter_.Set(error); return new bool(false); } return new bool(true); }, TaskType::IO_BLOCKING); RecreateViews(); } else { FinishFolderMove(); } return UI::EVENT_DONE; } void ConfirmMemstickMoveScreen::FinishFolderMove() { auto iz = GetI18NCategory("MemStick"); // Successful so far, switch the memstick folder. if (!SwitchMemstickFolderTo(newMemstickFolder_)) { // TODO: More precise errors. error_ = iz->T("That folder doesn't work as a memstick folder."); return; } // If the chosen folder already had a config, reload it! g_Config.Load(); if (!initialSetup_) { // We restart the app here, to get the new settings. System_SendMessage("graphics_restart", ""); } if (g_Config.Save("MemstickPathChanged")) { TriggerFinish(DialogResult::DR_OK); } else { error_ = iz->T("Failed to save config"); RecreateViews(); } }