Add a (super ugly) panic screen
Some checks failed
Build / build-native-libs (arm64-v8a, aarch64-linux-android) (push) Failing after 1s
Build / build-native-libs (armeabi-v7a, armv7-linux-androideabi) (push) Failing after 1s
Build / build-native-libs (x86, i686-linux-android) (push) Failing after 1s
Build / build-native-libs (x86_64, x86_64-linux-android) (push) Failing after 2s
Build / build-apks (push) Has been skipped
Build / Android Tests (26) (push) Has been skipped
Build / Android Tests (34) (push) Has been skipped
Lint & Format / rust (push) Failing after 2s
Lint & Format / android (push) Failing after 1s

This commit is contained in:
Nathan Adams 2024-09-17 22:48:41 +02:00 committed by TÖRÖK Attila
parent fd9c1b656e
commit 0319ac9dad
8 changed files with 277 additions and 123 deletions

12
Cargo.lock generated
View File

@ -1926,16 +1926,6 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "log-panics"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f9dd8546191c1850ecf67d22f5ff00a935b890d0e84713159a55495cc2ac5f"
dependencies = [
"backtrace",
"log",
]
[[package]]
name = "lru"
version = "0.12.4"
@ -2769,9 +2759,9 @@ version = "0.1.0"
dependencies = [
"android-activity",
"android_logger",
"backtrace",
"jni",
"log",
"log-panics",
"ndk",
"ndk-context",
"ruffle_core",

View File

@ -40,10 +40,10 @@ ruffle_video_software = { git = "https://github.com/ruffle-rs/ruffle.git", branc
ruffle_frontend_utils = { git = "https://github.com/ruffle-rs/ruffle.git", branch = "master" }
log = "0.4.22"
log-panics = { version = "2.1.0", features = ["with-backtrace"]}
# Redirect tracing to log
tracing = {version = "0.1.40", features = ["log", "log-always"]}
backtrace = "0.3.74"
url = "2.5.2"
webbrowser = "1.0.1"

View File

@ -1,68 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Ruffle"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".PlayerActivity"
android:exported="true"
android:configChanges="orientation|keyboardHidden|screenSize"
android:screenOrientation="user"
>
<meta-data android:name="android.app.lib_name" android:value="ruffle_android" />
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.OPENABLE" />
<data android:scheme="file"/>
<data android:scheme="content"/>
<data android:mimeType="application/x-shockwave-flash"/>
<data android:pathSuffix="swf" />
<data android:pathPattern="*.swf" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="*" />
<data android:pathPattern=".*\\.swf" />
</intent-filter>
</activity>
</application>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Ruffle"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".PanicActivity"
android:launchMode="singleTask"
android:exported="false">
</activity>
<activity
android:name=".PlayerActivity"
android:exported="true"
android:configChanges="orientation|keyboardHidden|screenSize"
android:screenOrientation="user"
>
<meta-data android:name="android.app.lib_name" android:value="ruffle_android" />
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.OPENABLE" />
<data android:scheme="file"/>
<data android:scheme="content"/>
<data android:mimeType="application/x-shockwave-flash"/>
<data android:pathSuffix="swf" />
<data android:pathPattern="*.swf" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="*" />
<data android:pathPattern=".*\\.swf" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,20 @@
package rs.ruffle
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import rs.ruffle.ui.theme.RuffleTheme
class PanicActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
RuffleTheme {
PanicScreen(message = intent.getStringExtra("message") ?: "Unknown")
}
}
}
}

View File

@ -0,0 +1,77 @@
package rs.ruffle
import android.content.res.Configuration
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import rs.ruffle.ui.theme.RuffleTheme
@Composable
fun PanicScreen(message: String) {
Scaffold { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
verticalArrangement = Arrangement.Center
) {
Text(
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(align = Alignment.Center),
style = MaterialTheme.typography.headlineLarge,
text = "Ruffle Panicked :("
)
SelectionContainer {
Text(
modifier = Modifier
.wrapContentSize(align = Alignment.Center)
.padding(horizontal = 8.dp, vertical = 20.dp)
.verticalScroll(rememberScrollState())
.horizontalScroll(rememberScrollState()),
text = message,
softWrap = false
)
}
}
}
}
@Preview(name = "Panic - Light", uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Panic - Dark", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun PanicScreenPreview() {
RuffleTheme {
PanicScreen(
message = """Error: panicked at core/src/display_object/movie_clip.rs:477:9:
assertion `left == right` failed: Called replace_movie on a clip with LoaderInfo set
left: Some(LoaderInfoObject(LoaderInfoObject { ptr: 0x31b30a8 }))
right: None
at n.wbg.__wbg_new_796382978dfd4fb0 (https://unpkg.com/@ruffle-rs/ruffle/core.ruffle.90db0a0ab193ed0c601b.js:1:83857)
at ruffle_web.wasm.js_sys::Error::new::hfb561c222a4e70eb (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[12733]:0x98671a)
at ruffle_web.wasm.core::ops::function::FnOnce::call_once{{vtable.shim}}::h8a2a563fa204b611 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[9789]:0x9164aa)
at ruffle_web.wasm.std::panicking::rust_panic_with_hook::h33fe77d38d305ca3 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[6355]:0x8070ed)
at ruffle_web.wasm.core::panicking::panic_fmt::hde8b7aa66e2831e1 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[9511]:0x9071fd)
at ruffle_web.wasm.core::panicking::assert_failed_inner::hc95b7725cb4077cb (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[4402]:0x73cb5e)
at ruffle_web.wasm.ruffle_core::display_object::movie_clip::MovieClip::replace_with_movie::haf940b0718ed269c (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[2052]:0x50a035)
at ruffle_web.wasm.ruffle_core::loader::Loader::movie_loader::{{closure}}::h566c935379317178 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[1053]:0x2bc268)
at ruffle_web.wasm.<ruffle_web::navigator::WebNavigatorBackend as ruffle_core::backend::navigator::NavigatorBackend>::spawn_future::{{closure}}::h13f3540dbe40e875 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[1520]:0x419980)
at ruffle_web.wasm.wasm_bindgen_futures::queue::Queue::new::{{closure}}::hf37247571cf9bbf7 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[3648]:0x6ba342)"""
)
}
}

View File

@ -7,6 +7,7 @@ import android.net.Uri
import android.os.Build
import android.os.Build.VERSION_CODES
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
@ -230,6 +231,14 @@ class PlayerActivity : GameActivity() {
}
override fun onCreate(savedInstanceState: Bundle?) {
nativeInit { message ->
Log.e("ruffle", "Handling panic: $message")
startActivity(
Intent(this, PanicActivity::class.java).apply {
putExtra("message", message)
}
)
}
// When true, the app will fit inside any system UI windows.
// When false, we render behind any system UI windows.
WindowCompat.setDecorFitsSystemWindows(window, false)
@ -264,11 +273,10 @@ class PlayerActivity : GameActivity() {
init {
// load the native activity
System.loadLibrary("ruffle_android")
nativeInit()
}
@JvmStatic
private external fun nativeInit()
private external fun nativeInit(crashCallback: CrashCallback)
private fun <T> gatherAllDescendantsOfType(v: View, t: Class<*>): List<T> {
val result: MutableList<T> = ArrayList()
@ -282,4 +290,8 @@ class PlayerActivity : GameActivity() {
return result
}
}
fun interface CrashCallback {
fun onCrash(message: String)
}
}

View File

@ -172,33 +172,31 @@ impl JavaInterface {
}
pub fn init(env: &mut JNIEnv, class: &JClass) {
JAVA_INTERFACE
.set(JavaInterface {
get_surface_width: env
.get_method_id(class, "getSurfaceWidth", "()I")
.expect("getSurfaceWidth must exist"),
get_surface_height: env
.get_method_id(class, "getSurfaceHeight", "()I")
.expect("getSurfaceHeight must exist"),
show_context_menu: env
.get_method_id(class, "showContextMenu", "([Ljava/lang/String;)V")
.expect("showContextMenu must exist"),
get_swf_bytes: env
.get_method_id(class, "getSwfBytes", "()[B")
.expect("getSwfBytes must exist"),
get_swf_uri: env
.get_method_id(class, "getSwfUri", "()Ljava/lang/String;")
.expect("getSwfUri must exist"),
get_trace_output: env
.get_method_id(class, "getTraceOutput", "()Ljava/lang/String;")
.expect("getTraceOutput must exist"),
get_loc_in_window: env
.get_method_id(class, "getLocInWindow", "()[I")
.expect("getLocInWindow must exist"),
get_android_data_storage_dir: env
.get_method_id(class, "getAndroidDataStorageDir", "()Ljava/lang/String;")
.expect("getAndroidDataStorageDir must exist"),
})
.unwrap_or_else(|_| panic!("Init cannot be called more than once!"))
let _ = JAVA_INTERFACE.set(JavaInterface {
get_surface_width: env
.get_method_id(class, "getSurfaceWidth", "()I")
.expect("getSurfaceWidth must exist"),
get_surface_height: env
.get_method_id(class, "getSurfaceHeight", "()I")
.expect("getSurfaceHeight must exist"),
show_context_menu: env
.get_method_id(class, "showContextMenu", "([Ljava/lang/String;)V")
.expect("showContextMenu must exist"),
get_swf_bytes: env
.get_method_id(class, "getSwfBytes", "()[B")
.expect("getSwfBytes must exist"),
get_swf_uri: env
.get_method_id(class, "getSwfUri", "()Ljava/lang/String;")
.expect("getSwfUri must exist"),
get_trace_output: env
.get_method_id(class, "getTraceOutput", "()Ljava/lang/String;")
.expect("getTraceOutput must exist"),
get_loc_in_window: env
.get_method_id(class, "getLocInWindow", "()[I")
.expect("getLocInWindow must exist"),
get_android_data_storage_dir: env
.get_method_id(class, "getAndroidDataStorageDir", "()Ljava/lang/String;")
.expect("getAndroidDataStorageDir must exist"),
});
}
}

View File

@ -18,13 +18,16 @@ use std::sync::mpsc::Sender;
use std::sync::{mpsc, MutexGuard};
use std::time::Duration;
use std::{
panic,
sync::{Arc, Mutex},
thread,
time::Instant,
};
use wgpu::rwh::{AndroidDisplayHandle, HasWindowHandle, RawDisplayHandle};
use android_activity::input::{InputEvent, KeyAction, MotionAction};
use android_activity::{AndroidApp, AndroidAppWaker, InputStatus, MainEvent, PollEvent};
use backtrace::Backtrace;
use jni::objects::JClass;
use audio::AAudioAudioBackend;
@ -565,7 +568,69 @@ pub unsafe extern "C" fn Java_rs_ruffle_PlayerActivity_clearContextMenu(
#[no_mangle]
#[allow(clippy::missing_safety_doc)]
pub unsafe extern "C" fn Java_rs_ruffle_PlayerActivity_nativeInit(mut env: JNIEnv, class: JClass) {
pub unsafe extern "C" fn Java_rs_ruffle_PlayerActivity_nativeInit(
mut env: JNIEnv,
class: JClass,
crash_callback: JObject,
) {
let crash_callback = env.new_global_ref(crash_callback).unwrap();
let jvm = env.get_java_vm().unwrap();
android_logger::init_once(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Info)
.with_tag("ruffle")
.with_filter(
android_logger::FilterBuilder::new()
.parse("warn,ruffle=info")
.build(),
),
);
panic::set_hook(Box::new(move |info| {
let backtrace = Backtrace::new();
let thread = thread::current();
let thread = thread.name().unwrap_or("<unnamed>");
let message = match info.payload().downcast_ref::<&'static str>() {
Some(s) => *s,
None => match info.payload().downcast_ref::<String>() {
Some(s) => &**s,
None => "Box<Any>",
},
};
let full = match info.location() {
Some(location) => format!(
"thread '{}' panicked at '{}': {}:{}\n{:?}",
thread,
message,
location.file(),
location.line(),
backtrace
),
None => format!(
"thread '{}' panicked at '{}'\n{:?}",
thread, message, backtrace
),
};
log::error!(target: "panic","{}", full);
let mut env = jvm.attach_current_thread().unwrap();
if env.exception_check().unwrap() {
// There's a pending exception, java will discover this on their own
} else {
let java_message = env.new_string(full).unwrap();
let crash_callback = env.new_global_ref(&crash_callback).unwrap();
env.call_method(
crash_callback,
"onCrash",
"(Ljava/lang/String;)V",
&[(&java_message).into()],
)
.unwrap();
}
}));
JavaInterface::init(&mut env, &class)
}
@ -591,19 +656,6 @@ fn get_view_size() -> Result<(i32, i32), Box<dyn std::error::Error>> {
#[no_mangle]
fn android_main(app: AndroidApp) {
android_logger::init_once(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Info)
.with_tag("ruffle")
.with_filter(
android_logger::FilterBuilder::new()
.parse("warn,ruffle=info")
.build(),
),
);
log_panics::init();
log::info!("Starting android_main...");
run(app);
}