diff --git a/Common/Data/Text/Parsers.cpp b/Common/Data/Text/Parsers.cpp index 427fb74316..b6ce78d958 100644 --- a/Common/Data/Text/Parsers.cpp +++ b/Common/Data/Text/Parsers.cpp @@ -6,6 +6,23 @@ #include "Common/StringUtils.h" +// Not strictly a parser... +void NiceSizeFormat(size_t size, char *out, size_t bufSize) { + const char *sizes[] = { "B","KB","MB","GB","TB","PB","EB" }; + int s = 0; + int frac = 0; + while (size >= 1024) { + s++; + frac = (int)size & 1023; + size /= 1024; + } + float f = (float)size + ((float)frac / 1024.0f); + if (s == 0) + snprintf(out, bufSize, "%d B", (int)size); + else + snprintf(out, bufSize, "%3.1f %s", f, sizes[s]); +} + bool Version::ParseVersionString(std::string str) { if (str.empty()) return false; diff --git a/Common/Data/Text/Parsers.h b/Common/Data/Text/Parsers.h index bfa111774c..35b3839cfb 100644 --- a/Common/Data/Text/Parsers.h +++ b/Common/Data/Text/Parsers.h @@ -70,3 +70,5 @@ static bool TryParse(const std::string &str, N *const output) { } else return false; } + +void NiceSizeFormat(size_t size, char *out, size_t bufSize); diff --git a/Common/File/AndroidStorage.cpp b/Common/File/AndroidStorage.cpp index d8d6468b91..78d457aabf 100644 --- a/Common/File/AndroidStorage.cpp +++ b/Common/File/AndroidStorage.cpp @@ -279,7 +279,9 @@ const char *Android_ErrorToString(StorageError error) { #else -// This string should never appear except on Android. +// These strings should never appear except on Android. +// Very hacky. std::string g_extFilesDir = "(IF YOU SEE THIS THERE'S A BUG)"; +std::string g_externalDir = "(IF YOU SEE THIS THERE'S A BUG (2))"; #endif diff --git a/Common/File/AndroidStorage.h b/Common/File/AndroidStorage.h index 131c0125d2..2a890d33b4 100644 --- a/Common/File/AndroidStorage.h +++ b/Common/File/AndroidStorage.h @@ -30,12 +30,13 @@ inline StorageError StorageErrorFromInt(int ival) { } } +extern std::string g_extFilesDir; +extern std::string g_externalDir; + #if PPSSPP_PLATFORM(ANDROID) && !defined(__LIBRETRO__) #include -extern std::string g_extFilesDir; - void Android_StorageSetNativeActivity(jobject nativeActivity); bool Android_IsContentUri(const std::string &uri); @@ -59,8 +60,6 @@ void Android_RegisterStorageCallbacks(JNIEnv * env, jobject obj); #else -extern std::string g_extFilesDir; - // Stub out the Android Storage wrappers, so that we can avoid ifdefs everywhere. inline bool Android_IsContentUri(const std::string &uri) { return false; } diff --git a/Common/File/DiskFree.cpp b/Common/File/DiskFree.cpp index 3a5fb9e722..ea8e0f0d39 100644 --- a/Common/File/DiskFree.cpp +++ b/Common/File/DiskFree.cpp @@ -23,7 +23,7 @@ #include "Common/File/AndroidStorage.h" #include "Common/Data/Encoding/Utf8.h" -bool free_disk_space(const Path &path, uint64_t &space) { +bool free_disk_space(const Path &path, int64_t &space) { #ifdef _WIN32 ULARGE_INTEGER free; if (GetDiskFreeSpaceExW(path.ToWString().c_str(), &free, nullptr, nullptr)) { @@ -41,8 +41,10 @@ bool free_disk_space(const Path &path, uint64_t &space) { int res = statvfs(path.c_str(), &diskstat); if (res == 0) { + // Not sure why we're excluding Android here anyway... #ifndef __ANDROID__ if (diskstat.f_flag & ST_RDONLY) { + // No space to write. space = 0; return true; } diff --git a/Common/File/DiskFree.h b/Common/File/DiskFree.h index 5405fc4dc4..a08f44bdd1 100644 --- a/Common/File/DiskFree.h +++ b/Common/File/DiskFree.h @@ -4,4 +4,5 @@ #include "Common/File/Path.h" -bool free_disk_space(const Path &path, uint64_t &space); +// If this fails, false is returned and space is negative. +bool free_disk_space(const Path &path, int64_t &space); diff --git a/Common/File/FileUtil.cpp b/Common/File/FileUtil.cpp index 134d84f74b..dc8551767f 100644 --- a/Common/File/FileUtil.cpp +++ b/Common/File/FileUtil.cpp @@ -510,7 +510,10 @@ bool CreateFullPath(const Path &path) { Path root = path.GetRootVolume(); - std::string diff = root.PathTo(path); // always with forward slashes + std::string diff; + if (!root.ComputePathTo(path, diff)) { + return false; + } std::vector parts; SplitString(diff, '/', parts); diff --git a/Common/File/Path.cpp b/Common/File/Path.cpp index 5f0e905dda..3ffd64549d 100644 --- a/Common/File/Path.cpp +++ b/Common/File/Path.cpp @@ -10,16 +10,6 @@ #include "android/jni/AndroidContentURI.h" Path::Path(const std::string &str) { - if (str.empty()) { - type_ = PathType::UNDEFINED; - } else if (startsWith(str, "http://") || startsWith(str, "https://")) { - type_ = PathType::HTTP; - } else if (Android_IsContentUri(str)) { - type_ = PathType::CONTENT_URI; - } else { - type_ = PathType::NATIVE; - } - Init(str); } @@ -31,7 +21,35 @@ Path::Path(const std::wstring &str) { #endif void Path::Init(const std::string &str) { - path_ = str; + if (str.empty()) { + type_ = PathType::UNDEFINED; + path_.clear(); + } else if (startsWith(str, "http://") || startsWith(str, "https://")) { + type_ = PathType::HTTP; + path_ = str; + } else if (Android_IsContentUri(str)) { + // Content URIs on non scoped-storage (and possibly other cases?) can contain + // "raw:/" URIs inside. This happens when picking the Download folder using the folder browser + // on Android 9. + // Here's an example: + // content://com.android.providers.downloads.documents/tree/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2Fp/document/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2Fp + // + // Since this is a legacy use case, I think it's safe enough to just detect this + // and flip it to a NATIVE url and hope for the best. + AndroidContentURI uri(str); + if (startsWith(uri.FilePath(), "raw:/")) { + INFO_LOG(SYSTEM, "Raw path detected: %s", uri.FilePath().c_str()); + path_ = uri.FilePath().substr(4); + type_ = PathType::NATIVE; + } else { + // A normal content URI path. + type_ = PathType::CONTENT_URI; + path_ = str; + } + } else { + type_ = PathType::NATIVE; + path_ = str; + } #if PPSSPP_PLATFORM(WINDOWS) // Flip all the slashes around. We flip them back on ToWString(). @@ -236,7 +254,6 @@ bool Path::CanNavigateUp() const { if (type_ == PathType::CONTENT_URI) { return AndroidContentURI(path_).CanNavigateUp(); } - if (path_ == "/" || path_ == "") { return false; } @@ -300,27 +317,32 @@ bool Path::IsAbsolute() const { return false; } -std::string Path::PathTo(const Path &other) const { +bool Path::ComputePathTo(const Path &other, std::string &path) const { if (!other.StartsWith(*this)) { // Can't do this. Should return an error. - return std::string(); + return false; + } + + if (*this == other) { + // Equal, the path is empty. + path.clear(); + return true; } std::string diff; - if (type_ == PathType::CONTENT_URI) { AndroidContentURI a(path_); AndroidContentURI b(other.path_); if (a.RootPath() != b.RootPath()) { // No common root, can't do anything - return std::string(); + return false; } - diff = a.PathTo(b); + return a.ComputePathTo(b, path); } else if (path_ == "/") { - diff = other.path_.substr(1); + path = other.path_.substr(1); + return true; } else { - diff = other.path_.substr(path_.size() + 1); + path = other.path_.substr(path_.size() + 1); + return true; } - - return diff; } diff --git a/Common/File/Path.h b/Common/File/Path.h index afec18ac7e..4a0658e5f7 100644 --- a/Common/File/Path.h +++ b/Common/File/Path.h @@ -82,7 +82,7 @@ public: // For Android directory trees, navigates to the root of the tree. Path GetRootVolume() const; - std::string PathTo(const Path &child) const; + bool ComputePathTo(const Path &other, std::string &path) const; bool operator ==(const Path &other) const { return path_ == other.path_ && type_ == other.type_; diff --git a/Common/File/PathBrowser.cpp b/Common/File/PathBrowser.cpp index ab85e2422b..756f0f74b3 100644 --- a/Common/File/PathBrowser.cpp +++ b/Common/File/PathBrowser.cpp @@ -144,11 +144,14 @@ void PathBrowser::HandlePath() { std::unique_lock guard(pendingLock_); std::vector results; - Path lastPath; + Path lastPath("NONSENSE THAT WONT EQUAL A PATH"); while (!pendingStop_) { while (lastPath == pendingPath_ && !pendingCancel_) { pendingCond_.wait(guard); } + if (pendingStop_) { + break; + } lastPath = pendingPath_; bool success = false; if (lastPath.Type() == PathType::HTTP) { @@ -156,6 +159,9 @@ void PathBrowser::HandlePath() { results.clear(); success = LoadRemoteFileList(lastPath, &pendingCancel_, results); guard.lock(); + } else if (lastPath.empty()) { + results.clear(); + success = true; } else { guard.unlock(); results.clear(); diff --git a/Common/File/PathBrowser.h b/Common/File/PathBrowser.h index dd8cdeebb2..9f10cde560 100644 --- a/Common/File/PathBrowser.h +++ b/Common/File/PathBrowser.h @@ -35,6 +35,10 @@ public: } std::string GetFriendlyPath() const; + bool empty() const { + return path_.empty(); + } + private: void HandlePath(); void ResetPending(); diff --git a/Common/System/NativeApp.h b/Common/System/NativeApp.h index d501d03b43..d911fd9076 100644 --- a/Common/System/NativeApp.h +++ b/Common/System/NativeApp.h @@ -89,3 +89,5 @@ void NativeSetMixer(void* mixer); // Main thread. void NativeShutdownGraphics(); void NativeShutdown(); + +void PostLoadConfig(); diff --git a/Common/UI/Screen.cpp b/Common/UI/Screen.cpp index 1945b5be4f..ea5ed185f5 100644 --- a/Common/UI/Screen.cpp +++ b/Common/UI/Screen.cpp @@ -163,6 +163,8 @@ void ScreenManager::render() { iter--; Layer backback = *iter; + _assert_(backback.screen); + // TODO: Make really sure that this "mismatched" pre/post only happens // when screens are "compatible" (both are UIScreens, for example). backback.screen->preRender(); @@ -174,6 +176,7 @@ void ScreenManager::render() { break; } default: + _assert_(stack_.back().screen); stack_.back().screen->preRender(); stack_.back().screen->render(); if (postRenderCb_) diff --git a/Core/Config.cpp b/Core/Config.cpp index d97e2509cd..7c35cd89de 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -1412,12 +1412,14 @@ void Config::Load(const char *iniFileName, const char *controllerIniFilename) { INFO_LOG(LOADER, "Config loaded: '%s'", iniFilename_.c_str()); } -void Config::Save(const char *saveReason) { +bool Config::Save(const char *saveReason) { if (!IsFirstInstance()) { // TODO: Should we allow saving config if started from a different directory? // How do we tell? WARN_LOG(LOADER, "Not saving config - secondary instances don't."); - return; + + // Don't want to retry or something. + return true; } if (jitForcedOff) { @@ -1488,7 +1490,7 @@ void Config::Save(const char *saveReason) { if (!iniFile.Save(iniFilename_)) { ERROR_LOG(LOADER, "Error saving config (%s)- can't write ini '%s'", saveReason, iniFilename_.c_str()); System_SendMessage("toast", "Failed to save settings!\nCheck permissions, or try to restart the device."); - return; + return false; } INFO_LOG(LOADER, "Config saved (%s): '%s'", saveReason, iniFilename_.c_str()); @@ -1501,7 +1503,7 @@ void Config::Save(const char *saveReason) { KeyMap::SaveToIni(controllerIniFile); if (!controllerIniFile.Save(controllerIniFilename_)) { ERROR_LOG(LOADER, "Error saving config - can't write ini '%s'", controllerIniFilename_.c_str()); - return; + return false; } INFO_LOG(LOADER, "Controller config saved: %s", controllerIniFilename_.c_str()); } @@ -1512,6 +1514,7 @@ void Config::Save(const char *saveReason) { // force JIT off again just in case Config::Save() is called without exiting PPSSPP g_Config.iCpuCore = (int)CPUCore::INTERPRETER; } + return true; } // Use for debugging the version check without messing with the server diff --git a/Core/Config.h b/Core/Config.h index a07a6182d8..f5dcd9ad44 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -490,7 +490,7 @@ public: std::string dismissedVersion; void Load(const char *iniFileName = nullptr, const char *controllerIniFilename = nullptr); - void Save(const char *saveReason); + bool Save(const char *saveReason); void Reload(); void RestoreDefaults(); diff --git a/Core/Dialog/SavedataParam.cpp b/Core/Dialog/SavedataParam.cpp index 1e26486e2c..0558dacc60 100644 --- a/Core/Dialog/SavedataParam.cpp +++ b/Core/Dialog/SavedataParam.cpp @@ -1045,11 +1045,11 @@ int SavedataParam::BuildHash(unsigned char *output, return 0; } +// TODO: Merge with NiceSizeFormat? That one has a decimal though. std::string SavedataParam::GetSpaceText(u64 size, bool roundUp) { - static const char *suffixes[] = {"B", "KB", "MB", "GB"}; char text[50]; - + static const char *suffixes[] = {"B", "KB", "MB", "GB"}; for (size_t i = 0; i < ARRAY_SIZE(suffixes); ++i) { if (size < 1024) @@ -1063,7 +1063,6 @@ std::string SavedataParam::GetSpaceText(u64 size, bool roundUp) size /= 1024; } } - snprintf(text, sizeof(text), "%llu TB", size); return std::string(text); } diff --git a/Core/FileLoaders/DiskCachingFileLoader.cpp b/Core/FileLoaders/DiskCachingFileLoader.cpp index 06b140e0e5..73ba1ca868 100644 --- a/Core/FileLoaders/DiskCachingFileLoader.cpp +++ b/Core/FileLoaders/DiskCachingFileLoader.cpp @@ -754,9 +754,9 @@ u64 DiskCachingFileLoaderCache::FreeDiskSpace() { dir = GetSysDirectory(DIRECTORY_CACHE); } - uint64_t result = 0; + int64_t result = 0; if (free_disk_space(dir, result)) { - return result; + return (u64)result; } // We can't know for sure how much is free, so we have to assume none. diff --git a/Core/FileSystems/DirectoryFileSystem.cpp b/Core/FileSystems/DirectoryFileSystem.cpp index 4e2ab9b2d8..528654a045 100644 --- a/Core/FileSystems/DirectoryFileSystem.cpp +++ b/Core/FileSystems/DirectoryFileSystem.cpp @@ -925,9 +925,9 @@ std::vector DirectoryFileSystem::GetDirListing(std::string path) { } u64 DirectoryFileSystem::FreeSpace(const std::string &path) { - uint64_t result = 0; + int64_t result = 0; if (free_disk_space(GetLocalPath(path), result)) { - return ReplayApplyDisk64(ReplayAction::FREESPACE, result, CoreTiming::GetGlobalTimeUs()); + return ReplayApplyDisk64(ReplayAction::FREESPACE, (uint64_t)result, CoreTiming::GetGlobalTimeUs()); } #if HOST_IS_CASE_SENSITIVE diff --git a/Core/HLE/sceCtrl.cpp b/Core/HLE/sceCtrl.cpp index fe78433792..78dee49bda 100644 --- a/Core/HLE/sceCtrl.cpp +++ b/Core/HLE/sceCtrl.cpp @@ -501,7 +501,8 @@ static int sceCtrlReadBufferNegative(u32 ctrlDataPtr, u32 nBufs) static int sceCtrlPeekBufferPositive(u32 ctrlDataPtr, u32 nBufs) { int done = __CtrlReadBuffer(ctrlDataPtr, nBufs, false, true); - DEBUG_LOG(SCECTRL, "%d=sceCtrlPeekBufferPositive(%08x, %i)", done, ctrlDataPtr, nBufs); + // Some homebrew call this in a tight loop - so VERBOSE it is. + VERBOSE_LOG(SCECTRL, "%d=sceCtrlPeekBufferPositive(%08x, %i)", done, ctrlDataPtr, nBufs); hleEatCycles(330); return done; } @@ -509,7 +510,8 @@ static int sceCtrlPeekBufferPositive(u32 ctrlDataPtr, u32 nBufs) static int sceCtrlPeekBufferNegative(u32 ctrlDataPtr, u32 nBufs) { int done = __CtrlReadBuffer(ctrlDataPtr, nBufs, true, true); - DEBUG_LOG(SCECTRL, "%d=sceCtrlPeekBufferNegative(%08x, %i)", done, ctrlDataPtr, nBufs); + // Some homebrew call this in a tight loop - so VERBOSE it is. + VERBOSE_LOG(SCECTRL, "%d=sceCtrlPeekBufferNegative(%08x, %i)", done, ctrlDataPtr, nBufs); hleEatCycles(330); return done; } diff --git a/Core/HLE/sceIo.cpp b/Core/HLE/sceIo.cpp index c856794110..0c4dcf20e1 100644 --- a/Core/HLE/sceIo.cpp +++ b/Core/HLE/sceIo.cpp @@ -558,13 +558,6 @@ static void __IoAsyncEndCallback(SceUID threadID, SceUID prevCallbackId) { } } -static DirectoryFileSystem *exdataSystem = nullptr; -#if defined(USING_WIN_UI) || defined(APPLE) -static DirectoryFileSystem *flash0System = nullptr; -#else -static VFSFileSystem *flash0System = nullptr; -#endif - static void __IoManagerThread() { SetCurrentThreadName("IO"); while (ioManagerThreadEnabled && coreState != CORE_BOOT_ERROR && coreState != CORE_RUNTIME_ERROR && coreState != CORE_POWERDOWN) { diff --git a/UI/DevScreens.cpp b/UI/DevScreens.cpp index 735ae47928..2d18c69315 100644 --- a/UI/DevScreens.cpp +++ b/UI/DevScreens.cpp @@ -23,6 +23,7 @@ #include "Common/System/NativeApp.h" #include "Common/System/System.h" #include "Common/GPU/OpenGL/GLFeatures.h" +#include "Common/File/AndroidStorage.h" #include "Common/Data/Text/I18n.h" #include "Common/Net/HTTPClient.h" #include "Common/UI/Context.h" @@ -59,12 +60,8 @@ int GetD3DCompilerVersion(); #endif -#if PPSSPP_PLATFORM(ANDROID) - #include "android/jni/app-android.h" -#endif - static const char *logLevelList[] = { "Notice", "Error", @@ -592,8 +589,11 @@ void SystemInfoScreen::CreateViews() { #if PPSSPP_PLATFORM(ANDROID) storage->Add(new InfoItem("ExtFilesDir", g_extFilesDir)); - if (System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE)) { - storage->Add(new InfoItem("Scoped Storage", di->T("Yes"))); + bool scoped = System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE); + storage->Add(new InfoItem("Scoped Storage Enabled", scoped ? di->T("Yes") : di->T("No"))); + if (System_GetPropertyInt(SYSPROP_SYSTEMVERSION) >= 30) { + // This flag is only relevant on Android API 30+. + storage->Add(new InfoItem("IsStoragePreservedLegacy", Android_IsExternalStoragePreservedLegacy() ? di->T("Yes") : di->T("No"))); } #endif diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 996638b294..6b1e97e368 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -903,9 +903,6 @@ void GameSettingsScreen::CreateViews() { systemSettings->Add(new ItemHeader(sy->T("PSP Memory Stick"))); - systemSettings->Add(new CheckBox(&g_Config.bMemStickInserted, sy->T("Memory Stick inserted"))); - systemSettings->Add(new PopupSliderChoice(&g_Config.iMemStickSizeGB, 1, 32, sy->T("Change Memory Stick Size", "Memory Stick Size (GB)"), screenManager(), "GB")); - #if PPSSPP_PLATFORM(ANDROID) memstickDisplay_ = g_Config.memStickDirectory.ToVisualString(); auto memstickPath = systemSettings->Add(new ChoiceWithValueDisplay(&memstickDisplay_, sy->T("Change Memory Stick folder", "Memory Stick folder"), (const char *)nullptr)); @@ -955,6 +952,9 @@ void GameSettingsScreen::CreateViews() { } } #endif + systemSettings->Add(new CheckBox(&g_Config.bMemStickInserted, sy->T("Memory Stick inserted"))); + systemSettings->Add(new PopupSliderChoice(&g_Config.iMemStickSizeGB, 1, 32, sy->T("Change Memory Stick Size", "Memory Stick Size (GB)"), screenManager(), "GB")); + systemSettings->Add(new ItemHeader(sy->T("General"))); #if PPSSPP_PLATFORM(ANDROID) @@ -1097,7 +1097,7 @@ UI::EventReturn GameSettingsScreen::OnJitAffectingSetting(UI::EventParams &e) { } UI::EventReturn GameSettingsScreen::OnChangeMemStickDir(UI::EventParams &e) { - screenManager()->push(new MemStickScreen()); + screenManager()->push(new MemStickScreen(false)); return UI::EVENT_DONE; } @@ -1257,6 +1257,12 @@ void GameSettingsScreen::sendMessage(const char *message, const char *value) { } } +void GameSettingsScreen::dialogFinished(const Screen *dialog, DialogResult result) { + if (result == DialogResult::DR_OK) { + RecreateViews(); + } +} + void GameSettingsScreen::CallbackMemstickFolder(bool yes) { auto sy = GetI18NCategory("System"); diff --git a/UI/GameSettingsScreen.h b/UI/GameSettingsScreen.h index ebc5d59f88..09127656ec 100644 --- a/UI/GameSettingsScreen.h +++ b/UI/GameSettingsScreen.h @@ -43,6 +43,7 @@ protected: void CallbackInflightFrames(bool yes); void CallbackMemstickFolder(bool yes); bool UseVerticalLayout() const; + void dialogFinished(const Screen *dialog, DialogResult result) override; private: void TriggerRestart(const char *why); diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index 6f7872289a..4009dc7c42 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -816,7 +816,7 @@ void GameBrowser::Refresh() { } // Show a button to toggle pinning at the very end. - if (browseFlags_ & BrowseFlags::PIN) { + if ((browseFlags_ & BrowseFlags::PIN) && !path_.GetPath().empty()) { std::string caption = IsCurrentPathPinned() ? "-" : "+"; if (!*gridStyle_) { caption = IsCurrentPathPinned() ? mm->T("UnpinPath", "Unpin") : mm->T("PinPath", "Pin"); @@ -825,6 +825,10 @@ void GameBrowser::Refresh() { OnClick.Handle(this, &GameBrowser::PinToggleClick); } + if (path_.GetPath().empty()) { + Add(new TextView(mm->T("UseBrowseOrLoad", "Use Browse to choose a folder, or Load to choose a file."))); + } + if (!lastText_.empty() && gameButtons.empty()) { Add(new Spacer()); Add(new Choice(lastText_, new UI::LinearLayoutParams(UI::WRAP_CONTENT, UI::WRAP_CONTENT)))->OnClick.Handle(this, &GameBrowser::LastClick); @@ -978,7 +982,8 @@ void MainScreen::CreateViews() { tabHolder_->SetClip(true); bool showRecent = g_Config.iMaxRecent > 0; - bool hasStorageAccess = System_GetPermissionStatus(SYSTEM_PERMISSION_STORAGE) == PERMISSION_STATUS_GRANTED; + bool hasStorageAccess = !System_GetPropertyBool(SYSPROP_SUPPORTS_PERMISSIONS) || + System_GetPermissionStatus(SYSTEM_PERMISSION_STORAGE) == PERMISSION_STATUS_GRANTED; bool storageIsTemporary = IsTempPath(GetSysDirectory(DIRECTORY_SAVEDATA)) && !confirmedTemporary_; if (showRecent && !hasStorageAccess) { showRecent = !g_Config.recentIsos.empty(); diff --git a/UI/MemStickScreen.cpp b/UI/MemStickScreen.cpp index 6759457f39..318195738e 100644 --- a/UI/MemStickScreen.cpp +++ b/UI/MemStickScreen.cpp @@ -25,23 +25,84 @@ #include "Common/UI/ViewGroup.h" #include "Common/StringUtils.h" -#include "Common/File/FileUtil.h" -#include "Common/File/Path.h" #include "Common/System/System.h" #include "Common/System/NativeApp.h" -#include "Common/Data/Text/I18n.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" -#include "Core/Config.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; + } +} -MemStickScreen::~MemStickScreen() { } +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; @@ -51,49 +112,91 @@ void MemStickScreen::CreateViews() { Margins actionMenuMargins(15, 15, 15, 0); - root_ = new AnchorLayout(); + root_ = new LinearLayout(ORIENT_HORIZONTAL); - ViewGroup *columns = new LinearLayout(ORIENT_HORIZONTAL, new AnchorLayoutParams(FILL_PARENT, FILL_PARENT)); - root_->Add(columns); + 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 AnchorLayout(new LinearLayoutParams(1.0)); + ViewGroup *leftColumn = new LinearLayoutList(ORIENT_VERTICAL); ViewGroup *rightColumnItems = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(300, FILL_PARENT, actionMenuMargins)); - columns->Add(leftColumn); - columns->Add(rightColumnItems); + root_->Add(spacerColumn); + root_->Add(leftColumnScroll); + root_->Add(rightColumnItems); - Path path = g_Config.memStickDirectory; - int64_t freeSpaceAtMemStick = -1; + 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 (Android_IsContentUri(path.ToString())) { - freeSpaceAtMemStick = Android_GetFreeSpaceByContentUri(path.ToString()); - } else { - freeSpaceAtMemStick = Android_GetFreeSpaceByFilePath(path.ToString()); + 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("ChooseFolderDesc", "* Data will stay even if you uninstall PPSSPP.\n* Data can be shared with PPSSPP Gold\n* Easy USB access"), ALIGN_LEFT, false)); } #endif - int leftSide = 100; - settingInfo_ = new SettingInfoMessage(ALIGN_CENTER | FLAG_WRAP_TEXT, new AnchorLayoutParams(dp_xres - leftSide - 40.0f, WRAP_CONTENT, leftSide, dp_yres - 80.0f - 40.0f, NONE, NONE)); - settingInfo_->SetBottomCutoff(dp_yres - 200.0f); + leftColumn->Add(new Choice(iz->T("Create or Choose a PSP folder")))->OnClick.Handle(this, &MemStickScreen::OnBrowse); + leftColumn->Add(new TextView(iz->T("ChooseFolderDesc", "* Data will stay even if you uninstall PPSSPP.\n* Data can be shared with PPSSPP Gold\n* Easy USB access"), ALIGN_LEFT, false)); - root_->Add(settingInfo_); + leftColumn->Add(new Choice(iz->T("Use App Private Directory")))->OnClick.Handle(this, &MemStickScreen::OnUseInternalStorage); + leftColumn->Add(new TextView(iz->T("InternalStorageDesc", "* Warning! Data will be deleted if you uninstall PPSSPP!\n* Data cannot be shared with PPSSPP Gold\n* USB access through Android/data/org.ppsspp.ppsspp/files"), ALIGN_LEFT, false)); - leftColumn->Add(new TextView(iz->T("Memory Stick Storage"), ALIGN_LEFT, false, new AnchorLayoutParams(10, 10, NONE, NONE))); - leftColumn->Add(new TextView(iz->T("MemoryStickDescription", "Choose where your PSP memory stick data (savegames, etc) is stored"), ALIGN_LEFT, false, new AnchorLayoutParams(10, 50, NONE, NONE))); - leftColumn->Add(new TextView(g_Config.memStickDirectory.ToVisualString(), ALIGN_LEFT, false, new AnchorLayoutParams(10, 140, NONE, NONE))); + leftColumn->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 12.0f, 0.0f))); - std::string freeSpaceText = "Free space: N/A"; - if (freeSpaceAtMemStick >= 0) { - freeSpaceText = StringFromFormat("free space: %lld MB", freeSpaceAtMemStick / (1024 * 1024)); + if (!initialSetup_) { + rightColumnItems->Add(new Choice(di->T("Back")))->OnClick.Handle(this, &UIScreen::OnBack); } - leftColumn->Add(new TextView(freeSpaceText, ALIGN_LEFT, false, new AnchorLayoutParams(10, 240, NONE, NONE))); + INFO_LOG(SYSTEM, "MemStickScreen: initialSetup=%d", (int)initialSetup_); +} - rightColumnItems->Add(new Choice(iz->T("Browse")))->OnClick.Handle(this, &MemStickScreen::OnBrowse); - rightColumnItems->Add(new Choice(di->T("Back")))->OnClick.Handle(this, &UIScreen::OnOK); +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) { - auto sy = GetI18NCategory("System"); System_SendMessage("browse_folder", ""); return UI::EVENT_DONE; } @@ -107,49 +210,310 @@ void MemStickScreen::sendMessage(const char *message, const char *value) { std::string filename; filename = value; INFO_LOG(SYSTEM, "Got folder: '%s'", filename.c_str()); - pendingMemStickFolder_ = Path(filename); - CallbackMemStickFolder(true); - } - } -} -void MemStickScreen::CallbackMemStickFolder(bool yes) { - auto sy = GetI18NCategory("System"); + // Browse finished. Let's pop up the confirmation dialog. + Path pendingMemStickFolder = Path(filename); - if (yes) { - Path memStickDirFile = g_Config.internalDataDirectory / "memstick_dir.txt"; - Path testWriteFile = pendingMemStickFolder_ / ".write_verify_file"; - - // Doesn't already exist, create. - // Should this ever happen? - if (pendingMemStickFolder_.Type() == PathType::NATIVE) { - if (!File::Exists(pendingMemStickFolder_)) { - File::CreateFullPath(pendingMemStickFolder_); - } - if (!File::WriteDataToFile(true, "1", 1, testWriteFile)) { - settingInfo_->Show(sy->T("ChangingMemstickPathInvalid", "That path couldn't be used to save Memory Stick files."), nullptr); + if (pendingMemStickFolder == g_Config.memStickDirectory) { + auto iz = GetI18NCategory("MemStick"); +#if PPSSPP_PLATFORM(ANDROID) + SystemToast(iz->T("That's the folder being used!")); +#endif return; } - 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, barring wacky race conditions like the user being connected - // by USB and deleting it. + + bool existingFiles = FolderSeemsToBeUsed(pendingMemStickFolder); + screenManager()->push(new ConfirmMemstickMoveScreen(pendingMemStickFolder, initialSetup_)); } + } +} - // This doesn't need the storage API - this path is accessible the normal way. - std::string str = pendingMemStickFolder_.ToString(); - if (!File::WriteDataToFile(true, str.c_str(), (unsigned int)str.size(), memStickDirFile)) { - ERROR_LOG(SYSTEM, "Failed to write memstick path '%s' to '%s'", pendingMemStickFolder_.c_str(), memStickDirFile.c_str()); - // Not sure what to do if this file. +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(); - // Save so the settings, at least, are transferred. - g_Config.memStickDirectory = pendingMemStickFolder_; - g_Config.SetSearchPath(GetSysDirectory(DIRECTORY_SYSTEM)); - g_Config.UpdateIniLocation(); + if (!initialSetup_) { + // We restart the app here, to get the new settings. + System_SendMessage("graphics_restart", ""); + } - g_Config.Save("MemstickPathChanged"); - screenManager()->RecreateAllViews(); + if (g_Config.Save("MemstickPathChanged")) { + TriggerFinish(DialogResult::DR_OK); + } else { + error_ = iz->T("Failed to save config"); + RecreateViews(); } } diff --git a/UI/MemStickScreen.h b/UI/MemStickScreen.h index b0c4f55dee..2c248103f0 100644 --- a/UI/MemStickScreen.h +++ b/UI/MemStickScreen.h @@ -19,30 +19,93 @@ #include #include +#include #include "ppsspp_config.h" #include "Common/File/Path.h" -#include "UI/MiscScreens.h" #include "Common/UI/UIScreen.h" +#include "Common/Thread/Promise.h" + +#include "UI/MiscScreens.h" // MemStickScreen - let's you configure your memory stick directory. // Currently only useful for Android. class MemStickScreen : public UIDialogScreenWithBackground { public: - MemStickScreen() {} - virtual ~MemStickScreen(); + MemStickScreen(bool initialSetup); + ~MemStickScreen() {} std::string tag() const override { return "game"; } protected: void CreateViews() override; - void CallbackMemStickFolder(bool yes); + void sendMessage(const char *message, const char *value) override; + void dialogFinished(const Screen *dialog, DialogResult result) override; + void update() override; + void render() override { + // Simple anti-flicker due to delayed finish. + if (!done_) { + // render as usual. + UIDialogScreenWithBackground::render(); + } else { + // no render. black frame insertion is better than flicker. + } + } private: // Event handlers UI::EventReturn OnBrowse(UI::EventParams &e); - Path pendingMemStickFolder_; - SettingInfoMessage *settingInfo_; + UI::EventReturn OnUseInternalStorage(UI::EventParams ¶ms); + UI::EventReturn OnUseStorageRoot(UI::EventParams ¶ms); + + SettingInfoMessage *settingInfo_ = nullptr; + + bool initialSetup_; + bool done_ = false; +}; + +class ProgressReporter { +public: + void Set(std::string value) { + std::lock_guard guard(mutex_); + progress_ = value; + } + + std::string Get() { + std::lock_guard guard(mutex_); + return progress_; + } + +private: + std::string progress_; + std::mutex mutex_; +}; + +class ConfirmMemstickMoveScreen : public UIDialogScreenWithBackground { +public: + ConfirmMemstickMoveScreen(Path newMemstickFolder, bool initialSetup); + ~ConfirmMemstickMoveScreen(); +protected: + void update() override; + void CreateViews() override; + +private: + UI::EventReturn OnMoveDataClick(UI::EventParams ¶ms); + void FinishFolderMove(); + + UI::EventReturn OnConfirm(UI::EventParams ¶ms); + + Path newMemstickFolder_; + bool existingFilesInNewFolder_; + bool moveData_ = true; + bool initialSetup_; + + ProgressReporter progressReporter_; + UI::TextView *progressView_ = nullptr; + + Promise *moveDataTask_ = nullptr; + + std::string error_; }; diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 5fb7f3a8de..fd11d1ca5f 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -108,6 +108,7 @@ #include "UI/GPUDriverTestScreen.h" #include "UI/HostTypes.h" #include "UI/MiscScreens.h" +#include "UI/MemStickScreen.h" #include "UI/OnScreenDisplay.h" #include "UI/RemoteISOScreen.h" #include "UI/TiltEventProcessor.h" @@ -321,7 +322,9 @@ static bool CheckFontIsUsable(const wchar_t *fontFace) { } #endif -static void PostLoadConfig() { +bool CreateDirectoriesAndroid(); + +void PostLoadConfig() { // On Windows, we deal with currentDirectory in InitSysDirectories(). #if !PPSSPP_PLATFORM(WINDOWS) if (g_Config.currentDirectory.empty()) { @@ -339,12 +342,13 @@ static void PostLoadConfig() { else i18nrepo.LoadIni(g_Config.sLanguageIni, langOverridePath); - g_threadManager.Init(cpu_info.num_cores, cpu_info.logical_cpu_count); +#if PPSSPP_PLATFORM(ANDROID) + CreateDirectoriesAndroid(); +#endif } -static bool CreateDirectoriesAndroid() { - // On Android, create a PSP directory tree in the external_dir, - // to hopefully reduce confusion a bit. +bool CreateDirectoriesAndroid() { + // TODO: We should probably simply use this as the shared function to create memstick directories. Path pspDir = g_Config.memStickDirectory; if (pspDir.GetFilename() != "PSP") { @@ -436,7 +440,7 @@ static void ClearFailedGPUBackends() { // We've successfully started graphics without crashing, hurray. // In case they update drivers and have totally different problems much later, clear the failed list. g_Config.sFailedGPUBackends.clear(); - if (System_GetPropertyBool(SYSPROP_SUPPORTS_PERMISSIONS)) { + if (System_GetPropertyBool(SYSPROP_SUPPORTS_PERMISSIONS) || System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE)) { File::Delete(GetSysDirectory(DIRECTORY_APP_CACHE) / "FailedGraphicsBackends.txt"); } else { g_Config.Save("clearFailedGPUBackends"); @@ -449,6 +453,8 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch ShaderTranslationInit(); InitFastMath(cpu_info.bNEON); + g_threadManager.Init(cpu_info.num_cores, cpu_info.logical_cpu_count); + SetupAudioFormats(); g_Discord.SetPresenceMenu(); @@ -504,22 +510,22 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch // is no longer the plain root of external storage, but it's an app specific directory // on external storage (g_extFilesDir). if (System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE)) { - g_Config.defaultCurrentDirectory = Path(g_extFilesDir); + // There's no sensible default directory. Let the user browse for files. + g_Config.defaultCurrentDirectory.clear(); } else { - // Maybe there should be an option to use internal memory instead, but I think - // that for most people, using external memory (SDCard/USB Storage) makes the - // most sense. + g_Config.memStickDirectory = Path(external_dir); g_Config.defaultCurrentDirectory = Path(external_dir); + CreateDirectoriesAndroid(); } // Might also add an option to move it to internal / non-visible storage, but there's // little point, really. - g_Config.memStickDirectory = Path(external_dir); g_Config.flash0Directory = Path(external_dir) / "flash0"; Path memstickDirFile = g_Config.internalDataDirectory / "memstick_dir.txt"; if (File::Exists(memstickDirFile)) { + INFO_LOG(SYSTEM, "Reading '%s' to find memstick dir.", memstickDirFile.c_str()); std::string memstickDir; if (File::ReadFileToString(true, memstickDirFile, memstickDir)) { Path memstickPath(memstickDir); @@ -529,15 +535,14 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch } else { ERROR_LOG(SYSTEM, "Couldn't read directory '%s' specified by memstick_dir.txt.", memstickDir.c_str()); if (System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE)) { - // TODO: Gotta resolve this somehow... - // I think we wanna pop up the memstick dir chooser before any other screen in this case. - // For now we just choose a default. - g_Config.memStickDirectory = g_Config.defaultCurrentDirectory; + // Ask the user to configure a memstick directory. + INFO_LOG(SYSTEM, "Asking the user."); + g_Config.memStickDirectory.clear(); } } } } else { - INFO_LOG(SYSTEM, "No memstick directory file found. Using '%s'", memstickDirFile.c_str()); + INFO_LOG(SYSTEM, "No memstick directory file found (tried to open '%s')", memstickDirFile.c_str()); } #elif PPSSPP_PLATFORM(IOS) @@ -572,8 +577,9 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch DiskCachingFileLoaderCache::SetCacheDir(g_Config.appCacheDirectory); } - if (!LogManager::GetInstance()) + if (!LogManager::GetInstance()) { LogManager::Init(&g_Config.bEnableLogging); + } #if !PPSSPP_PLATFORM(WINDOWS) g_Config.SetSearchPath(GetSysDirectory(DIRECTORY_SYSTEM)); @@ -585,11 +591,6 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch LogManager *logman = LogManager::GetInstance(); -#if PPSSPP_PLATFORM(ANDROID) - // On early versions of Android we don't need to ask permission. - CreateDirectoriesAndroid(); -#endif - const char *fileToLog = 0; Path stateToLoad; @@ -785,8 +786,13 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch }); } + INFO_LOG(SYSTEM, "ScreenManager!"); screenManager = new ScreenManager(); - if (gotoGameSettings) { + if (g_Config.memStickDirectory.empty()) { + INFO_LOG(SYSTEM, "No memstick directory! Asking for one to be configured."); + screenManager->switchScreen(new LogoScreen(false)); + screenManager->push(new MemStickScreen(true)); + } else if (gotoGameSettings) { screenManager->switchScreen(new LogoScreen(true)); } else if (gotoTouchScreenTest) { screenManager->switchScreen(new MainScreen()); @@ -832,7 +838,6 @@ static UI::Style MakeStyle(uint32_t fg, uint32_t bg) { UI::Style s; s.background = UI::Drawable(bg); s.fgColor = fg; - return s; } @@ -1477,6 +1482,8 @@ void NativeShutdown() { logger = nullptr; } + g_threadManager.Teardown(); + // Previously we did exit() here on Android but that makes it hard to do things like restart on backend change. // I think we handle most globals correctly or correct-enough now. } diff --git a/Windows/W32Util/Misc.cpp b/Windows/W32Util/Misc.cpp index d646302986..4f0ffb982f 100644 --- a/Windows/W32Util/Misc.cpp +++ b/Windows/W32Util/Misc.cpp @@ -53,24 +53,6 @@ namespace W32Util MoveWindow(hwnd, x, y, width, height, FALSE); } - void NiceSizeFormat(size_t size, char *out) - { - const char *sizes[] = {"B","KB","MB","GB","TB","PB","EB"}; - int s = 0; - int frac = 0; - while (size>=1024) - { - s++; - frac = (int)size & 1023; - size /= 1024; - } - float f = (float)size + ((float)frac / 1024.0f); - if (s==0) - sprintf(out, "%d B", (int)size); - else - sprintf(out, "%3.1f %s", f, sizes[s]); - } - BOOL CopyTextToClipboard(HWND hwnd, const char *text) { std::wstring wtext = ConvertUTF8ToWString(text); return CopyTextToClipboard(hwnd, wtext); diff --git a/Windows/W32Util/Misc.h b/Windows/W32Util/Misc.h index fa447de78d..0cddf63df9 100644 --- a/Windows/W32Util/Misc.h +++ b/Windows/W32Util/Misc.h @@ -6,8 +6,6 @@ namespace W32Util { void CenterWindow(HWND hwnd); - HBITMAP CreateBitmapFromARGB(HWND someHwnd, DWORD *image, int w, int h); - void NiceSizeFormat(size_t size, char *out); BOOL CopyTextToClipboard(HWND hwnd, const char *text); BOOL CopyTextToClipboard(HWND hwnd, const std::wstring &wtext); void MakeTopMost(HWND hwnd, bool topMost); diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 002a779cc4..833ed5827b 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -19,8 +19,8 @@ - - + + @@ -45,7 +45,8 @@ android:logo="@drawable/ic_banner" android:isGame="true" android:banner="@drawable/tv_banner" - android:requestLegacyExternalStorage="true"> + android:requestLegacyExternalStorage="true" + android:preserveLegacyExternalStorage="true"> = 14000000) { @@ -61,7 +61,7 @@ android { new File("versioncode.txt").write(androidGitVersion.code().toString()) minSdkVersion 9 - targetSdkVersion 29 + targetSdkVersion 30 if (project.hasProperty("ANDROID_VERSION_CODE") && project.hasProperty("ANDROID_VERSION_NAME")) { versionCode ANDROID_VERSION_CODE versionName ANDROID_VERSION_NAME diff --git a/android/jni/AndroidContentURI.h b/android/jni/AndroidContentURI.h index f9837a5ba4..92e77afb62 100644 --- a/android/jni/AndroidContentURI.h +++ b/android/jni/AndroidContentURI.h @@ -112,8 +112,16 @@ public: } // Only goes downwards in hierarchies. No ".." will ever be generated. - std::string PathTo(const AndroidContentURI &other) const { - return other.FilePath().substr(FilePath().size() + 1); + bool ComputePathTo(const AndroidContentURI &other, std::string &path) const { + size_t offset = FilePath().size() + 1; + std::string otherFilePath = other.FilePath(); + if (offset >= otherFilePath.size()) { + ERROR_LOG(SYSTEM, "Bad call to PathTo. '%s' -> '%s'", FilePath().c_str(), other.FilePath().c_str()); + return false; + } + + path = other.FilePath().substr(FilePath().size() + 1); + return true; } std::string GetFileExtension() const { diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 0cfe1f4e19..795ec23f4e 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -97,9 +97,6 @@ struct JNIEnv {}; bool useCPUThread = true; -// We'll turn this on when we target Android 12. -bool useScopedStorageIfRequired = false; - enum class EmuThreadState { DISABLED, START_REQUESTED, @@ -131,7 +128,8 @@ std::string langRegion; std::string mogaVersion; std::string boardName; -std::string g_extFilesDir; +std::string g_externalDir; // Original external dir (root of Android storage). +std::string g_extFilesDir; // App private external dir. std::vector g_additionalStorageDirs; @@ -433,7 +431,15 @@ float System_GetPropertyFloat(SystemProperty prop) { bool System_GetPropertyBool(SystemProperty prop) { switch (prop) { case SYSPROP_SUPPORTS_PERMISSIONS: - return androidVersion >= 23; // 6.0 Marshmallow introduced run time permissions. + if (androidVersion < 23) { + // 6.0 Marshmallow introduced run time permissions. + return false; + } else { + // It gets a bit complicated here. If scoped storage enforcement is on, + // we also don't need to request permissions. We'll have the access we request + // on a per-folder basis. + return !System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE); + } case SYSPROP_SUPPORTS_SUSTAINED_PERF_MODE: return sustainedPerfSupported; // 7.0 introduced sustained performance mode as an optional feature. case SYSPROP_HAS_ADDITIONAL_STORAGE: @@ -448,6 +454,8 @@ bool System_GetPropertyBool(SystemProperty prop) { return androidVersion >= 19; // when ACTION_OPEN_DOCUMENT was added case SYSPROP_HAS_FOLDER_BROWSER: // Uses OPEN_DOCUMENT_TREE to let you select a folder. + // Doesn't actually mean it's usable though, in many early versions of Android + // this dialog is complete garbage and only lets you select subfolders of the Downloads folder. return androidVersion >= 21; // when ACTION_OPEN_DOCUMENT_TREE was added case SYSPROP_APP_GOLD: #ifdef GOLD @@ -458,8 +466,24 @@ bool System_GetPropertyBool(SystemProperty prop) { case SYSPROP_CAN_JIT: return true; case SYSPROP_ANDROID_SCOPED_STORAGE: - if (useScopedStorageIfRequired && androidVersion >= 28) - return true; + // We turn this on for Android 30+ (11) now that when we target Android 11+. + // Along with adding: + // android:preserveLegacyExternalStorage="true" + // To the already requested: + // android:requestLegacyExternalStorage="true" + // + // This will cause Android 11+ to still behave like Android 10 until the app + // is manually uninstalled. We can detect this state with + // Android_IsExternalStoragePreservedLegacy(), but most of the app will just see + // that scoped storage enforcement is disabled in this case. + if (androidVersion >= 30) { + // Here we do a check to see if we ended up in the preserveLegacyExternalStorage path. + // That won't last if the user uninstalls/reinstalls though, but would preserve the user + // experience for simple upgrades so maybe let's support it. + return !Android_IsExternalStoragePreservedLegacy(); + } else { + return false; + } default: return false; } @@ -614,6 +638,7 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_init std::string additionalStorageDirsString = GetJavaString(env, jadditionalStorageDirs); std::string externalFilesDir = GetJavaString(env, jexternalFilesDir); + g_externalDir = externalStorageDir; g_extFilesDir = externalFilesDir; if (!additionalStorageDirsString.empty()) { diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index 761cc156ab..f7786ef707 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -5,6 +5,7 @@ import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.Environment; import android.os.Looper; import android.os.ParcelFileDescriptor; import android.util.Log; diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 7c399f0f9c..f2ef8cdb88 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -607,8 +607,14 @@ static bool TestPath() { EXPECT_EQ_STR(Path("C:\\Yo\\Lo").GetDirectory(), std::string("C:/Yo")); EXPECT_EQ_STR(Path("C:\\Yo\\Lo").GetFilename(), std::string("Lo")); - EXPECT_EQ_STR(Path("/a/b").PathTo(Path("/a/b/c/d/e")), std::string("c/d/e")); - EXPECT_EQ_STR(Path("/").PathTo(Path("/home/foo/bar")), std::string("home/foo/bar")); + std::string computedPath; + + EXPECT_TRUE(Path("/a/b").ComputePathTo(Path("/a/b/c/d/e"), computedPath)); + + EXPECT_EQ_STR(computedPath, std::string("c/d/e")); + + EXPECT_TRUE(Path("/").ComputePathTo(Path("/home/foo/bar"), computedPath)); + EXPECT_EQ_STR(computedPath, std::string("home/foo/bar")); return true; } @@ -639,7 +645,8 @@ static bool TestAndroidContentURI() { EXPECT_EQ_STR(fileURI.ToString(), std::string(directoryURIString)); - std::string diff = dirURI.PathTo(fileURI); + std::string diff; + EXPECT_TRUE(dirURI.ComputePathTo(fileURI, diff)); EXPECT_EQ_STR(diff, std::string("Tekken 6.iso")); return true;