Skip to content
Merged
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
30 changes: 30 additions & 0 deletions framework/src/org/apache/cordova/CordovaWebView.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,36 @@ public interface CordovaWebView {

boolean backHistory();

/**
* Performs Cordova's handling of a single Android back action.
*
* <p>A back press can reach Cordova in two ways:</p>
* <ul>
* <li>As a back-press callback. Cordova's {@link CoreAndroid} plugin registers one
* while the back button is overridden, that is while a JavaScript "backbutton"
* listener exists, and calls this method from it.</li>
* <li>As a back key event, handled by the web view's key event handling. This
* also works without a "backbutton" listener.</li>
* </ul>
* <p>Depending on the Android version, one back press may arrive through one or both
* ways. The implementation makes sure that the press is handled only once.</p>
*
* <p>Android invokes the back-press callback on the UI thread. This method does not
* switch threads itself, so any other caller must also call it on the UI thread.</p>
*
* <p>The first applicable action is performed, in this order:</p>
* <ol>
* <li>Dismiss a custom view, such as a full-screen video.</li>
* <li>Fire the JavaScript "backbutton" event, if the back button is overridden.</li>
* <li>Navigate back in the WebView history, if possible.</li>
* </ol>
*
* @return true if one of these actions was performed and the back action is consumed.
* false if Cordova did nothing, so the caller should let Android handle the
* back action, for example by finishing the activity.
*/
boolean handleBackButton();

void handlePause(boolean keepRunning);

void onNewIntent(Intent intent);
Expand Down
78 changes: 62 additions & 16 deletions framework/src/org/apache/cordova/CordovaWebViewImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.SystemClock;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
Expand Down Expand Up @@ -64,6 +65,12 @@
private NativeToJsMessageQueue nativeToJsMessageQueue;
private EngineClient engineClient = new EngineClient();
private boolean hasPausedEver;
// A back press can reach us as a back-press callback (handleBackButton) and as back key
// events (EngineClient.onDispatchKeyEvent). Depending on the Android version, one or
// both arrive. On Android 16 both do: the callback runs when the key is released, just
// before its key-up event. This is the time when the callback last handled a back
// press, or -1 if none, so that key-up can be ignored.
private long lastBackCallbackTime = -1;

// The URL passed to loadUrl(), not necessarily the URL of the current page.
String loadedUrl;
Expand Down Expand Up @@ -402,8 +409,49 @@
appPlugin.fireJavascriptEvent(event);
}

/**
* Called by the Android back-press callback. Performs the back action and, if it was
* handled, records the time so the duplicate key event of the same press is ignored.
*/
@Override
public boolean handleBackButton() {
long callbackTime = SystemClock.uptimeMillis();
boolean handled = performBackButtonAction();
if (handled) {
lastBackCallbackTime = callbackTime;
}
return handled;
}

/**
* Performs the first applicable back action: dismiss a custom view, fire the
* JavaScript "backbutton" event if the back button is overridden, or navigate back in
* the WebView history. Called by {@link #handleBackButton()} (back-press callback) and
* by the back key handling in {@link EngineClient#onDispatchKeyEvent(KeyEvent)}
* (back key event).
*
* @return true if an action was performed, false if Android should handle the back action
*/
private boolean performBackButtonAction() {
// 1. A custom view (for example a full-screen video) is showing: close it.
if (mCustomView != null) {
hideCustomView();
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call already existed in the Back key handler and has been moved into the shared back-action helper. Preserving custom-view dismissal is necessary so closing a full-screen view does not also fire backbutton and navigate the app. Modernizing the deprecated custom-view API should be handled separately; this PR preserves the existing behavior.

return true;
}
// 2. The app listens to the "backbutton" event: let JavaScript decide what to do.
if (boundKeyCodes.contains(KeyEvent.KEYCODE_BACK)) {
sendJavascriptEvent("backbutton");
return true;
}
// 3. Otherwise go back in the WebView history. Returns false if there is none.
return engine.goBack();
}

@Override
public void setButtonPlumbedToJs(int keyCode, boolean override) {
if (keyCode == KeyEvent.KEYCODE_BACK && !override) {
lastBackCallbackTime = -1;
}
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_DOWN:
case KeyEvent.KEYCODE_VOLUME_UP:
Expand Down Expand Up @@ -612,20 +660,23 @@
@Override
public Boolean onDispatchKeyEvent(KeyEvent event) {
int keyCode = event.getKeyCode();
boolean isBackButton = keyCode == KeyEvent.KEYCODE_BACK;
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (isBackButton && mCustomView != null) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
// On Android 16, one back press reaches us twice: the back callback runs
// when the key is released, just before the key-up event of the same press
// arrives here. Skip that key-up (an event not newer than the callback)
// so back is handled only once.
if (event.getEventTime() <= lastBackCallbackTime) {
return true;
} else if (boundKeyCodes.contains(keyCode)) {
return true;
} else if (isBackButton) {
return engine.canGoBack();
}
} else if (event.getAction() == KeyEvent.ACTION_UP) {
if (isBackButton && mCustomView != null) {
hideCustomView();
if (event.getAction() == KeyEvent.ACTION_DOWN) {
return mCustomView != null || boundKeyCodes.contains(keyCode) || engine.canGoBack();
} else if (event.getAction() == KeyEvent.ACTION_UP) {
return performBackButtonAction();
}
} else if (boundKeyCodes.contains(keyCode)) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
return true;
} else if (boundKeyCodes.contains(keyCode)) {
} else if (event.getAction() == KeyEvent.ACTION_UP) {
String eventName = null;
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_DOWN:
Expand All @@ -640,16 +691,11 @@
case KeyEvent.KEYCODE_MENU:
eventName = "menubutton";
break;
case KeyEvent.KEYCODE_BACK:
eventName = "backbutton";
break;
}
if (eventName != null) {
sendJavascriptEvent(eventName);
return true;
}
} else if (isBackButton) {
return engine.goBack();
}
}
return null;
Expand Down
34 changes: 18 additions & 16 deletions framework/src/org/apache/cordova/CoreAndroid.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ Licensed to the Apache Software Foundation (ASF) under one
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.SystemClock;
import android.content.IntentFilter;
import android.telephony.TelephonyManager;
import android.view.KeyEvent;
Expand Down Expand Up @@ -269,7 +268,7 @@ public void run() {
backCallback = new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
dispatchBackKeyEvent();
handleBackPressed();
}
};
OnBackPressedDispatcherOwner backPressedDispatcherOwner = cordova.getActivity();
Expand All @@ -290,23 +289,26 @@ public void handleOnBackPressed() {
});
}

private void dispatchBackKeyEvent() {
// Build a synthetic BACK key press (DOWN + UP) and route it through the WebView.
// This lets Cordova's existing key handling fire the JS "backbutton" event.
final long eventTime = SystemClock.uptimeMillis();
final KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK, 0);
final KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK, 0);

final boolean handledDown = webView.getView().dispatchKeyEvent(downEvent);
final boolean handledUp = webView.getView().dispatchKeyEvent(upEvent);

// If either event was consumed, Cordova/WebView already handled back behavior.
if (handledDown || handledUp) {
/**
* Called by the back-press callback registered in {@link #overrideBackbutton(boolean)},
* so it only runs while the back button is overridden. The JavaScript side enables the
* override when the first "backbutton" event listener is added and disables it when
* the last one is removed.
*
* <p>Lets the web view handle the back press first. Only if it reports the press as
* unhandled is the press passed on to Android's default handling. Normally this does
* not happen while the override is active, because the JavaScript "backbutton" event
* handles it.</p>
*/
private void handleBackPressed() {
// Let Cordova handle the back press first: close a custom view, fire the JS
// "backbutton" event, or go back in the WebView history. If it did, we are done.
if (webView.handleBackButton()) {
return;
}

// Otherwise, delegate to Android's default back dispatcher.
// Temporarily disable this callback to avoid recursive re-entry.
// Cordova did nothing, so pass the back press on to Android's default handling.
// This callback is disabled meanwhile, otherwise the dispatcher would call it again.
backCallback.setEnabled(false);
try {
cordova.getActivity().getOnBackPressedDispatcher().onBackPressed();
Expand Down
Loading
Loading