Merge pull request #21656 from hrydgard/file-picker-exception-error

Fix the java exception reporting for Android file picker errors, fix some search issues
This commit is contained in:
Henrik Rydgård
2026-05-11 13:58:48 +02:00
committed by GitHub
16 changed files with 108 additions and 49 deletions
+6
View File
@@ -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;
+6
View File
@@ -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;
}
+3 -2
View File
@@ -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<std::mutex> guard(callbackMutex_);
auto iter = callbackMap_.find(requestId);
if (iter == callbackMap_.end()) {
@@ -127,6 +127,7 @@ void RequestManager::PostSystemFailure(int requestId) {
std::lock_guard<std::mutex> 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();
+6 -2
View File
@@ -12,13 +12,16 @@
class Path;
typedef std::function<void(const char *responseString, int responseValue)> RequestCallback;
typedef std::function<void()> RequestFailedCallback;
typedef std::function<void(int responseValue)> 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.
+2 -2
View File
@@ -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<std::mutex> 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<std::mutex> guard(nativeMutex_);
if (nativeStatus_ != PSPOskNativeStatus::WAITING) {
+1
View File
@@ -212,6 +212,7 @@ void UpdateNativeMenuKeys() {
RemoveKeyboardLetterKeys(tabRight);
RemoveKeyboardLetterKeys(confirmKeys);
RemoveKeyboardLetterKeys(cancelKeys);
RemoveKeyboardLetterKeys(infoKeys);
SetDPadKeys(upKeys, downKeys, leftKeys, rightKeys);
SetConfirmCancelKeys(confirmKeys, cancelKeys);
+3 -3
View File
@@ -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);
+1 -6
View File
@@ -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);
-1
View File
@@ -92,7 +92,6 @@ protected:
bool lockBackgroundAudio_ = false;
bool lastVertical_ = false;
bool confirmedTemporary_ = false;
bool searchKeyModifier_ = false;
bool searchChanged_ = false;
std::string searchFilter_;
+4 -4
View File
@@ -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<UIScreen>(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<MemStickScreen>(this, &MemStickScreen::OnHelp);
rightColumnItems->Add(new UI::Choice(ms->T("WhatsThis", "What's this?"), ImageID("I_LINK_OUT_QUESTION")))->OnClick.Handle<MemStickScreen>(this, &MemStickScreen::OnHelp);
}
INFO_LOG(Log::System, "MemStickScreen: initialSetup=%d", (int)initialSetup_);
@@ -429,8 +429,8 @@ void MemStickScreen::Browse(UI::EventParams &params) {
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);
});
}
+7
View File
@@ -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).
+2 -2
View File
@@ -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);
}
}
@@ -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();
@@ -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();
@@ -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();
@@ -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 {