diff --git a/Common/Data/Encoding/Utf8.cpp b/Common/Data/Encoding/Utf8.cpp index 4e7a2c7a63..737b049bcd 100644 --- a/Common/Data/Encoding/Utf8.cpp +++ b/Common/Data/Encoding/Utf8.cpp @@ -387,7 +387,13 @@ std::string NormalizeForSearch(std::string_view input) { while (index < size) { uint32_t codepoint = u8_nextchar(input.data(), &index, size); + // Skip spaces and control characters. + if (codepoint <= 0x20) { + continue; + } + // 1. Convert Fullwidth Roman/Numbers to ASCII + // These are common in Japanese game names. // Range: U+FF01 (!) to U+FF5E (~) if (codepoint >= 0xFF01 && codepoint <= 0xFF5E) { codepoint -= 0xFEE0; diff --git a/Common/Log/LogManager.h b/Common/Log/LogManager.h index 5e7e6565c0..59bb1b5cdc 100644 --- a/Common/Log/LogManager.h +++ b/Common/Log/LogManager.h @@ -123,6 +123,12 @@ public: } } + void SetAllLogEnable(bool enable) { + for (int i = 0; i < (int)Log::NUMBER_OF_LOGS; ++i) { + g_log[i].enabled = enable; + } + } + void SetEnabled(Log type, bool enable) { g_log[(size_t)type].enabled = enable; } diff --git a/Common/System/Request.cpp b/Common/System/Request.cpp index 833252979e..4d24ac1140 100644 --- a/Common/System/Request.cpp +++ b/Common/System/Request.cpp @@ -114,7 +114,7 @@ void RequestManager::PostSystemSuccess(int requestId, std::string_view responseS callbackMap_.erase(iter); } -void RequestManager::PostSystemFailure(int requestId) { +void RequestManager::PostSystemFailure(int requestId, int responseValue) { std::lock_guard guard(callbackMutex_); auto iter = callbackMap_.find(requestId); if (iter == callbackMap_.end()) { @@ -127,6 +127,7 @@ void RequestManager::PostSystemFailure(int requestId) { std::lock_guard responseGuard(responseMutex_); PendingFailure response; response.failedCallback = iter->second.failedCallback; + response.responseValue = responseValue; pendingFailures_.push_back(response); callbackMap_.erase(iter); } @@ -141,7 +142,7 @@ void RequestManager::ProcessRequests() { pendingSuccesses_.clear(); for (auto &iter : pendingFailures_) { if (iter.failedCallback) { - iter.failedCallback(); + iter.failedCallback(iter.responseValue); } } pendingFailures_.clear(); diff --git a/Common/System/Request.h b/Common/System/Request.h index 281e0bc0e9..c860ad132b 100644 --- a/Common/System/Request.h +++ b/Common/System/Request.h @@ -12,13 +12,16 @@ class Path; typedef std::function RequestCallback; -typedef std::function RequestFailedCallback; +typedef std::function RequestFailedCallback; typedef int RequesterToken; #define NO_REQUESTER_TOKEN -1 #define NON_EPHEMERAL_TOKEN -2 +// Used on Android for file browsing requests and similar. +#define NO_ACTIVITY_AVAILABLE 1 + // Platforms often have to process requests asynchronously, on wildly different threads, // and then somehow pass a response back to the main thread (especially Android...) // This acts as bridge and buffer. @@ -35,7 +38,7 @@ public: // Called by the platform implementation, when it's finished with a request. void PostSystemSuccess(int requestId, std::string_view responseString, int responseValue = 0); - void PostSystemFailure(int requestId); + void PostSystemFailure(int requestId, int responseValue = 0); // This must be called every frame from the beginning of NativeFrame(). // This will call the callback of any finished requests. @@ -69,6 +72,7 @@ private: struct PendingFailure { RequestFailedCallback failedCallback; + int responseValue; // can have error codes for example. }; // Let's start at 10 to get a recognizably valid ID in logs. diff --git a/Core/Dialog/PSPOskDialog.cpp b/Core/Dialog/PSPOskDialog.cpp index c1e9ef5716..bdd0e86645 100755 --- a/Core/Dialog/PSPOskDialog.cpp +++ b/Core/Dialog/PSPOskDialog.cpp @@ -810,7 +810,7 @@ int PSPOskDialog::NativeKeyboard() { // There's already ConvertUCS2ToUTF8 in this file. Should we use that instead of the global ones? System_InputBoxGetString(NON_EPHEMERAL_TOKEN, ::ConvertUCS2ToUTF8(titleText), ::ConvertUCS2ToUTF8(defaultText), false, - [&](const std::string &value, int) { + [this](const std::string &value, int) { // Success callback std::lock_guard guard(nativeMutex_); if (nativeStatus_ != PSPOskNativeStatus::WAITING) { @@ -819,7 +819,7 @@ int PSPOskDialog::NativeKeyboard() { nativeValue_ = value; nativeStatus_ = PSPOskNativeStatus::SUCCESS; }, - [&]() { + [this](int responseValue) { // Failure callback std::lock_guard guard(nativeMutex_); if (nativeStatus_ != PSPOskNativeStatus::WAITING) { diff --git a/Core/KeyMap.cpp b/Core/KeyMap.cpp index 2fe3842ed6..7e3140948f 100644 --- a/Core/KeyMap.cpp +++ b/Core/KeyMap.cpp @@ -212,6 +212,7 @@ void UpdateNativeMenuKeys() { RemoveKeyboardLetterKeys(tabRight); RemoveKeyboardLetterKeys(confirmKeys); RemoveKeyboardLetterKeys(cancelKeys); + RemoveKeyboardLetterKeys(infoKeys); SetDPadKeys(upKeys, downKeys, leftKeys, rightKeys); SetConfirmCancelKeys(confirmKeys, cancelKeys); diff --git a/UI/IAPScreen.cpp b/UI/IAPScreen.cpp index b52aa600f8..b0ce6faf48 100644 --- a/UI/IAPScreen.cpp +++ b/UI/IAPScreen.cpp @@ -67,7 +67,7 @@ void IAPScreen::CreateSettingsViews(UI::ViewGroup *rightColumnItems) { auto di = GetI18NCategory(I18NCat::DIALOG); g_OSD.Show(OSDType::MESSAGE_SUCCESS, di->T("GoldThankYou", "Thank you for supporting the PPSSPP project!"), 3.0f); RecreateViews(); - }, []() { + }, [](int responseValue) { WARN_LOG(Log::System, "Purchase failed or cancelled!"); }); } else { @@ -93,8 +93,8 @@ void IAPScreen::CreateSettingsViews(UI::ViewGroup *rightColumnItems) { System_IAPRestorePurchases(requesterToken, [this](const char *responseString, int) { INFO_LOG(Log::System, "Successfully restored purchases!"); RecreateViews(); - }, []() { - WARN_LOG(Log::System, "Failed restoring purchases"); + }, [](int responseValue) { + WARN_LOG(Log::System, "Failed restoring purchases: %d", responseValue); }); }); rightColumnItems->Add(restorePurchases); diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index 568e7ec1e3..bac0ad1661 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -515,18 +515,13 @@ void MainScreen::CreateViews() { bool MainScreen::key(const KeyInput &key) { if (key.flags & KeyInputFlags::DOWN) { - if (key.keyCode == NKCODE_CTRL_LEFT || key.keyCode == NKCODE_CTRL_RIGHT) - searchKeyModifier_ = true; - if (key.keyCode == NKCODE_F && searchKeyModifier_ && System_GetPropertyBool(SYSPROP_HAS_TEXT_INPUT_DIALOG)) { + if (key.keyCode == NKCODE_F && (key.flags & KeyInputFlags::MOD_CTRL) && System_GetPropertyBool(SYSPROP_HAS_TEXT_INPUT_DIALOG)) { auto se = GetI18NCategory(I18NCat::SEARCH); System_InputBoxGetString(GetRequesterToken(), se->T("Search term"), searchFilter_, false, [&](const std::string &value, int) { searchFilter_ = StripSpaces(value); searchChanged_ = true; }); } - } else if (key.flags & KeyInputFlags::UP) { - if (key.keyCode == NKCODE_CTRL_LEFT || key.keyCode == NKCODE_CTRL_RIGHT) - searchKeyModifier_ = false; } bool retval = UIBaseScreen::key(key); diff --git a/UI/MainScreen.h b/UI/MainScreen.h index 5c6f29f1e1..3181606103 100644 --- a/UI/MainScreen.h +++ b/UI/MainScreen.h @@ -92,7 +92,6 @@ protected: bool lockBackgroundAudio_ = false; bool lastVertical_ = false; bool confirmedTemporary_ = false; - bool searchKeyModifier_ = false; bool searchChanged_ = false; std::string searchFilter_; diff --git a/UI/MemStickScreen.cpp b/UI/MemStickScreen.cpp index 28a89a5b88..a5a4598ec4 100644 --- a/UI/MemStickScreen.cpp +++ b/UI/MemStickScreen.cpp @@ -190,7 +190,7 @@ void MemStickScreen::CreateViews() { 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)); + ViewGroup *rightColumnItems = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(250, 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. @@ -271,7 +271,7 @@ void MemStickScreen::CreateViews() { rightColumnItems->Add(new UI::Choice(di->T("Back")))->OnClick.Handle(this, &UIScreen::OnBack); } if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) != DEVICE_TYPE_TV) { - rightColumnItems->Add(new UI::Choice(ms->T("WhatsThis", "What's this?")))->OnClick.Handle(this, &MemStickScreen::OnHelp); + rightColumnItems->Add(new UI::Choice(ms->T("WhatsThis", "What's this?"), ImageID("I_LINK_OUT_QUESTION")))->OnClick.Handle(this, &MemStickScreen::OnHelp); } INFO_LOG(Log::System, "MemStickScreen: initialSetup=%d", (int)initialSetup_); @@ -429,8 +429,8 @@ void MemStickScreen::Browse(UI::EventParams ¶ms) { return; } screenManager()->push(new ConfirmMemstickMoveScreen(pendingMemStickFolder, initialSetup_)); - }, [this]() { - WARN_LOG(Log::System, "Folder browse cancelled"); + }, [this](int responseValue) { + WARN_LOG(Log::System, "Folder browse cancelled: %d", responseValue); }); } diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index b1dbd78c63..53b838e165 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -390,6 +390,13 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch #endif #if PPSSPP_PLATFORM(ANDROID) +#ifdef _DEBUG + g_logManager.SetAllLogLevels(LogLevel::LINFO); + g_logManager.SetAllLogEnable(true); + g_logManager.SetOutputsEnabled(LogOutput::Stdio); + INFO_LOG(Log::System, "Logging test"); +#endif + // In Android 12 with scoped storage, due to the above, the external directory // is no longer the plain root of external storage, but it's an app specific directory // on external storage (g_extFilesDir). diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 8396f0860d..c826a2b6d8 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -1197,9 +1197,9 @@ extern "C" void JNICALL Java_org_ppsspp_ppsspp_NativeApp_sendRequestResult(JNIEn std::string value = jvalue ? GetJavaString(env, jvalue) : "(no value)"; INFO_LOG(Log::System, "Received result of request %d from Java: %d: %d '%s'", jrequestID, (int)result, jintValue, value.c_str()); if (result) { - g_requestManager.PostSystemSuccess(jrequestID, value.c_str()); + g_requestManager.PostSystemSuccess(jrequestID, value.c_str(), jintValue); } else { - g_requestManager.PostSystemFailure(jrequestID); + g_requestManager.PostSystemFailure(jrequestID, jintValue); } } diff --git a/android/src/org/ppsspp/ppsspp/DocumentResultProxyActivity.java b/android/src/org/ppsspp/ppsspp/DocumentResultProxyActivity.java index 07e7e03bfc..cae1424c18 100644 --- a/android/src/org/ppsspp/ppsspp/DocumentResultProxyActivity.java +++ b/android/src/org/ppsspp/ppsspp/DocumentResultProxyActivity.java @@ -1,6 +1,7 @@ package org.ppsspp.ppsspp; import android.app.Activity; +import android.content.ActivityNotFoundException; import android.content.Intent; import android.net.Uri; import android.os.Bundle; @@ -14,6 +15,18 @@ import androidx.documentfile.provider.DocumentFile; public class DocumentResultProxyActivity extends AppCompatActivity { public static final String TAG = "PPSSPP"; + private void returnWithResult(int resultCode, int requestId, String resultPath) { + Intent returnIntent = new Intent(this, PpssppActivity.class); + returnIntent.putExtra("result_code", resultCode); + returnIntent.putExtra("request_id", requestId); + if (resultPath != null) { + returnIntent.putExtra("result_path", resultPath); + } + returnIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + startActivity(returnIntent); + finish(); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -29,7 +42,7 @@ public class DocumentResultProxyActivity extends AppCompatActivity { result -> { Log.i(TAG, "DocumentResultProxy: Packing return intent, requestId = " + requestId); - Intent returnIntent = new Intent(this, PpssppActivity.class); + String resultPath = null; if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { Uri uri = result.getData().getData(); if (uri != null) { @@ -48,25 +61,31 @@ public class DocumentResultProxyActivity extends AppCompatActivity { } catch (Exception e) { Log.w(TAG, "DocumentResultProxy: Exception getting permissions or DocumentFile: " + e); } - returnIntent.putExtra("result_path", uri.toString()); + resultPath = uri.toString(); } else { Log.w(TAG, "DocumentResultProxy: URI is null in result data"); } } - returnIntent.putExtra("result_code", result.getResultCode()); - returnIntent.putExtra("request_id", requestId); - - // This flag is key: it finds the existing instance of your main activity - returnIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); - startActivity(returnIntent); - finish(); + returnWithResult(result.getResultCode(), requestId, resultPath); } ); + // Only launch the picker if we are starting fresh. If we're being recreated (e.g. rotation), + // the ActivityResultRegistry will handle delivering the pending result to the launcher + // automatically, and we don't want to launch the picker a second time. if (savedInstanceState == null) { if (pickerIntent != null) { - launcher.launch(pickerIntent); + try { + // throw new ActivityNotFoundException(); // Use this for testing the fallback. + launcher.launch(pickerIntent); + } catch (ActivityNotFoundException e) { + NativeApp.reportException(e, "DocumentResultProxy: failed to launch picker intent, activity not found: " + pickerIntent.getAction()); + returnWithResult(NativeApp.RESULT_ERROR_ACTIVITY_NOT_FOUND, requestId, null); + } catch (Exception e) { + NativeApp.reportException(e, "DocumentResultProxy: failed to launch picker intent, other error: " + pickerIntent.getAction()); + returnWithResult(NativeApp.RESULT_ERROR_OTHER_ACTIVITY_ERROR, requestId, null); + } } else { Log.e(TAG, "DocumentResultProxy: No picker intent provided"); finish(); diff --git a/android/src/org/ppsspp/ppsspp/ImageResultProxyActivity.java b/android/src/org/ppsspp/ppsspp/ImageResultProxyActivity.java index 4973f83ff1..3a778fe67e 100644 --- a/android/src/org/ppsspp/ppsspp/ImageResultProxyActivity.java +++ b/android/src/org/ppsspp/ppsspp/ImageResultProxyActivity.java @@ -1,6 +1,7 @@ package org.ppsspp.ppsspp; import android.app.Activity; +import android.content.ActivityNotFoundException; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -110,6 +111,18 @@ public class ImageResultProxyActivity extends AppCompatActivity { } } + private void returnWithResult(int resultCode, int requestId, String resultPath) { + Intent returnIntent = new Intent(this, PpssppActivity.class); + returnIntent.putExtra("result_code", resultCode); + returnIntent.putExtra("request_id", requestId); + if (resultPath != null) { + returnIntent.putExtra("result_path", resultPath); + } + returnIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + startActivity(returnIntent); + finish(); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -125,29 +138,31 @@ public class ImageResultProxyActivity extends AppCompatActivity { result -> { Log.i(TAG, "Packing return intent, requestId = " + requestId); - Intent returnIntent = new Intent(this, PpssppActivity.class); + String localPath; if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { - String localPath = copyAndDownscaleToCache(result.getData().getData()); - Log.i(TAG, "Putting extra: " + localPath); - // Pass the result back to the SingleInstance activity. - returnIntent.putExtra("result_path", localPath); + localPath = copyAndDownscaleToCache(result.getData().getData()); + } else { + localPath = null; } - returnIntent.putExtra("result_code", result.getResultCode()); - returnIntent.putExtra("request_id", requestId); - - // This flag is key: it finds the existing instance of your main activity - returnIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); - startActivity(returnIntent); - finish(); + returnWithResult(result.getResultCode(), requestId, localPath); } ); - // Only launch the picker if we are starting fresh. If we're being recreated, - // the ActivityResultRegistry will handle delivering the pending result to the launcher. + // Only launch the picker if we are starting fresh. If we're being recreated (e.g. rotation), + // the ActivityResultRegistry will handle delivering the pending result to the launcher + // automatically, and we don't want to launch the picker a second time. if (savedInstanceState == null) { if (pickerIntent != null) { - launcher.launch(pickerIntent); + try { + launcher.launch(pickerIntent); + } catch (ActivityNotFoundException e) { + NativeApp.reportException(e, "ImageResultProxy: failed to launch picker intent, activity not found: " + pickerIntent.getAction()); + returnWithResult(NativeApp.RESULT_ERROR_ACTIVITY_NOT_FOUND, requestId, null); + } catch (Exception e) { + NativeApp.reportException(e, "ImageResultProxy: failed to launch picker intent, other error: " + pickerIntent.getAction()); + returnWithResult(NativeApp.RESULT_ERROR_OTHER_ACTIVITY_ERROR, requestId, null); + } } else { Log.e(TAG, "No picker intent provided"); finish(); diff --git a/android/src/org/ppsspp/ppsspp/NativeApp.java b/android/src/org/ppsspp/ppsspp/NativeApp.java index e951f66023..d3fe003379 100644 --- a/android/src/org/ppsspp/ppsspp/NativeApp.java +++ b/android/src/org/ppsspp/ppsspp/NativeApp.java @@ -23,6 +23,12 @@ public class NativeApp { public static final int DEVICE_TYPE_DESKTOP = 2; public static final int DEVICE_TYPE_VR = 3; + // These are matched with the C++ RequestManager result codes, and also Activity.RESULT_OK/RESULT_CANCELED. + public static final int RESULT_OK = -1; + public static final int RESULT_CANCELED = 0; + public static final int RESULT_ERROR_ACTIVITY_NOT_FOUND = 1; + public static final int RESULT_ERROR_OTHER_ACTIVITY_ERROR = 2; + public static native void init(String model, int deviceType, String languageRegion, String apkPath, String dataDir, String externalStorageDir, String extFilesDir, String nativeLibDir, String additionalStorageDirs, String cacheDir, String shortcutParam, String installerName, int androidVersion, String board); public static native void audioInit(); public static native void audioShutdown(); diff --git a/android/src/org/ppsspp/ppsspp/PpssppActivity.java b/android/src/org/ppsspp/ppsspp/PpssppActivity.java index 2660243825..62caaa4f1a 100644 --- a/android/src/org/ppsspp/ppsspp/PpssppActivity.java +++ b/android/src/org/ppsspp/ppsspp/PpssppActivity.java @@ -1881,10 +1881,10 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList if (intent.hasExtra("request_id")) { logIntentExtras(intent); int requestId = intent.getIntExtra("request_id", -1); - int resultCode = intent.getIntExtra("result_code", RESULT_CANCELED); + int resultCode = intent.getIntExtra("result_code", NativeApp.RESULT_CANCELED); String path = intent.getStringExtra("result_path"); - if (resultCode == RESULT_OK && path != null) { + if (resultCode == NativeApp.RESULT_OK && path != null) { Log.i(TAG, "Received valid proxied result: path='" + path + "' requestId=" + requestId); NativeApp.sendRequestResult(requestId, true, path, 0); } else {