From f225588ec6143e805cb6337835e4966b7f529250 Mon Sep 17 00:00:00 2001 From: Miyoung Shin Date: Wed, 27 Nov 2024 15:29:53 +0900 Subject: [PATCH] Introduce OverlayWidget & Use it by PaymentHandler --- .../wolvic/browser/api/impl/SessionImpl.java | 25 +- .../wolvic/browser/api/impl/TabImpl.java | 29 ++- .../api/impl/TabWebContentsObserver.java | 64 ++++- .../browser/api/impl/TextInputImpl.java | 4 +- .../com/igalia/wolvic/VRBrowserActivity.java | 3 + .../igalia/wolvic/browser/api/WSession.java | 23 ++ .../igalia/wolvic/browser/engine/Session.java | 14 ++ .../ui/widgets/OverlayContentWidget.java | 228 ++++++++++++++++++ .../wolvic/ui/widgets/WindowWidget.java | 22 ++ 9 files changed, 402 insertions(+), 10 deletions(-) create mode 100644 app/src/common/shared/com/igalia/wolvic/ui/widgets/OverlayContentWidget.java diff --git a/app/src/common/chromium/com/igalia/wolvic/browser/api/impl/SessionImpl.java b/app/src/common/chromium/com/igalia/wolvic/browser/api/impl/SessionImpl.java index f27890bda6..55434d19e6 100644 --- a/app/src/common/chromium/com/igalia/wolvic/browser/api/impl/SessionImpl.java +++ b/app/src/common/chromium/com/igalia/wolvic/browser/api/impl/SessionImpl.java @@ -25,6 +25,7 @@ import org.chromium.wolvic.DownloadManagerBridge; import org.chromium.wolvic.PasswordForm; import org.chromium.wolvic.PermissionManagerBridge; +import org.chromium.wolvic.TabCompositorView; import org.chromium.wolvic.UserDialogManagerBridge; import java.io.InputStream; @@ -114,8 +115,8 @@ public void setActive(boolean active) { if (mTab == null) return; - assert mTab.getContentView() != null; - WebContents webContents = mTab.getContentView().getWebContents(); + assert mTab.getActiveWebContents() != null; + WebContents webContents = mTab.getActiveWebContents(); if (active) { webContents.onShow(); } else { @@ -129,8 +130,8 @@ public void setActive(boolean active) { public void setFocused(boolean focused) { if (mTab == null) return; - assert mTab.getContentView() != null; - mTab.getContentView().getWebContents().setFocus(focused); + assert mTab.getActiveWebContents() != null; + mTab.getActiveWebContents().setFocus(focused); } @Override @@ -215,6 +216,20 @@ public void releaseDisplay(@NonNull WDisplay display) { getTextInput().setView(null); } + public WDisplay acquireOverlayDisplay(TabCompositorView compositorView) { + SettingsStore settings = SettingsStore.getInstance(mRuntime.getContext()); + WDisplay display = new DisplayImpl(this, compositorView); + mRuntime.getContainerView().addView(compositorView, + new ViewGroup.LayoutParams(settings.getWindowWidth() / 2, settings.getWindowHeight() / 2)); + getTextInput().setView(getContentView()); + return display; + } + + public void releaseOverlayDisplay(TabCompositorView compositorView) { + mRuntime.getContainerView().removeView(compositorView); + getTextInput().setView(null); + } + @Override public void restoreState(@NonNull WSessionState state) { @@ -457,7 +472,7 @@ public TabImpl getTab() { } public ViewGroup getContentView() { - return mTab != null ? mTab.getContentView() : null; + return mTab != null ? mTab.getActiveContentView() : null; } // The onReadyCallback() mechanism is really limited because it heavily depends on renderers diff --git a/app/src/common/chromium/com/igalia/wolvic/browser/api/impl/TabImpl.java b/app/src/common/chromium/com/igalia/wolvic/browser/api/impl/TabImpl.java index b5f2fe8f34..c116ac7a1d 100644 --- a/app/src/common/chromium/com/igalia/wolvic/browser/api/impl/TabImpl.java +++ b/app/src/common/chromium/com/igalia/wolvic/browser/api/impl/TabImpl.java @@ -4,18 +4,25 @@ import androidx.annotation.NonNull; +import org.chromium.components.embedder_support.view.ContentView; import org.chromium.content_public.browser.MediaSessionObserver; import org.chromium.content_public.browser.SelectionPopupController; import org.chromium.content_public.browser.WebContents; import org.chromium.wolvic.Tab; +import org.chromium.wolvic.TabCompositorView; /** - * Controlls a single tab content in a browser for chromium backend. + * Controls a single tab content in a browser for chromium backend. */ public class TabImpl extends Tab { private TabMediaSessionObserver mTabMediaSessionObserver; private TabWebContentsDelegate mTabWebContentsDelegate; private TabWebContentsObserver mWebContentsObserver; + private WebContents mPaymentHandlerWebContents; + // TODO: Need to Payment's mediator + private ContentView mPaymentHandlerContentView; + private TabCompositorView mPaymentHandlerCompositorView; + public TabImpl(@NonNull Context context, @NonNull SessionImpl session, WebContents webContents) { super(context, session.getSettings().getUsePrivateMode(), webContents); @@ -27,7 +34,7 @@ private void registerCallbacks(@NonNull SessionImpl session) { mTabWebContentsDelegate = new TabWebContentsDelegate(session, mWebContents); setWebContentsDelegate(mWebContents, mTabWebContentsDelegate); - mWebContentsObserver = new TabWebContentsObserver(mWebContents, session); + mWebContentsObserver = new TabWebContentsObserver(this, session); SelectionPopupController controller = SelectionPopupController.fromWebContents(mWebContents); @@ -47,4 +54,22 @@ public void onMediaFullscreen(boolean isFullscreen) { public void purgeHistory() { mWebContents.getNavigationController().clearHistory(); } + + public void setPaymentWebContents(WebContents webContents, ContentView contentView, TabCompositorView compositorView) { + mPaymentHandlerWebContents = webContents; + mPaymentHandlerContentView = contentView; + mPaymentHandlerCompositorView = compositorView; + } + + public WebContents getActiveWebContents() { + return mPaymentHandlerWebContents != null ? mPaymentHandlerWebContents : mWebContents; + } + + public ContentView getActiveContentView() { + return mPaymentHandlerContentView != null ? mPaymentHandlerContentView : getContentView(); + } + + public TabCompositorView getActiveCompositorView() { + return mPaymentHandlerCompositorView != null ? mPaymentHandlerCompositorView : getCompositorView(); + } } diff --git a/app/src/common/chromium/com/igalia/wolvic/browser/api/impl/TabWebContentsObserver.java b/app/src/common/chromium/com/igalia/wolvic/browser/api/impl/TabWebContentsObserver.java index ce77dc15af..721fd4098d 100644 --- a/app/src/common/chromium/com/igalia/wolvic/browser/api/impl/TabWebContentsObserver.java +++ b/app/src/common/chromium/com/igalia/wolvic/browser/api/impl/TabWebContentsObserver.java @@ -1,24 +1,39 @@ package com.igalia.wolvic.browser.api.impl; +import android.content.Context; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.igalia.wolvic.browser.api.WDisplay; import com.igalia.wolvic.browser.api.WSession; import com.igalia.wolvic.browser.api.WWebRequestError; +import org.chromium.base.ContextUtils; +import org.chromium.components.embedder_support.view.ContentView; import org.chromium.content_public.browser.LifecycleState; +import org.chromium.content_public.browser.LoadUrlParams; import org.chromium.content_public.browser.NavigationHandle; import org.chromium.content_public.browser.WebContents; import org.chromium.content_public.browser.WebContentsObserver; +import org.chromium.ui.base.ActivityWindowAndroid; +import org.chromium.ui.base.IntentRequestTracker; +import org.chromium.ui.base.ViewAndroidDelegate; +import org.chromium.ui.base.WindowAndroid; import org.chromium.url.GURL; +import org.chromium.wolvic.TabCompositorView; +import org.chromium.wolvic.WolvicWebContentsFactory; import java.security.cert.X509Certificate; public class TabWebContentsObserver extends WebContentsObserver { private @NonNull SessionImpl mSession; + private @NonNull TabImpl mTab; + private WebContentsObserver mPaymentWebContentsObserver; - public TabWebContentsObserver(WebContents webContents, @NonNull SessionImpl session) { - super(webContents); + public TabWebContentsObserver(TabImpl tab, @NonNull SessionImpl session) { + super(tab.getActiveWebContents()); + mTab = tab; mSession = session; } @@ -106,6 +121,51 @@ public void didStopLoading(GURL url, boolean isKnownValid) { dispatchCanGoBackOrForward(); } + @Override + public void onCreateNewPaymentHandler(final WebContents newWebContents) { + WSession.ContentDelegate contentDelegate = mSession.getContentDelegate(); + if (contentDelegate == null) { + return; + } + + assert newWebContents.getViewAndroidDelegate() != null + : "WebContents should be initialized."; + WindowAndroid windowAndroid = newWebContents.getTopLevelNativeWindow(); + Context context = mWebContents.get().getTopLevelNativeWindow().getContext().get(); + final TabCompositorView compositorView = new TabCompositorView(context); + compositorView.onNativeLibraryLoaded(windowAndroid); + windowAndroid.setAnimationPlaceholderView(compositorView); + + ViewAndroidDelegate viewDelegate = newWebContents.getViewAndroidDelegate(); + assert viewDelegate.getContainerView() instanceof ContentView + : "WebContents should not set container views other than ContentView."; + + mTab.setPaymentWebContents(newWebContents, (ContentView) viewDelegate.getContainerView(), compositorView); + + WDisplay display = mSession.acquireOverlayDisplay(compositorView); + contentDelegate.onShowPaymentHandler(mSession, display, () -> { + if (newWebContents.isDestroyed()) { + return; + } + mTab.setPaymentWebContents(null, null, null); + newWebContents.destroy(); + }); + + // Show Compositor View after attaching to the parent view. + compositorView.setCurrentWebContents(newWebContents); + + mPaymentWebContentsObserver = new WebContentsObserver(newWebContents) { + @Override + public void destroy() { + mSession.releaseOverlayDisplay(compositorView); + mTab.setPaymentWebContents(null, null, null); + + contentDelegate.onHidePaymentHandler(mSession); + newWebContents.removeObserver(this); + } + }; + } + @Override public void didFailLoad(boolean isInPrimaryMainFrame, int errorCode, GURL failingUrl, @LifecycleState int rfhLifecycleState) { @Nullable WSession.ProgressDelegate delegate = mSession.getProgressDelegate(); diff --git a/app/src/common/chromium/com/igalia/wolvic/browser/api/impl/TextInputImpl.java b/app/src/common/chromium/com/igalia/wolvic/browser/api/impl/TextInputImpl.java index 66ff7bedff..f6470aa11c 100644 --- a/app/src/common/chromium/com/igalia/wolvic/browser/api/impl/TextInputImpl.java +++ b/app/src/common/chromium/com/igalia/wolvic/browser/api/impl/TextInputImpl.java @@ -19,6 +19,7 @@ import org.chromium.content_public.browser.ImeAdapter; import org.chromium.content_public.browser.InputMethodManagerWrapper; +import org.chromium.content_public.browser.WebContents; import org.chromium.ui.base.WindowAndroid; public class TextInputImpl implements WTextInput { @@ -180,6 +181,7 @@ public WSession.TextInputDelegate getDelegate() { } private ImeAdapter getImeAdapter() { - return mSession.getTab() != null ? mSession.getTab().getImeAdapter() : null; + WebContents webContents = mSession.getTab() != null ? mSession.getTab().getActiveWebContents() : null; + return webContents != null ? ImeAdapter.fromWebContents(webContents) : null; } } diff --git a/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java b/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java index 0e9801afe9..2af89edb93 100644 --- a/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java +++ b/app/src/common/shared/com/igalia/wolvic/VRBrowserActivity.java @@ -71,6 +71,7 @@ import com.igalia.wolvic.ui.widgets.AppServicesProvider; import com.igalia.wolvic.ui.widgets.KeyboardWidget; import com.igalia.wolvic.ui.widgets.NavigationBarWidget; +import com.igalia.wolvic.ui.widgets.OverlayContentWidget; import com.igalia.wolvic.ui.widgets.RootWidget; import com.igalia.wolvic.ui.widgets.TrayWidget; import com.igalia.wolvic.ui.widgets.UISurfaceTextureRenderer; @@ -1107,6 +1108,8 @@ void handleMotionEvent(final int aHandle, final int aDevice, final boolean aFocu if (!windowWidget.isLibraryVisible()) { scale = 1.0f; } + } else if (widget instanceof OverlayContentWidget) { + scale = 1.0f; } final float x = aX / scale; final float y = aY / scale; diff --git a/app/src/common/shared/com/igalia/wolvic/browser/api/WSession.java b/app/src/common/shared/com/igalia/wolvic/browser/api/WSession.java index 021355b5b3..3b0c704fbb 100644 --- a/app/src/common/shared/com/igalia/wolvic/browser/api/WSession.java +++ b/app/src/common/shared/com/igalia/wolvic/browser/api/WSession.java @@ -317,6 +317,29 @@ WResult onSlowScript( */ @UiThread default void onShowDynamicToolbar(@NonNull final WSession aSession) {} + + public interface OnPaymentHandlerCallback { + void onDismiss(); + } + + /** + * The view should display its payment handler, overlayed on the active tab. + * + * @param aSession ISession that initiated the callback. + * @param aDisplay IDisplay that initiated the callback. + * @param callback Callback interface. + */ + @UiThread + default void onShowPaymentHandler(@NonNull final WSession aSession, + @NonNull final WDisplay aDisplay, + @NonNull final OnPaymentHandlerCallback callback) {} + /** + * The view should hide its payment handler. + * + * @param aSession ISession that initiated the callback. + */ + @UiThread + default void onHidePaymentHandler(@NonNull final WSession aSession) {} } interface NavigationDelegate { diff --git a/app/src/common/shared/com/igalia/wolvic/browser/engine/Session.java b/app/src/common/shared/com/igalia/wolvic/browser/engine/Session.java index 6a21ea7f74..e79fa08607 100644 --- a/app/src/common/shared/com/igalia/wolvic/browser/engine/Session.java +++ b/app/src/common/shared/com/igalia/wolvic/browser/engine/Session.java @@ -1422,6 +1422,20 @@ public void onExternalResponse(@NonNull WSession aSession, @NonNull WWebResponse } } + @Override + public void onShowPaymentHandler(@NonNull final WSession aSession, @NonNull WDisplay aDisplay, @NonNull OnPaymentHandlerCallback callback) { + for (WSession.ContentDelegate listener : mContentListeners) { + listener.onShowPaymentHandler(aSession, aDisplay, callback); + } + } + + @Override + public void onHidePaymentHandler(@NonNull final WSession aSession) { + for (WSession.ContentDelegate listener : mContentListeners) { + listener.onHidePaymentHandler(aSession); + } + } + // TextInput Delegate @Override diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/OverlayContentWidget.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/OverlayContentWidget.java new file mode 100644 index 0000000000..5e3eb94aa3 --- /dev/null +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/OverlayContentWidget.java @@ -0,0 +1,228 @@ +package com.igalia.wolvic.ui.widgets; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.Surface; + +import androidx.annotation.NonNull; + +import com.igalia.wolvic.R; +import com.igalia.wolvic.VRBrowserApplication; +import com.igalia.wolvic.browser.api.WDisplay; +import com.igalia.wolvic.browser.api.WSession; + +import java.util.concurrent.Executor; + +public class OverlayContentWidget extends UIWidget implements WidgetManagerDelegate.WorldClickListener { + private Surface mSurface; + private int mSurfaceWidth; + private int mSurfaceHeight; + private Runnable mFirstDrawCallback; + private WSession mSession; + private WDisplay mDisplay; + private WSession.ContentDelegate.OnPaymentHandlerCallback mCallback; + private Executor mUIThreadExecutor; + private Handler mHandler; + + public OverlayContentWidget(Context aContext) { + super(aContext); + initialize(aContext); + } + + public OverlayContentWidget(Context aContext, AttributeSet aAttrs) { + super(aContext, aAttrs); + initialize(aContext); + } + + public OverlayContentWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) { + super(aContext, aAttrs, aDefStyle); + initialize(aContext); + } + + public void setDelegates(@NonNull WSession session, @NonNull WDisplay display, + @NonNull WSession.ContentDelegate.OnPaymentHandlerCallback callback) { + mSession = session; + mDisplay = display; + mCallback = callback; + } + + @Override + public void releaseWidget() { + mWidgetManager.removeWorldClickListener(this); + mCallback.onDismiss(); + + super.releaseWidget(); + } + + @Override + protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { + aPlacement.visible = false; + aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.tabs_width); + aPlacement.height = WidgetPlacement.dpDimension(getContext(), R.dimen.tabs_height); + + aPlacement.parentAnchorX = 0.5f; + aPlacement.parentAnchorY = 0.0f; + aPlacement.anchorX = 0.5f; + aPlacement.anchorY = 0.5f; + aPlacement.translationY = WidgetPlacement.unitFromMeters(getContext(), R.dimen.settings_world_y) - + WidgetPlacement.unitFromMeters(getContext(), R.dimen.window_world_y); + updatePlacementTranslationZ(); + } + + @Override + public void updatePlacementTranslationZ() { + getPlacement().translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.settings_world_z) - + WidgetPlacement.getWindowWorldZMeters(getContext()); + } + + private void initialize(Context aContext) { + mUIThreadExecutor = ((VRBrowserApplication)aContext.getApplicationContext()).getExecutors().mainThread(); + mHandler = new Handler(Looper.getMainLooper()); + + mWidgetManager.addWorldClickListener(this); + } + + @Override + public void attachToWindow(@NonNull WindowWidget window) { + mWidgetPlacement.parentHandle = window.getHandle(); + } + + // View + @Override + public boolean onKeyPreIme(int aKeyCode, KeyEvent aEvent) { + if (super.onKeyPreIme(aKeyCode, aEvent)) { + return true; + } + return mSession.getTextInput().onKeyPreIme(aKeyCode, aEvent); + } + + @Override + public boolean onKeyUp(int aKeyCode, KeyEvent aEvent) { + if (super.onKeyUp(aKeyCode, aEvent)) { + return true; + } + return mSession.getTextInput().onKeyUp(aKeyCode, aEvent); + } + + @Override + public boolean onKeyDown(int aKeyCode, KeyEvent aEvent) { + if (super.onKeyDown(aKeyCode, aEvent)) { + return true; + } + return mSession.getTextInput().onKeyDown(aKeyCode, aEvent); + } + + @Override + public boolean onKeyLongPress(int aKeyCode, KeyEvent aEvent) { + if (super.onKeyLongPress(aKeyCode, aEvent)) { + return true; + } + return mSession.getTextInput().onKeyLongPress(aKeyCode, aEvent); + } + + @Override + public boolean onKeyMultiple(int aKeyCode, int repeatCount, KeyEvent aEvent) { + if (super.onKeyMultiple(aKeyCode, repeatCount, aEvent)) { + return true; + } + return mSession.getTextInput().onKeyMultiple(aKeyCode, repeatCount, aEvent); + } + + @Override + public void handleHoverEvent(MotionEvent aEvent) { + mSession.getPanZoomController().onMotionEvent(aEvent); + } + + @Override + public void handleTouchEvent(MotionEvent aEvent) { + if (aEvent.getActionMasked() == MotionEvent.ACTION_DOWN) { + requestFocus(); + requestFocusFromTouch(); + } + + mSession.getPanZoomController().onTouchEvent(aEvent); + } + + @Override + public boolean onTouchEvent(MotionEvent aEvent) { + mSession.getPanZoomController().onTouchEvent(aEvent); + return true; + } + + @Override + public boolean onGenericMotionEvent(MotionEvent aEvent) { + mSession.getPanZoomController().onMotionEvent(aEvent); + return true; + } + + @Override + public void setSurfaceTexture(SurfaceTexture aTexture, final int aWidth, final int aHeight, Runnable aFirstDrawCallback) { + mFirstDrawCallback = aFirstDrawCallback; + if (aTexture == null) { + setWillNotDraw(true); + return; + } + mSurfaceWidth = aWidth; + mSurfaceHeight = aHeight; + mTexture = aTexture; + aTexture.setDefaultBufferSize(aWidth, aHeight); + mSurface = new Surface(aTexture); + callSurfaceChanged(); + if (mFirstDrawCallback != null) { + mUIThreadExecutor.execute(mFirstDrawCallback); + mFirstDrawCallback = null; + } + } + + @Override + public void setSurface(Surface aSurface, final int aWidth, final int aHeight, Runnable aFirstDrawCallback) { + mSurfaceWidth = aWidth; + mSurfaceHeight = aHeight; + mSurface = aSurface; + mFirstDrawCallback = aFirstDrawCallback; + if (mSurface != null) { + callSurfaceChanged(); + } else { + mDisplay.surfaceDestroyed(); + } + if (mFirstDrawCallback != null) { + mUIThreadExecutor.execute(mFirstDrawCallback); + mFirstDrawCallback = null; + } + } + + private void callSurfaceChanged() { + if (mSurface != null) { + mDisplay.surfaceChanged(mSurface, 0, 0, mSurfaceWidth, mSurfaceHeight); + } + } + + @Override + public void resizeSurface(final int aWidth, final int aHeight) { + mSurfaceWidth = aWidth; + mSurfaceHeight = aHeight; + if (mTexture != null) { + mTexture.setDefaultBufferSize(aWidth, aHeight); + } + + if (mTexture != null && mSurface != null) { + callSurfaceChanged(); + } + } + + @Override + public void onWorldClick() { + post(this::onDismiss); + mCallback.onDismiss(); + } + + @Override + public boolean isDialog() { + return true; + } +} \ No newline at end of file diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/WindowWidget.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/WindowWidget.java index ba3db0c8cc..4756ce3ffb 100644 --- a/app/src/common/shared/com/igalia/wolvic/ui/widgets/WindowWidget.java +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/WindowWidget.java @@ -48,6 +48,7 @@ import com.igalia.wolvic.browser.SettingsStore; import com.igalia.wolvic.browser.VideoAvailabilityListener; import com.igalia.wolvic.browser.api.WAllowOrDeny; +import com.igalia.wolvic.browser.api.WDisplay; import com.igalia.wolvic.browser.api.WMediaSession; import com.igalia.wolvic.browser.api.WResult; import com.igalia.wolvic.browser.api.WSession; @@ -125,6 +126,7 @@ public class WindowWidget extends UIWidget implements SessionChangeListener, private PromptDialogWidget mAppDialog; private ContextMenuWidget mContextMenu; private SelectionActionWidget mSelectionMenu; + private OverlayContentWidget mPaymentHandler; private int mWidthBackup; private int mHeightBackup; private int mBorderWidth; @@ -1870,6 +1872,26 @@ public void onExternalResponse(@NonNull WSession aSession, @NonNull WWebResponse } } + @Override + public void onShowPaymentHandler(@NonNull WSession session, @NonNull WDisplay display, @NonNull OnPaymentHandlerCallback callback) { + assert mPaymentHandler == null; + mPaymentHandler = new OverlayContentWidget(getContext()); + mPaymentHandler.setDelegates(session, display, callback); + + mPaymentHandler.getPlacement().parentHandle = getHandle(); + mPaymentHandler.attachToWindow(this); + mPaymentHandler.show(KEEP_FOCUS); + } + + @Override + public void onHidePaymentHandler(@NonNull WSession session) { + if (mPaymentHandler != null) { + mPaymentHandler.hide(REMOVE_WIDGET); + mPaymentHandler.releaseWidget(); + mPaymentHandler = null; + } + } + // VideoAvailabilityListener @Override