Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,13 @@ public void onGlobalLayout() {
@SuppressWarnings("deprecation")
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
// Back is not a surface key. Left unhandled it reaches the activity,
// which delivers it once through dispatchBackPressed: from onKeyUp ->
// onBackPressed below API 33, or from the OnBackInvokedCallback on
// API 33+ (which also carries the system edge gesture). Forwarding it
// here as well produced a second native BackPressed: one closed the
// current panel and the next left the app.
if (keyCode == KeyEvent.KEYCODE_BACK) return false;
if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode != 0) {
int metaState = event.getMetaState();
boolean isRepeat = event.getRepeatCount() > 0;
Expand Down Expand Up @@ -1115,6 +1122,7 @@ public class MakepadActivity
private FrameLayout mRootLayout;
private FrameLayout mApplicationOverlay;
private ApplicationExtension mApplicationExtension;
private android.window.OnBackInvokedCallback mBackInvokedCallback;

/** Optional app Java client. Implementations enqueue blocking work on workers. */
public interface ApplicationExtension {
Expand All @@ -1124,7 +1132,16 @@ public interface ApplicationExtension {
void onIntent(Intent intent);
void onDestroy();
default boolean onActivityResult(int requestCode, int resultCode, Intent data) { return false; }
/** Return true to consume Back before it reaches the native widget tree. */
default boolean onBackPressed() { return false; }
/**
* True when the extension registers its own Android 13+
* OnBackInvokedCallback (calling {@link MakepadActivity#onBackPressed}).
* The activity then registers none and drops the compatibility Back
* key, so each Back is delivered once. Most extensions should leave
* this false and implement {@link #onBackPressed} instead.
*/
default boolean usesSystemBackCallback() { return false; }
}

private void createApplicationExtension() {
Expand Down Expand Up @@ -1532,6 +1549,7 @@ public void onCreate(Bundle savedInstanceState) {
}
MakepadNative.activityOnCreate(this);
createApplicationExtension();
registerBackInvokedCallback();
registerPhysicalKeyboardListener();

mVideoPlaybackThread = new HandlerThread("VideoPlayerThread");
Expand Down Expand Up @@ -1635,6 +1653,7 @@ protected void onStop() {

@Override
protected void onDestroy() {
unregisterBackInvokedCallback();
if (mApplicationExtension != null) {
mApplicationExtension.onDestroy();
mApplicationExtension = null;
Expand Down Expand Up @@ -1717,6 +1736,12 @@ public void onLowMemory() {
// key (BACK included) and forwards it to Rust, so onBackPressed never runs.
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// An extension that owns the system Back callback receives every Back
// through it; its compatibility key must not navigate a second time.
if (Build.VERSION.SDK_INT >= 33 && event.getKeyCode() == KeyEvent.KEYCODE_BACK
&& extensionOwnsSystemBack()) {
return true;
}
if (mQrScanning && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (event.getAction() == KeyEvent.ACTION_UP) closeQrScanner("cancelled");
return true;
Expand All @@ -1727,11 +1752,47 @@ public boolean dispatchKeyEvent(KeyEvent event) {
@Override
@SuppressWarnings("deprecation")
public void onBackPressed() {
dispatchBackPressed();
}

/** The single delivery point for Back: key, button and system gesture. */
private void dispatchBackPressed() {
if (mQrScanning) {
closeQrScanner("cancelled");
return;
}
if (mApplicationExtension != null && mApplicationExtension.onBackPressed()) return;
super.onBackPressed();
// No super.onBackPressed(): Activity's default finishes (or, on API
// 31+, backgrounds) the activity before the widget tree has seen the
// event. Navigation belongs to the native BackPressed handlers.
MakepadNative.onBackPressed();
}

private boolean extensionOwnsSystemBack() {
return mApplicationExtension != null && mApplicationExtension.usesSystemBackCallback();
}

// Android 13+ delivers Back, including the system edge gesture, to
// OnBackInvokedCallbacks when the manifest opts in
// (android:enableOnBackInvokedCallback, which cargo-makepad writes);
// onBackPressed is then no longer called. PRIORITY_DEFAULT leaves the
// IME's own Back (dismissing the keyboard) ahead of app navigation.
private void registerBackInvokedCallback() {
if (Build.VERSION.SDK_INT < 33 || mBackInvokedCallback != null || extensionOwnsSystemBack()) {
return;
}
mBackInvokedCallback = this::dispatchBackPressed;
getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
android.window.OnBackInvokedDispatcher.PRIORITY_DEFAULT, mBackInvokedCallback);
}

private void unregisterBackInvokedCallback() {
if (Build.VERSION.SDK_INT >= 33 && mBackInvokedCallback != null) {
getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mBackInvokedCallback);
mBackInvokedCallback = null;
}
}

@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
Expand Down
21 changes: 21 additions & 0 deletions tools/cargo_makepad/src/android/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ impl AndroidVariant {
android:configChanges="orientation|screenSize|keyboardHidden"
android:exported="true"
android:launchMode="singleTask"
android:enableOnBackInvokedCallback="true"
android:windowSoftInputMode="adjustNothing|stateUnchanged"
android:theme="@style/MakepadLaunchTheme">
<intent-filter>
Expand Down Expand Up @@ -193,6 +194,7 @@ impl AndroidVariant {
android:excludeFromRecents="false"
android:exported="true"
android:launchMode="singleTask"
android:enableOnBackInvokedCallback="true"
android:screenOrientation="landscape"
android:windowSoftInputMode="adjustNothing|stateUnchanged"
android:theme="@style/MakepadLaunchTheme"
Expand All @@ -209,6 +211,7 @@ impl AndroidVariant {
android:excludeFromRecents="false"
android:exported="true"
android:launchMode="singleTask"
android:enableOnBackInvokedCallback="true"
android:screenOrientation="landscape"
android:windowSoftInputMode="adjustNothing|stateUnchanged"
android:theme="@style/MakepadLaunchTheme"
Expand Down Expand Up @@ -283,6 +286,7 @@ Self::Quest=>format!(r#"<?xml version="1.0" encoding="utf-8"?>
android:name="{class_name}"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
android:launchMode="singleTask"
android:enableOnBackInvokedCallback="true"
android:screenOrientation="landscape"
android:excludeFromRecents="false"
android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
Expand All @@ -301,6 +305,7 @@ Self::Quest=>format!(r#"<?xml version="1.0" encoding="utf-8"?>
android:excludeFromRecents="false"
android:exported="true"
android:launchMode="singleTask"
android:enableOnBackInvokedCallback="true"
android:screenOrientation="landscape"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
>
Expand Down Expand Up @@ -927,6 +932,22 @@ mod tests {
assert!(xml.contains("android:launchMode=\"singleTask\""));
}

#[test]
fn every_activity_opts_into_back_invoked_callbacks() {
// MakepadActivity registers one OnBackInvokedCallback on API 33+;
// without the opt-in Android ignores it and finishes the activity
// on a system Back gesture instead.
for variant in [AndroidVariant::Default, AndroidVariant::Quest] {
let xml = variant.manifest_xml(&test_args("App", "dev.makepad.app"));
assert_eq!(
xml.matches("android:launchMode=\"singleTask\"").count(),
xml.matches("android:enableOnBackInvokedCallback=\"true\"").count(),
"every activity needs the Back opt-in: {xml}"
);
assert!(xml.contains("android:enableOnBackInvokedCallback=\"true\""));
}
}

#[test]
fn quest_manifest_uses_splash_and_app_themes() {
let xml = AndroidVariant::Quest.manifest_xml(&test_args("App", "dev.makepad.app"));
Expand Down