mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-14 06:50:29 +02:00
Merge pull request #22487 from hrydgard/android-lifecycle-fixes
Android lifecycle fixes, file sharing support (unused)
This commit is contained in:
21 files changed
+483
-92
No files matched your search
@@ -18,7 +18,7 @@ OpenGLGraphicsContext::OpenGLGraphicsContext() {
|
||||
bool OpenGLGraphicsContext::InitSurface(WindowSystem winsys, void *data1, void *data2, std::string *errorMessage) {
|
||||
CheckGLExtensions();
|
||||
INFO_LOG(Log::G3D, "OpenGLGraphicsContext::InitSurface");
|
||||
draw_ = Draw::T3DCreateGLContext(false); // Can't fail
|
||||
draw_ = Draw::T3DCreateGLContext(CanChangeSwapInterval()); // Can't fail
|
||||
renderManager_ = (GLRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
|
||||
// TODO: In VR mode, we don't want a lot of inflight frames.
|
||||
renderManager_->SetInflightFrames(g_Config.iInflightFrames);
|
||||
|
||||
@@ -40,6 +40,9 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
// Whether the subclass can set the swap interval (SetSwapIntervalFunction), which PresentMode::IMMEDIATE needs.
|
||||
virtual bool CanChangeSwapInterval() const { return false; }
|
||||
|
||||
// Protected rather than private so a subclass that owns the real context (see
|
||||
// android/jni/AndroidEGLGraphicsContext.h) can hook up a swap function.
|
||||
Draw::DrawContext *draw_ = nullptr;
|
||||
|
||||
@@ -34,6 +34,7 @@ const char *RequestTypeAsString(SystemRequestType type) {
|
||||
case SystemRequestType::RECREATE_ACTIVITY: return "RECREATE_ACTIVITY";
|
||||
case SystemRequestType::COPY_TO_CLIPBOARD: return "COPY_TO_CLIPBOARD";
|
||||
case SystemRequestType::SHARE_TEXT: return "SHARE_TEXT";
|
||||
case SystemRequestType::SHARE_FILE: return "SHARE_FILE";
|
||||
case SystemRequestType::SET_WINDOW_TITLE: return "SET_WINDOW_TITLE";
|
||||
case SystemRequestType::APPLY_FULLSCREEN_STATE: return "SET_FULLSCREEN_STATE";
|
||||
case SystemRequestType::GRAPHICS_BACKEND_FAILED_ALERT: return "GRAPHICS_BACKEND_FAILED_ALERT";
|
||||
|
||||
@@ -176,6 +176,12 @@ inline void System_ShareText(std::string_view text) {
|
||||
g_requestManager.MakeSystemRequest(SystemRequestType::SHARE_TEXT, NO_REQUESTER_TOKEN, nullptr, nullptr, text, "", 0);
|
||||
}
|
||||
|
||||
// Offers a file to other apps (mail, chat, cloud storage) through the system's share sheet, if
|
||||
// SYSPROP_SUPPORTS_SHARE_FILE. With an empty mimeType, it's guessed from the extension.
|
||||
inline void System_ShareFile(const Path &path, std::string_view mimeType = "") {
|
||||
g_requestManager.MakeSystemRequest(SystemRequestType::SHARE_FILE, NO_REQUESTER_TOKEN, nullptr, nullptr, path.ToString(), mimeType, 0);
|
||||
}
|
||||
|
||||
inline void System_NotifyUIEvent(UIEventNotification notification) {
|
||||
g_requestManager.MakeSystemRequest(SystemRequestType::NOTIFY_UI_EVENT, NO_REQUESTER_TOKEN, nullptr, nullptr, "", "", (int64_t)notification, 0);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ enum class SystemRequestType {
|
||||
RECREATE_ACTIVITY, // Android
|
||||
COPY_TO_CLIPBOARD,
|
||||
SHARE_TEXT,
|
||||
SHARE_FILE,
|
||||
SET_WINDOW_TITLE,
|
||||
APPLY_FULLSCREEN_STATE,
|
||||
GRAPHICS_BACKEND_FAILED_ALERT,
|
||||
@@ -239,6 +240,7 @@ enum SystemProperty {
|
||||
SYSPROP_USE_IAP,
|
||||
SYSPROP_USE_APP_STORE,
|
||||
SYSPROP_SUPPORTS_SHARE_TEXT,
|
||||
SYSPROP_SUPPORTS_SHARE_FILE,
|
||||
|
||||
SYSPROP_CAN_RESTRICT_ORIENTATION,
|
||||
|
||||
|
||||
@@ -91,6 +91,8 @@ list(APPEND UISource
|
||||
ReportScreen.cpp
|
||||
SavedataScreen.h
|
||||
SavedataScreen.cpp
|
||||
ShareFiles.h
|
||||
ShareFiles.cpp
|
||||
SystemInfoScreen.h
|
||||
SystemInfoScreen.cpp
|
||||
Store.h
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2012- PPSSPP Project.
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, version 2.0 or later versions.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License 2.0 for more details.
|
||||
|
||||
// A copy of the GPL 2.0 should have been included with the program.
|
||||
// If not, see http://www.gnu.org/licenses/
|
||||
|
||||
// Official git repository and contact information can be found at
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
#ifdef SHARED_LIBZIP
|
||||
#include <zip.h>
|
||||
#else
|
||||
#include "ext/libzip/zip.h"
|
||||
#endif
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include "Common/File/DirListing.h"
|
||||
#include "Common/File/FileUtil.h"
|
||||
#include "Common/Log.h"
|
||||
#include "Common/StringUtils.h"
|
||||
#include "Common/System/Request.h"
|
||||
#include "Common/System/System.h"
|
||||
#include "Common/TimeUtil.h"
|
||||
#include "Core/Config.h"
|
||||
#include "UI/ShareFiles.h"
|
||||
|
||||
// How long a shared zip is kept for the receiving app to read.
|
||||
static constexpr int64_t SHARE_KEEP_SECONDS = 60 * 60;
|
||||
|
||||
static Path ShareDirectory() {
|
||||
return g_Config.appCacheDirectory / "share";
|
||||
}
|
||||
|
||||
// Deletes the folders of earlier shares (named by their time) once they're old enough.
|
||||
static void CleanUpOldShares(int64_t now) {
|
||||
std::vector<File::FileInfo> folders;
|
||||
File::GetFilesInDir(ShareDirectory(), &folders);
|
||||
for (const File::FileInfo &folder : folders) {
|
||||
if (!folder.isDirectory) {
|
||||
continue;
|
||||
}
|
||||
const int64_t created = strtoll(folder.name.c_str(), nullptr, 10);
|
||||
if (created <= 0 || now - created > SHARE_KEEP_SECONDS) {
|
||||
INFO_LOG(Log::System, "Deleting old shared files in %s", folder.fullName.c_str());
|
||||
File::DeleteDirRecursively(folder.fullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CanShareFiles() {
|
||||
return System_GetPropertyBool(SYSPROP_SUPPORTS_SHARE_FILE) && !g_Config.appCacheDirectory.empty();
|
||||
}
|
||||
|
||||
bool ShareFilesAsZip(std::string_view zipName, const std::vector<ShareZipEntry> &files) {
|
||||
if (!CanShareFiles() || files.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int64_t now = (int64_t)time_now_unix_utc();
|
||||
CleanUpOldShares(now);
|
||||
|
||||
std::string name = SanitizeString(zipName, StringRestriction::FileName);
|
||||
if (!endsWithNoCase(name, ".zip")) {
|
||||
name += ".zip";
|
||||
}
|
||||
// A folder per share, so a share can't overwrite a zip that an app is still reading.
|
||||
Path folder = ShareDirectory() / StringFromFormat("%lld", (long long)now);
|
||||
for (int i = 1; File::Exists(folder); i++) {
|
||||
folder = ShareDirectory() / StringFromFormat("%lld_%d", (long long)now, i);
|
||||
}
|
||||
if (!File::CreateFullPath(folder)) {
|
||||
ERROR_LOG(Log::System, "ShareFilesAsZip: Couldn't create %s", folder.c_str());
|
||||
return false;
|
||||
}
|
||||
const Path zipPath = folder / name;
|
||||
|
||||
// libzip reads the buffers when the archive is closed, so they have to live until then.
|
||||
std::vector<std::string> contents(files.size());
|
||||
int error = 0;
|
||||
zip_t *zip = zip_open(zipPath.c_str(), ZIP_CREATE | ZIP_TRUNCATE, &error);
|
||||
if (!zip) {
|
||||
ERROR_LOG(Log::System, "ShareFilesAsZip: Couldn't create %s (libzip error %d)", zipPath.c_str(), error);
|
||||
File::DeleteDirRecursively(folder);
|
||||
return false;
|
||||
}
|
||||
bool success = true;
|
||||
for (size_t i = 0; i < files.size(); i++) {
|
||||
const ShareZipEntry &entry = files[i];
|
||||
const std::string entryName = entry.nameInZip.empty() ? entry.source.GetFilename() : entry.nameInZip;
|
||||
if (!File::ReadBinaryFileToString(entry.source, &contents[i])) {
|
||||
ERROR_LOG(Log::System, "ShareFilesAsZip: Couldn't read %s", entry.source.c_str());
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
zip_source_t *source = zip_source_buffer(zip, contents[i].data(), contents[i].size(), 0);
|
||||
// Fails on a name that's already in the zip, too.
|
||||
if (!source || zip_file_add(zip, entryName.c_str(), source, ZIP_FL_ENC_UTF_8) < 0) {
|
||||
ERROR_LOG(Log::System, "ShareFilesAsZip: Couldn't add %s as '%s': %s", entry.source.c_str(), entryName.c_str(), zip_strerror(zip));
|
||||
if (source) {
|
||||
zip_source_free(source);
|
||||
}
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
zip_discard(zip);
|
||||
File::DeleteDirRecursively(folder);
|
||||
return false;
|
||||
}
|
||||
if (zip_close(zip) != 0) {
|
||||
ERROR_LOG(Log::System, "ShareFilesAsZip: Couldn't write %s: %s", zipPath.c_str(), zip_strerror(zip));
|
||||
zip_discard(zip);
|
||||
File::DeleteDirRecursively(folder);
|
||||
return false;
|
||||
}
|
||||
|
||||
INFO_LOG(Log::System, "ShareFilesAsZip: Sharing %s, %d files", zipPath.c_str(), (int)files.size());
|
||||
System_ShareFile(zipPath, "application/zip");
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2012- PPSSPP Project.
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, version 2.0 or later versions.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License 2.0 for more details.
|
||||
|
||||
// A copy of the GPL 2.0 should have been included with the program.
|
||||
// If not, see http://www.gnu.org/licenses/
|
||||
|
||||
// Official git repository and contact information can be found at
|
||||
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/File/Path.h"
|
||||
|
||||
struct ShareZipEntry {
|
||||
Path source; // Any file we can read, content:// URIs included.
|
||||
std::string nameInZip; // Empty for the source's own filename.
|
||||
};
|
||||
|
||||
// Whether ShareFilesAsZip can work here (the platform can share files).
|
||||
bool CanShareFiles();
|
||||
|
||||
// Zips the files up and offers the zip to other apps through the system share sheet (System_ShareFile),
|
||||
// for example a savestate and its screenshot, to send to a friend. zipName is what the receiver sees, and is
|
||||
// sanitized. False if the zip couldn't be made, or the platform can't share files.
|
||||
//
|
||||
// The zip is written to a folder of its own in the app's cache, since the receiving app reads it whenever
|
||||
// it gets around to it, and deleted by a later call once it's an hour old. The files are read into memory
|
||||
// and compressed on the calling thread, which is fine for savestates but not for whole ISOs.
|
||||
bool ShareFilesAsZip(std::string_view zipName, const std::vector<ShareZipEntry> &files);
|
||||
@@ -71,6 +71,7 @@
|
||||
<ClCompile Include="ReportScreen.cpp" />
|
||||
<ClCompile Include="RetroAchievementScreens.cpp" />
|
||||
<ClCompile Include="SavedataScreen.cpp" />
|
||||
<ClCompile Include="ShareFiles.cpp" />
|
||||
<ClCompile Include="Store.cpp" />
|
||||
<ClCompile Include="AudioCommon.cpp" />
|
||||
<ClCompile Include="SystemInfoScreen.cpp" />
|
||||
@@ -133,6 +134,7 @@
|
||||
<ClInclude Include="ReportScreen.h" />
|
||||
<ClInclude Include="RetroAchievementScreens.h" />
|
||||
<ClInclude Include="SavedataScreen.h" />
|
||||
<ClInclude Include="ShareFiles.h" />
|
||||
<ClInclude Include="Store.h" />
|
||||
<ClInclude Include="SystemInfoScreen.h" />
|
||||
<ClInclude Include="TabbedDialogScreen.h" />
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
<Filter>Screens</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="BackgroundAudio.cpp" />
|
||||
<ClCompile Include="ShareFiles.cpp" />
|
||||
<ClCompile Include="ReportScreen.cpp">
|
||||
<Filter>Screens</Filter>
|
||||
</ClCompile>
|
||||
@@ -213,6 +214,7 @@
|
||||
<Filter>Screens</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="BackgroundAudio.h" />
|
||||
<ClInclude Include="ShareFiles.h" />
|
||||
<ClInclude Include="ReportScreen.h">
|
||||
<Filter>Screens</Filter>
|
||||
</ClInclude>
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
<ClInclude Include="..\..\UI\RemoteISOScreen.h" />
|
||||
<ClInclude Include="..\..\UI\ReportScreen.h" />
|
||||
<ClInclude Include="..\..\UI\SavedataScreen.h" />
|
||||
<ClInclude Include="..\..\UI\ShareFiles.h" />
|
||||
<ClInclude Include="..\..\UI\Store.h" />
|
||||
<ClInclude Include="..\..\UI\TiltAnalogSettingsScreen.h" />
|
||||
<ClInclude Include="..\..\UI\TouchControlLayoutScreen.h" />
|
||||
@@ -199,6 +200,7 @@
|
||||
<ClCompile Include="..\..\UI\RemoteISOScreen.cpp" />
|
||||
<ClCompile Include="..\..\UI\ReportScreen.cpp" />
|
||||
<ClCompile Include="..\..\UI\SavedataScreen.cpp" />
|
||||
<ClCompile Include="..\..\UI\ShareFiles.cpp" />
|
||||
<ClCompile Include="..\..\UI\Store.cpp" />
|
||||
<ClCompile Include="..\..\UI\TiltAnalogSettingsScreen.cpp" />
|
||||
<ClCompile Include="..\..\UI\TouchControlLayoutScreen.cpp" />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp" />
|
||||
<ClCompile Include="..\..\UI\BackgroundAudio.cpp" />
|
||||
<ClCompile Include="..\..\UI\ShareFiles.cpp" />
|
||||
<ClCompile Include="..\..\UI\DiscordIntegration.cpp" />
|
||||
<ClCompile Include="..\..\UI\GameInfoCache.cpp" />
|
||||
<ClCompile Include="..\..\UI\GamepadEmu.cpp" />
|
||||
@@ -156,6 +157,7 @@
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="targetver.h" />
|
||||
<ClInclude Include="..\..\UI\BackgroundAudio.h" />
|
||||
<ClInclude Include="..\..\UI\ShareFiles.h" />
|
||||
<ClInclude Include="..\..\UI\DiscordIntegration.h" />
|
||||
<ClInclude Include="..\..\UI\GameInfoCache.h" />
|
||||
<ClInclude Include="..\..\UI\GamepadEmu.h" />
|
||||
|
||||
@@ -126,6 +126,16 @@
|
||||
<action android:name="org.ppsspp.ppsspp.action.CLEAR_ACHIEVEMENTS_HOST_OVERRIDE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<!-- For sharing our files with other apps (System_ShareFile). Each share grants access to one file. -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
<profileable android:shell="true" android:enabled="true" />
|
||||
</application>
|
||||
|
||||
|
||||
@@ -1042,6 +1042,7 @@ LOCAL_SRC_FILES := \
|
||||
$(SRC)/UI/ReportScreen.cpp \
|
||||
$(SRC)/UI/PauseScreen.cpp \
|
||||
$(SRC)/UI/SavedataScreen.cpp \
|
||||
$(SRC)/UI/ShareFiles.cpp \
|
||||
$(SRC)/UI/Store.cpp \
|
||||
$(SRC)/UI/SystemInfoScreen.cpp \
|
||||
$(SRC)/UI/GamepadEmu.cpp \
|
||||
|
||||
@@ -208,14 +208,31 @@ bool AndroidEGLGraphicsContext::InitSurface(WindowSystem winsys, void *data1, vo
|
||||
}
|
||||
});
|
||||
|
||||
// NOTE: Deliberately *not* calling SetSwapIntervalFunction. GLSurfaceView gave us no way to
|
||||
// change the interval, so PresentMode::IMMEDIATE was always a no-op on Android GL, and this
|
||||
// class set out to behave identically. Now that it's the only path, eglSwapInterval(0) is
|
||||
// there for the taking - but that's a behavior change, so it wants its own commit.
|
||||
// For PresentMode::IMMEDIATE (vsync off, and fast-forward without skipping flips).
|
||||
if (CanChangeSwapInterval()) {
|
||||
renderManager_->SetSwapIntervalFunction([this](int interval) {
|
||||
if (!eglSwapInterval(display_, interval)) {
|
||||
WARN_LOG(Log::G3D, "%s", EGLFailure("eglSwapInterval").c_str());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AndroidEGLGraphicsContext::CanChangeSwapInterval() const {
|
||||
// In VR, OpenXR presents, not eglSwapBuffers.
|
||||
if (IsVREnabled() || display_ == EGL_NO_DISPLAY) {
|
||||
return false;
|
||||
}
|
||||
// eglSwapInterval silently clamps to the config's minimum, which some configs have at 1.
|
||||
EGLint minInterval = 1;
|
||||
if (!eglGetConfigAttrib(display_, config_, EGL_MIN_SWAP_INTERVAL, &minInterval)) {
|
||||
return false;
|
||||
}
|
||||
return minInterval == 0;
|
||||
}
|
||||
|
||||
void AndroidEGLGraphicsContext::ShutdownSurface() {
|
||||
// Order matters: the render manager's teardown still issues GL calls, so the context has to
|
||||
// stay current until it's done.
|
||||
|
||||
@@ -44,6 +44,9 @@ public:
|
||||
bool InitSurface(WindowSystem winsys, void *data1, void *data2, std::string *errorMessage) override;
|
||||
void ShutdownSurface() override;
|
||||
|
||||
protected:
|
||||
bool CanChangeSwapInterval() const override;
|
||||
|
||||
private:
|
||||
// Picks a config, from the most to the least desirable. Fills in config_.
|
||||
bool ChooseConfig(bool wantGLES3, std::string *errorMessage);
|
||||
|
||||
@@ -404,6 +404,7 @@ bool System_GetPropertyBool(SystemProperty prop) {
|
||||
case SYSPROP_SUPPORTS_OPEN_FILE_IN_EDITOR:
|
||||
return false; // Update if we add support in FileUtil.cpp: OpenFileInEditor
|
||||
case SYSPROP_SUPPORTS_SHARE_TEXT:
|
||||
case SYSPROP_SUPPORTS_SHARE_FILE:
|
||||
return true;
|
||||
case SYSPROP_APP_GOLD:
|
||||
#ifdef GOLD
|
||||
@@ -1068,6 +1069,10 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string
|
||||
case SystemRequestType::SHARE_TEXT:
|
||||
PushCommand("share_text", param1);
|
||||
return true;
|
||||
case SystemRequestType::SHARE_FILE:
|
||||
// The MIME type first, as a path could contain the separator.
|
||||
PushCommand("share_file", param2 + ":@:" + param1);
|
||||
return true;
|
||||
case SystemRequestType::SET_KEEP_SCREEN_BRIGHT:
|
||||
PushCommand("set_keep_screen_bright", param3 ? "on" : "off");
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Where System_ShareFile can share files from, when they are paths rather than content:// URIs. -->
|
||||
<paths>
|
||||
<files-path name="files" path="." />
|
||||
<cache-path name="cache" path="." />
|
||||
<external-files-path name="external_files" path="." />
|
||||
<external-path name="external" path="." />
|
||||
<!-- SD cards, and anything else outside the above. -->
|
||||
<root-path name="root" path="." />
|
||||
</paths>
|
||||
@@ -6,11 +6,11 @@ import android.graphics.Rect;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.graphics.YuvImage;
|
||||
import android.hardware.Camera;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.Surface;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
@@ -151,8 +151,10 @@ class CameraHelper {
|
||||
};
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
// Static in PpssppActivity, so this gets the application context: DisplayManager, unlike WindowManager,
|
||||
// gives a display that follows the rotation without one.
|
||||
CameraHelper(final Context context) {
|
||||
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
|
||||
mDisplay = ((DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE)).getDisplay(Display.DEFAULT_DISPLAY);
|
||||
mSurfaceTexture = new SurfaceTexture(10);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Toast;
|
||||
@@ -54,6 +55,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.DisplayCutoutCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
@@ -68,6 +70,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -77,10 +80,14 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
// Allows us to skip a lot of initialization on secondary calls to onCreate.
|
||||
private static boolean initialized = false;
|
||||
|
||||
// Bumped by every onCreate. An activity whose number is no longer the latest has been replaced
|
||||
// by a newer one, which now owns the native state.
|
||||
private static int latestInstance = 0;
|
||||
private int instance;
|
||||
// The activity that owns the native side (the render thread, the audio). Usually the only one alive, but
|
||||
// not always: after a finish() and a quick relaunch, Android creates and resumes the new activity before
|
||||
// the old one's onStop and onDestroy, which then must leave the native side alone. Weak, so this alone
|
||||
// doesn't keep a destroyed activity around.
|
||||
private static WeakReference<PpssppActivity> currentActivity = new WeakReference<>(null);
|
||||
// Numbers the activities, for the log.
|
||||
private static int activityCounter = 0;
|
||||
private int activityNumber = 0;
|
||||
|
||||
// The surface we hand to the native render loop thread, for both OpenGL and Vulkan.
|
||||
private NativeSurfaceView mSurfaceView;
|
||||
@@ -108,14 +115,18 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
// switched-away from or rotated etc.
|
||||
private boolean shuttingDown;
|
||||
|
||||
// surfaceChanged now also fires for rotations and resizes, where the thread just keeps going.
|
||||
private boolean renderLoopRunning = false;
|
||||
// There's one render loop thread. Whether it runs, and on which activity's surface (activityNumber).
|
||||
// surfaceChanged also fires for rotations and resizes, where the thread just keeps going.
|
||||
private static boolean renderLoopRunning = false;
|
||||
private static int renderLoopActivity = 0;
|
||||
|
||||
// Allow for multiple connected gamepads but just consider them the same for now.
|
||||
// Actually this is not entirely true, see the code.
|
||||
private final ArrayList<InputDeviceState> inputPlayers = new ArrayList<>();
|
||||
|
||||
private PowerSaveModeReceiver mPowerSaveModeReceiver = null;
|
||||
// The open input box, if any.
|
||||
private AlertDialog inputDialog = null;
|
||||
private NativeSurfaceManager surfaceManager = null;
|
||||
private static LocationHelper mLocationHelper;
|
||||
private static InfraredHelper mInfraredHelper;
|
||||
@@ -366,9 +377,9 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
return info.reqGlEsVersion >= 0x30000;
|
||||
}
|
||||
|
||||
public void Initialize() {
|
||||
// Initialize audio classes. Do this here since detectOptimalAudioSettings()
|
||||
// needs audioManager
|
||||
// What every activity needs for itself, not just the first one (Initialize).
|
||||
private void initActivityServices() {
|
||||
// detectOptimalAudioSettings() needs audioManager.
|
||||
this.audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
|
||||
if (this.audioManager != null) {
|
||||
this.audioFocusChangeListener = new AudioFocusChangeListener();
|
||||
@@ -385,7 +396,9 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
NativeApp.sendMessageFromJava("sustained_perf_supported", "1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize() {
|
||||
// isLandscape is used to trigger GetAppInfo currently, we
|
||||
boolean landscape = NativeApp.isLandscape();
|
||||
Log.d(TAG, "Landscape: " + landscape);
|
||||
@@ -493,8 +506,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
mInfraredHelper = null;
|
||||
Log.i(TAG, "InfraredHelper exception: " + e);
|
||||
}
|
||||
// android.graphics.SurfaceTexture is not available before version 11.
|
||||
mCameraHelper = new CameraHelper(this);
|
||||
mCameraHelper = new CameraHelper(getApplicationContext());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -635,6 +647,35 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
surfaceManager.updateDisplayMeasurements();
|
||||
}
|
||||
|
||||
// Makes this the activity that owns the native side.
|
||||
private void takeOver() {
|
||||
activityNumber = ++activityCounter;
|
||||
PpssppActivity previous = currentActivity.get();
|
||||
currentActivity = new WeakReference<>(this);
|
||||
if (previous == null) {
|
||||
Log.i(TAG, "Activity #" + activityNumber + " created" + (initialized ? ", adopting the native side left running" : ""));
|
||||
} else if (previous.isDestroyed()) {
|
||||
Log.i(TAG, "Activity #" + activityNumber + " replaces #" + previous.activityNumber + ", which is destroyed");
|
||||
} else {
|
||||
// The finish() and quick relaunch case. Fine, but the old one's lifecycle calls from here on are worth seeing.
|
||||
Log.w(TAG, "Activity #" + activityNumber + " replaces #" + previous.activityNumber + ", which is still alive (finishing: " + previous.isFinishing() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCurrent() {
|
||||
return currentActivity.get() == this;
|
||||
}
|
||||
|
||||
// True if this activity owns the native side, otherwise logs that it skipped what.
|
||||
private boolean checkCurrent(String what) {
|
||||
if (isCurrent()) {
|
||||
return true;
|
||||
}
|
||||
PpssppActivity current = currentActivity.get();
|
||||
Log.w(TAG, "Activity #" + activityNumber + ": skipping " + what + ", " + (current != null ? "#" + current.activityNumber : "no activity") + " owns the native side now");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Starts the native render loop thread, which owns the graphics context and its surface.
|
||||
public native boolean runRenderLoop(Surface surface);
|
||||
// Tells the render loop thread to exit, so we can restart it.
|
||||
@@ -656,7 +697,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
return;
|
||||
}
|
||||
|
||||
instance = ++latestInstance;
|
||||
takeOver();
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
|
||||
@@ -690,6 +731,8 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
// whether to start at 1x or 2x.
|
||||
surfaceManager.updateDisplayMeasurements();
|
||||
|
||||
initActivityServices();
|
||||
|
||||
// On the first run, the shortcut parameter is passed to NativeApp.init in here.
|
||||
final boolean firstRun = !initialized;
|
||||
if (!initialized) {
|
||||
@@ -879,8 +922,8 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
Log.i(TAG, "notifySurface: got surface, starting thread.");
|
||||
startRenderLoopThread();
|
||||
} else {
|
||||
// The surface must not be touched once surfaceDestroyed returns. Normally onPause has
|
||||
// already joined the thread and this does nothing, but that order isn't guaranteed.
|
||||
// The surface must not be touched once surfaceDestroyed returns. This is what normally stops the
|
||||
// render loop; onStop does it too, for a stopped activity that keeps its surface.
|
||||
Log.i(TAG, "notifySurface: Surface is gone, making sure the render thread is too.");
|
||||
joinRenderLoopThread();
|
||||
}
|
||||
@@ -888,39 +931,49 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
}
|
||||
|
||||
// The render loop thread (EmuThread) is now spawned from the native side.
|
||||
protected synchronized void startRenderLoopThread() {
|
||||
if (mSurface == null) {
|
||||
Log.w(TAG, "startRenderLoopThread - not starting thread, needs surface");
|
||||
return;
|
||||
}
|
||||
if (instance != latestInstance) {
|
||||
Log.w(TAG, "startRenderLoopThread - not starting thread, a newer activity has taken over");
|
||||
return;
|
||||
}
|
||||
protected void startRenderLoopThread() {
|
||||
synchronized (PpssppActivity.class) {
|
||||
if (mSurface == null) {
|
||||
Log.i(TAG, "Activity #" + activityNumber + ": not starting the render loop yet, no surface");
|
||||
return;
|
||||
}
|
||||
if (!checkCurrent("starting the render loop")) {
|
||||
return;
|
||||
}
|
||||
if (renderLoopRunning) {
|
||||
if (renderLoopActivity == activityNumber) {
|
||||
Log.i(TAG, "Activity #" + activityNumber + ": render loop already running, surface changed in place");
|
||||
return;
|
||||
}
|
||||
// The previous activity should have stopped it when it finished or lost its surface.
|
||||
Log.e(TAG, "Activity #" + activityNumber + ": the render loop is still running on #" + renderLoopActivity + "'s surface. Stopping it first.");
|
||||
requestExitRenderLoop();
|
||||
renderLoopRunning = false;
|
||||
}
|
||||
|
||||
if (renderLoopRunning) {
|
||||
Log.i(TAG, "startRenderLoopThread: already running, surface changed in place");
|
||||
return;
|
||||
Log.i(TAG, "Activity #" + activityNumber + ": starting the render loop");
|
||||
applyFrameRate(mSurface, 60.0f);
|
||||
renderLoopRunning = runRenderLoop(mSurface);
|
||||
renderLoopActivity = activityNumber;
|
||||
}
|
||||
|
||||
Log.w(TAG, "startRenderLoopThread: Starting thread");
|
||||
|
||||
applyFrameRate(mSurface, 60.0f);
|
||||
renderLoopRunning = runRenderLoop(mSurface);
|
||||
}
|
||||
|
||||
private synchronized void joinRenderLoopThread() {
|
||||
if (instance != latestInstance) {
|
||||
// There's only one render thread, and it's rendering to the newer activity's surface now.
|
||||
// We get here when our surface goes away late, after a finish() and a quick relaunch.
|
||||
Log.w(TAG, "joinRenderLoopThread - leaving the thread alone, a newer activity has taken over");
|
||||
return;
|
||||
// Stops the render loop if it's running on this activity's surface. Waits until the thread has exited.
|
||||
private void joinRenderLoopThread() {
|
||||
synchronized (PpssppActivity.class) {
|
||||
if (!renderLoopRunning) {
|
||||
return;
|
||||
}
|
||||
if (renderLoopActivity != activityNumber) {
|
||||
// After a finish() and a quick relaunch, the old activity's surface goes away late, with the
|
||||
// loop already running on the new one's.
|
||||
Log.i(TAG, "Activity #" + activityNumber + ": leaving the render loop alone, it's running on #" + renderLoopActivity + "'s surface");
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, "Activity #" + activityNumber + ": stopping the render loop");
|
||||
requestExitRenderLoop();
|
||||
renderLoopRunning = false;
|
||||
}
|
||||
|
||||
// This will wait until the thread has exited.
|
||||
Log.i(TAG, "requestExitRenderLoop");
|
||||
requestExitRenderLoop();
|
||||
renderLoopRunning = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -946,15 +999,17 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
mPowerSaveModeReceiver.destroy(this);
|
||||
mPowerSaveModeReceiver = null;
|
||||
}
|
||||
// Otherwise its window leaks, and its request is never answered. Dismissing answers it as cancelled.
|
||||
if (inputDialog != null) {
|
||||
inputDialog.dismiss();
|
||||
inputDialog = null;
|
||||
}
|
||||
|
||||
if (instance != latestInstance) {
|
||||
// After finish(), Android is free to deliver onDestroy late, after it has already created
|
||||
// and resumed a new activity if the user was quick to relaunch. That one found initialized
|
||||
// still set and adopted the native state, so shutting down here would pull the audio and
|
||||
// the graphics context out from under it. It's not ours to shut down anymore.
|
||||
// (recreate() is not affected, it destroys the old activity before creating the new one.)
|
||||
Log.w(TAG, "in onDestroy, but a newer activity has taken over. Leaving the native side alone.");
|
||||
} else {
|
||||
// After finish(), Android is free to deliver onDestroy late, after it has already created and resumed a
|
||||
// new activity if the user was quick to relaunch. That one found initialized still set and adopted the
|
||||
// native state, so shutting down here would pull the audio and the graphics context out from under it.
|
||||
// (recreate() is not affected, it destroys the old activity before creating the new one.)
|
||||
if (checkCurrent("shutting down audio and the native side in onDestroy")) {
|
||||
// TODO: Can we ensure that the GL thread has stopped rendering here?
|
||||
// I've seen crashes that seem to indicate that sometimes it hasn't...
|
||||
NativeApp.audioShutdown();
|
||||
@@ -963,6 +1018,11 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
NativeApp.shutdown();
|
||||
unregisterCallbacks();
|
||||
initialized = false;
|
||||
} else if (!isChangingConfigurations()) {
|
||||
// Backed out of, or destroyed by the system, with the native side left running for the next
|
||||
// activity. Until that one registers, the native side mustn't keep this one alive or call it.
|
||||
Log.i(TAG, "in onDestroy, but not shutting down. Letting go of the activity.");
|
||||
unregisterCallbacks();
|
||||
} else {
|
||||
Log.i(TAG, "in onDestroy, but not shutting down.");
|
||||
}
|
||||
@@ -974,16 +1034,64 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
Log.i(TAG, "onDestroy end");
|
||||
}
|
||||
|
||||
// Emulation, audio and input follow visibility (onStart/onStop) rather than focus (onPause/onResume): a
|
||||
// permission dialog on top, or the other app of a split screen having focus, leaves the game on screen,
|
||||
// and it should keep running and playing. The render loop is really owned by the surface (notifySurface),
|
||||
// which normally goes away with the visibility anyway.
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
Log.i(TAG, "onStart");
|
||||
if (m_hasNoNativeBinary) {
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, "onStart begin");
|
||||
|
||||
// A replaced activity shouldn't be started again, but if it is, the native side isn't its to resume.
|
||||
if (checkCurrent("resuming audio and location in onStart")) {
|
||||
if (mLocationHelper != null) {
|
||||
mLocationHelper.resume();
|
||||
}
|
||||
NativeApp.resume();
|
||||
}
|
||||
updateAudioFocus(this.audioManager, this.audioFocusChangeListener);
|
||||
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME);
|
||||
|
||||
InputManager inputManager = (InputManager)getSystemService(Context.INPUT_SERVICE);
|
||||
inputManager.registerInputDeviceListener(inputDeviceListener, null);
|
||||
|
||||
// In case the surface outlived the stop. Otherwise this waits for surfaceCreated.
|
||||
startRenderLoopThread();
|
||||
Log.i(TAG, "onStart end");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
Log.i(TAG, "onStop");
|
||||
if (m_hasNoNativeBinary) {
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, "onStop begin");
|
||||
|
||||
InputManager inputManager = (InputManager)getSystemService(Context.INPUT_SERVICE);
|
||||
inputManager.unregisterInputDeviceListener(inputDeviceListener);
|
||||
|
||||
// Normally surfaceDestroyed has done this already, but not every device destroys the surface of a
|
||||
// stopped activity, and an invisible game shouldn't keep running.
|
||||
joinRenderLoopThread();
|
||||
|
||||
mSensorManager.unregisterListener(this);
|
||||
loseAudioFocus(this.audioManager, this.audioFocusChangeListener);
|
||||
|
||||
// After a finish() and a quick relaunch, this comes after the new activity's onStart, and the audio and
|
||||
// the location updates are its now.
|
||||
if (checkCurrent("pausing audio and location in onStop")) {
|
||||
Log.i(TAG, "Calling NativeApp.pause...");
|
||||
NativeApp.pause();
|
||||
if (mLocationHelper != null) {
|
||||
mLocationHelper.pause();
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "onStop end");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -993,27 +1101,17 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, "onPause begin");
|
||||
|
||||
InputManager inputManager = (InputManager)getSystemService(Context.INPUT_SERVICE);
|
||||
inputManager.unregisterInputDeviceListener(inputDeviceListener);
|
||||
|
||||
Log.i(TAG, "Joining render thread...");
|
||||
joinRenderLoopThread();
|
||||
Log.i(TAG, "Joined render thread");
|
||||
|
||||
Log.i(TAG, "mSensorManager.unregisterListener...");
|
||||
mSensorManager.unregisterListener(this);
|
||||
|
||||
loseAudioFocus(this.audioManager, this.audioFocusChangeListener);
|
||||
Log.i(TAG, "Calling NativeApp.pause...");
|
||||
NativeApp.pause();
|
||||
if (isFinishing()) {
|
||||
// Gone for good. A quick relaunch creates the next activity before this one's onStop, and the
|
||||
// render thread has to be free for it by then.
|
||||
Log.i(TAG, "Finishing, joining render thread...");
|
||||
joinRenderLoopThread();
|
||||
}
|
||||
// The camera goes to whichever app has focus.
|
||||
if (mCameraHelper != null) {
|
||||
Log.i(TAG, "Calling mCameraHelper.pause");
|
||||
mCameraHelper.pause();
|
||||
}
|
||||
if (mLocationHelper != null) {
|
||||
mLocationHelper.pause();
|
||||
}
|
||||
Log.i(TAG, "onPause end");
|
||||
}
|
||||
|
||||
@@ -1034,20 +1132,6 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
if (mCameraHelper != null) {
|
||||
mCameraHelper.resume();
|
||||
}
|
||||
if (mLocationHelper != null) {
|
||||
mLocationHelper.resume();
|
||||
}
|
||||
|
||||
updateAudioFocus(this.audioManager, this.audioFocusChangeListener);
|
||||
NativeApp.resume();
|
||||
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME);
|
||||
|
||||
InputManager inputManager =
|
||||
(InputManager)getSystemService(Context.INPUT_SERVICE);
|
||||
inputManager.registerInputDeviceListener(inputDeviceListener, null);
|
||||
|
||||
// Restart the render loop.
|
||||
startRenderLoopThread();
|
||||
Log.i(TAG, "onResume end");
|
||||
}
|
||||
|
||||
@@ -1424,7 +1508,13 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
Log.i(TAG, "input box dismissed");
|
||||
// This will be ignored if we already sent a success.
|
||||
NativeApp.sendRequestResult(requestId, false, "", 0);
|
||||
updateSystemUiVisibility();
|
||||
if (inputDialog == d) {
|
||||
inputDialog = null;
|
||||
}
|
||||
// Also comes after onDestroy dismissed it.
|
||||
if (!isDestroyed()) {
|
||||
updateSystemUiVisibility();
|
||||
}
|
||||
});
|
||||
|
||||
AlertDialog dlg = builder.create();
|
||||
@@ -1446,6 +1536,7 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
}
|
||||
try {
|
||||
dlg.show();
|
||||
inputDialog = dlg;
|
||||
input.requestFocus();
|
||||
} catch (Exception e) {
|
||||
NativeApp.reportException(e, "AlertDialog");
|
||||
@@ -1578,6 +1669,13 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
Log.e(TAG, e.toString());
|
||||
return false;
|
||||
}
|
||||
} else if (command.equals("share_file")) {
|
||||
String[] parts = params.split(":@:", 2);
|
||||
if (parts.length != 2) {
|
||||
Log.e(TAG, "share_file: bad parameter '" + params + "'");
|
||||
return false;
|
||||
}
|
||||
return shareFile(parts[1], parts[0]);
|
||||
} else if (command.equals("toast")) {
|
||||
Toast toast = Toast.makeText(this, params, Toast.LENGTH_LONG);
|
||||
toast.show();
|
||||
@@ -1798,6 +1896,40 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
return false;
|
||||
}
|
||||
|
||||
// Offers a file to other apps through the share sheet. The receiver gets read access to this file only.
|
||||
private boolean shareFile(String path, String mimeType) {
|
||||
try {
|
||||
Uri uri;
|
||||
if (path.startsWith("content://")) {
|
||||
// In a folder picked through the storage access framework. Our access to it can be passed on.
|
||||
uri = Uri.parse(path);
|
||||
} else {
|
||||
uri = FileProvider.getUriForFile(this, getPackageName() + ".fileprovider", new File(path));
|
||||
}
|
||||
if (mimeType.isEmpty()) {
|
||||
// Document URIs have the path encoded, so decode before looking for the extension.
|
||||
String decoded = Uri.decode(path);
|
||||
String extension = MimeTypeMap.getFileExtensionFromUrl(Uri.encode(decoded.substring(decoded.lastIndexOf('/') + 1)));
|
||||
String guessed = extension.isEmpty() ? null : MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase(Locale.ROOT));
|
||||
mimeType = guessed != null ? guessed : "application/octet-stream";
|
||||
}
|
||||
Log.i(TAG, "Sharing " + uri + " as " + mimeType);
|
||||
Intent send = new Intent(Intent.ACTION_SEND);
|
||||
send.setType(mimeType);
|
||||
send.putExtra(Intent.EXTRA_STREAM, uri);
|
||||
// Through a chooser, the read grant only reaches the chosen app via the ClipData.
|
||||
send.setClipData(ClipData.newRawUri("", uri));
|
||||
send.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
startActivity(Intent.createChooser(send, null));
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
// IllegalArgumentException from FileProvider: a path outside file_paths.xml.
|
||||
Log.e(TAG, "shareFile failed for " + path + ": " + e);
|
||||
NativeApp.reportException(e, path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isVRDevice() {
|
||||
return BuildConfig.FLAVOR.startsWith("vr");
|
||||
}
|
||||
@@ -1881,6 +2013,19 @@ public class PpssppActivity extends AppCompatActivity implements SensorEventList
|
||||
@SuppressWarnings("unused")
|
||||
public void postCommand(String command, String parameter) {
|
||||
runOnUiThread(() -> {
|
||||
// Posted before this activity was destroyed, but runs after.
|
||||
if (isDestroyed()) {
|
||||
Log.w(TAG, "Dropping command '" + command + "' for a destroyed activity");
|
||||
if (command.equals("inputbox")) {
|
||||
// The native side waits for an answer.
|
||||
try {
|
||||
NativeApp.sendRequestResult(Integer.parseInt(parameter.split(":@:", 2)[0]), false, "", 0);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e(TAG, "Bad inputbox parameter: " + parameter);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!processCommand(command, parameter)) {
|
||||
Log.e(TAG, "processCommand failed: cmd: '" + command + "' param: '" + parameter + "'");
|
||||
}
|
||||
|
||||
@@ -91,6 +91,8 @@ static std::string g_debugOutputBuffer;
|
||||
// Set when a run was asked for a configuration that couldn't be honoured. That isn't a test result,
|
||||
// so it fails the process whether or not this run was comparing anything.
|
||||
static bool g_configRefused = false;
|
||||
// Set when a test or game failed to boot. Like g_configRefused, this fails the process even when not comparing.
|
||||
static bool g_bootFailed = false;
|
||||
static bool g_writeFailureScreenshot = true;
|
||||
static bool g_writeDebugOutput = true;
|
||||
// Whether the emulated program's stdout/stderr are forwarded to ours. On by default - just running
|
||||
@@ -445,7 +447,10 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar
|
||||
}
|
||||
|
||||
if (!PSP_IsInited()) {
|
||||
// Without this, a failed boot outside --compare printed nothing and exited 0.
|
||||
fprintf(stderr, "Failed to boot '%s': %s\n", coreParameter.fileToStart.c_str(), error_string.c_str());
|
||||
GitHubActionsPrint("error", "Test init failed for %s", currentTestName.c_str());
|
||||
g_bootFailed = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -729,7 +734,7 @@ int RunTests(GraphicsContext *graphicsContext, CoreParameter &coreParameter, con
|
||||
}
|
||||
}
|
||||
|
||||
if (g_configRefused) {
|
||||
if (g_configRefused || g_bootFailed) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user