Merge pull request #22487 from hrydgard/android-lifecycle-fixes

Android lifecycle fixes, file sharing support (unused)
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-10-10 16:43:58 +02:00
commit bd3e55a104
21 files changed
+483 -92

No files matched your search

+1 -1
View File
@@ -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;
+1
View File
@@ -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";
+6
View File
@@ -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);
}
+2
View File
@@ -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,
+2
View File
@@ -91,6 +91,8 @@ list(APPEND UISource
ReportScreen.cpp
SavedataScreen.h
SavedataScreen.cpp
ShareFiles.h
ShareFiles.cpp
SystemInfoScreen.h
SystemInfoScreen.cpp
Store.h
+130
View File
@@ -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;
}
+41
View File
@@ -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);
+2
View File
@@ -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" />
+2
View File
@@ -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>
+2
View File
@@ -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" />
+2
View File
@@ -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" />
+10
View File
@@ -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>
+1
View File
@@ -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 \
+21 -4
View File
@@ -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.
+3
View File
@@ -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);
+5
View File
@@ -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;
+10
View File
@@ -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);
}
+229 -84
View File
@@ -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 + "'");
}
+6 -1
View File
@@ -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;
}