1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.vr;
6 
7 import android.annotation.SuppressLint;
8 import android.annotation.TargetApi;
9 import android.content.pm.PackageManager;
10 import android.graphics.Color;
11 import android.graphics.PixelFormat;
12 import android.graphics.Point;
13 import android.graphics.PointF;
14 import android.os.Build;
15 import android.util.DisplayMetrics;
16 import android.view.KeyEvent;
17 import android.view.MotionEvent;
18 import android.view.Surface;
19 import android.view.SurfaceHolder;
20 import android.view.SurfaceView;
21 import android.view.View;
22 import android.view.ViewConfiguration;
23 import android.view.ViewGroup;
24 import android.widget.FrameLayout;
25 
26 import androidx.annotation.VisibleForTesting;
27 
28 import com.google.vr.ndk.base.AndroidCompat;
29 import com.google.vr.ndk.base.GvrLayout;
30 
31 import org.chromium.base.Log;
32 import org.chromium.base.StrictModeContext;
33 import org.chromium.base.ThreadUtils;
34 import org.chromium.base.annotations.CalledByNative;
35 import org.chromium.base.annotations.JNINamespace;
36 import org.chromium.base.annotations.NativeMethods;
37 import org.chromium.base.supplier.ObservableSupplierImpl;
38 import org.chromium.base.task.PostTask;
39 import org.chromium.chrome.R;
40 import org.chromium.chrome.browser.ChromeTabbedActivity;
41 import org.chromium.chrome.browser.app.ChromeActivity;
42 import org.chromium.chrome.browser.compositor.CompositorView;
43 import org.chromium.chrome.browser.offlinepages.OfflinePageUtils;
44 import org.chromium.chrome.browser.page_info.ChromePageInfoControllerDelegate;
45 import org.chromium.chrome.browser.page_info.ChromePermissionParamsListBuilderDelegate;
46 import org.chromium.chrome.browser.tab.EmptyTabObserver;
47 import org.chromium.chrome.browser.tab.RedirectHandlerTabHelper;
48 import org.chromium.chrome.browser.tab.Tab;
49 import org.chromium.chrome.browser.tab.TabAssociatedApp;
50 import org.chromium.chrome.browser.tab.TabBrowserControlsConstraintsHelper;
51 import org.chromium.chrome.browser.tab.TabCreationState;
52 import org.chromium.chrome.browser.tab.TabLaunchType;
53 import org.chromium.chrome.browser.tab.TabObserver;
54 import org.chromium.chrome.browser.tabmodel.ChromeTabCreator;
55 import org.chromium.chrome.browser.tabmodel.EmptyTabModelSelectorObserver;
56 import org.chromium.chrome.browser.tabmodel.TabCreator;
57 import org.chromium.chrome.browser.tabmodel.TabModel;
58 import org.chromium.chrome.browser.tabmodel.TabModelSelector;
59 import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
60 import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver;
61 import org.chromium.chrome.browser.util.VoiceRecognitionUtil;
62 import org.chromium.chrome.browser.vr.keyboard.VrInputMethodManagerWrapper;
63 import org.chromium.components.external_intents.RedirectHandler;
64 import org.chromium.components.page_info.PageInfoController;
65 import org.chromium.content_public.browser.ImeAdapter;
66 import org.chromium.content_public.browser.LoadUrlParams;
67 import org.chromium.content_public.browser.UiThreadTaskTraits;
68 import org.chromium.content_public.browser.ViewEventSink;
69 import org.chromium.content_public.browser.WebContents;
70 import org.chromium.content_public.common.BrowserControlsState;
71 import org.chromium.ui.base.PermissionCallback;
72 import org.chromium.ui.base.WindowAndroid;
73 import org.chromium.ui.display.DisplayAndroid;
74 import org.chromium.ui.display.DisplayAndroidManager;
75 import org.chromium.ui.display.VirtualDisplayAndroid;
76 import org.chromium.ui.modaldialog.DialogDismissalCause;
77 import org.chromium.ui.modaldialog.ModalDialogManager;
78 import org.chromium.ui.widget.UiWidgetFactory;
79 
80 import java.util.ArrayList;
81 
82 /**
83  * This view extends from GvrLayout which wraps a GLSurfaceView that renders VR shell.
84  */
85 @JNINamespace("vr")
86 public class VrShell extends GvrLayout
87         implements SurfaceHolder.Callback, VrInputMethodManagerWrapper.BrowserKeyboardInterface,
88                    EmptySniffingVrViewContainer.EmptyListener, VrDialogManager, VrToastManager {
89     private static final String TAG = "VrShellImpl";
90     private static final float INCHES_TO_METERS = 0.0254f;
91 
92     private final ChromeActivity mActivity;
93     private final CompositorView mCompositorView;
94     private final VrCompositorSurfaceManager mVrCompositorSurfaceManager;
95     private final VrShellDelegate mDelegate;
96     private final VirtualDisplayAndroid mContentVirtualDisplay;
97     private final RedirectHandler mRedirectHandler;
98     private final TabObserver mTabObserver;
99     private final TabModelSelectorObserver mTabModelSelectorObserver;
100     private final View.OnTouchListener mTouchListener;
101     private final boolean mVrBrowsingEnabled;
102 
103     private TabModelSelectorTabObserver mTabModelSelectorTabObserver;
104 
105     private long mNativeVrShell;
106 
107     private View mPresentationView;
108 
109     // The tab that holds the main WebContents.
110     private Tab mTab;
111     private ViewEventSink mViewEventSink;
112     private Boolean mCanGoBack;
113     private Boolean mCanGoForward;
114 
115     private VrWindowAndroid mContentVrWindowAndroid;
116 
117     private boolean mReprojectedRendering;
118 
119     private RedirectHandler mNonVrRedirectHandler;
120     private UiWidgetFactory mNonVrUiWidgetFactory;
121 
122     private TabModelSelector mTabModelSelector;
123     private float mLastContentWidth;
124     private float mLastContentHeight;
125     private float mLastContentDpr;
126     private Boolean mPaused;
127 
128     private boolean mPendingVSyncPause;
129 
130     private AndroidUiGestureTarget mAndroidUiGestureTarget;
131     private AndroidUiGestureTarget mAndroidDialogGestureTarget;
132 
133     private OnDispatchTouchEventCallback mOnDispatchTouchEventForTesting;
134     private Runnable mOnVSyncPausedForTesting;
135 
136     private Surface mContentSurface;
137     private EmptySniffingVrViewContainer mNonVrViews;
138     private VrViewContainer mVrUiViewContainer;
139     private FrameLayout mUiView;
140     private ModalDialogManager mNonVrModalDialogManager;
141     private ModalDialogManager mVrModalDialogManager;
142     private VrModalPresenter mVrModalPresenter;
143     private Runnable mVrDialogDismissHandler;
144 
145     private VrInputMethodManagerWrapper mInputMethodManagerWrapper;
146 
147     private ArrayList<Integer> mUiOperationResults;
148     private ArrayList<Runnable> mUiOperationResultCallbacks;
149 
150     /**
151      * A struct-like object for registering UI operations during tests.
152      */
153     @VisibleForTesting
154     public static class UiOperationData {
155         // The UiTestOperationType of this operation.
156         public int actionType;
157         // The callback to run when the operation completes.
158         public Runnable resultCallback;
159         // The timeout of the operation.
160         public int timeoutMs;
161         // The UserFriendlyElementName to perform the operation on.
162         public int elementName;
163         // The desired visibility status of the element.
164         public boolean visibility;
165     }
166 
VrShell( ChromeActivity activity, VrShellDelegate delegate, TabModelSelector tabModelSelector)167     public VrShell(
168             ChromeActivity activity, VrShellDelegate delegate, TabModelSelector tabModelSelector) {
169         super(activity);
170         mActivity = activity;
171         mDelegate = delegate;
172         mTabModelSelector = tabModelSelector;
173         mVrBrowsingEnabled = mDelegate.isVrBrowsingEnabled();
174 
175         mReprojectedRendering = setAsyncReprojectionEnabled(true);
176         if (mReprojectedRendering) {
177             // No need render to a Surface if we're reprojected. We'll be rendering with surfaceless
178             // EGL.
179             mPresentationView = new FrameLayout(mActivity);
180 
181             // This can show up behind popups on standalone devices, so make sure it's black.
182             mPresentationView.setBackgroundColor(Color.BLACK);
183 
184             // Only enable sustained performance mode when Async reprojection decouples the app
185             // framerate from the display framerate.
186             AndroidCompat.setSustainedPerformanceMode(mActivity, true);
187         } else {
188             if (VrShellDelegate.isDaydreamCurrentViewer()) {
189                 // We need Async Reprojection on when entering VR browsing, because otherwise our
190                 // GL context will be lost every time we're hidden, like when we go to the dashboard
191                 // and come back.
192                 // TODO(mthiesse): Supporting context loss turned out to be hard. We should consider
193                 // spending more effort on supporting this in the future if it turns out to be
194                 // important.
195                 Log.e(TAG, "Could not turn async reprojection on for Daydream headset.");
196                 throw new VrShellDelegate.VrUnsupportedException();
197             }
198             SurfaceView surfaceView = new SurfaceView(mActivity);
199             surfaceView.getHolder().addCallback(this);
200             mPresentationView = surfaceView;
201         }
202 
203         mActivity.getToolbarManager().setProgressBarEnabled(false);
204 
205         DisplayAndroid primaryDisplay = DisplayAndroid.getNonMultiDisplay(activity);
206         mContentVirtualDisplay = VirtualDisplayAndroid.createVirtualDisplay();
207         mContentVirtualDisplay.setTo(primaryDisplay);
208 
209         mContentVrWindowAndroid = new VrWindowAndroid(mActivity, mContentVirtualDisplay);
210         reparentAllTabs(mContentVrWindowAndroid);
211 
212         mCompositorView = mActivity.getCompositorViewHolder().getCompositorView();
213         mVrCompositorSurfaceManager = new VrCompositorSurfaceManager(mCompositorView);
214         mCompositorView.replaceSurfaceManagerForVr(
215                 mVrCompositorSurfaceManager, mContentVrWindowAndroid);
216 
217         if (mVrBrowsingEnabled) {
218             injectVrRootView();
219         }
220 
221         // This overrides the default intent created by GVR to return to Chrome when the DON flow
222         // is triggered by resuming the GvrLayout, which is the usual way Daydream apps enter VR.
223         // See VrShellDelegate#getEnterVrPendingIntent for why we need to do this.
224         setReentryIntent(VrShellDelegate.getEnterVrPendingIntent(activity));
225 
226         setPresentationView(mPresentationView);
227 
228         getUiLayout().setCloseButtonListener(mDelegate.getVrCloseButtonListener());
229         getUiLayout().setSettingsButtonListener(mDelegate.getVrSettingsButtonListener());
230 
231         if (mVrBrowsingEnabled) injectVrHostedUiView();
232 
233         // This has to happen after VrModalDialogManager is created.
234         mNonVrUiWidgetFactory = UiWidgetFactory.getInstance();
235         UiWidgetFactory.setInstance(new VrUiWidgetFactory(this, mActivity.getModalDialogManager()));
236 
237         mRedirectHandler = new RedirectHandler() {
238             @Override
239             public boolean shouldStayInApp(boolean hasExternalProtocol) {
240                 return !hasExternalProtocol;
241             }
242         };
243 
244         mTabObserver = new EmptyTabObserver() {
245             @Override
246             public void onContentChanged(Tab tab) {
247                 // Restore proper focus on the old content.
248                 if (mViewEventSink != null) mViewEventSink.onWindowFocusChanged(true);
249                 mViewEventSink = null;
250                 if (mNativeVrShell == 0) return;
251                 if (mLastContentWidth != 0) {
252                     setContentCssSize(mLastContentWidth, mLastContentHeight, mLastContentDpr);
253                 }
254                 if (tab != null && tab.getContentView() != null && tab.getWebContents() != null) {
255                     tab.getContentView().requestFocus();
256                     // We need the content layer to think it has Window Focus so it doesn't blur
257                     // the page, even though we're drawing VR layouts over top of it.
258                     mViewEventSink = ViewEventSink.from(tab.getWebContents());
259                     if (mViewEventSink != null) mViewEventSink.onWindowFocusChanged(true);
260                 }
261                 VrShellJni.get().swapContents(mNativeVrShell, VrShell.this, tab);
262                 updateHistoryButtonsVisibility();
263             }
264 
265             @Override
266             public void onWebContentsSwapped(Tab tab, boolean didStartLoad, boolean didFinishLoad) {
267                 onContentChanged(tab);
268                 // It is not needed to restore IME for old web contents as it is going away and
269                 // replaced by the new web contents.
270                 configWebContentsImeForVr(tab.getWebContents());
271             }
272 
273             @Override
274             public void onLoadProgressChanged(Tab tab, float progress) {
275                 if (mNativeVrShell == 0) return;
276                 VrShellJni.get().onLoadProgressChanged(mNativeVrShell, VrShell.this, progress);
277             }
278 
279             @Override
280             public void onCrash(Tab tab) {
281                 updateHistoryButtonsVisibility();
282             }
283 
284             @Override
285             public void onLoadStarted(Tab tab, boolean toDifferentDocument) {
286                 if (!toDifferentDocument) return;
287                 updateHistoryButtonsVisibility();
288             }
289 
290             @Override
291             public void onLoadStopped(Tab tab, boolean toDifferentDocument) {
292                 if (!toDifferentDocument) return;
293                 updateHistoryButtonsVisibility();
294             }
295 
296             @Override
297             public void onUrlUpdated(Tab tab) {
298                 updateHistoryButtonsVisibility();
299             }
300         };
301 
302         mTabModelSelectorObserver = new EmptyTabModelSelectorObserver() {
303             @Override
304             public void onChange() {
305                 swapToForegroundTab();
306             }
307 
308             @Override
309             public void onNewTabCreated(Tab tab, @TabCreationState int creationState) {
310                 if (mNativeVrShell == 0) return;
311                 VrShellJni.get().onTabUpdated(mNativeVrShell, VrShell.this, tab.isIncognito(),
312                         tab.getId(), tab.getTitle());
313             }
314         };
315 
316         mTouchListener = new View.OnTouchListener() {
317             @Override
318             @SuppressLint("ClickableViewAccessibility")
319             public boolean onTouch(View v, MotionEvent event) {
320                 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
321                     VrShellJni.get().onTriggerEvent(mNativeVrShell, VrShell.this, true);
322                     return true;
323                 } else if (event.getActionMasked() == MotionEvent.ACTION_UP
324                         || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
325                     VrShellJni.get().onTriggerEvent(mNativeVrShell, VrShell.this, false);
326                     return true;
327                 }
328                 return false;
329             }
330         };
331     }
332 
injectVrRootView()333     private void injectVrRootView() {
334         // Inject a view into the hierarchy above R.id.content so that the rest of Chrome can
335         // remain unaware/uncaring of its existence. This view is used to draw the view hierarchy
336         // into a texture when browsing in VR. See https://crbug.com/793430.
337         View content = mActivity.getWindow().findViewById(android.R.id.content);
338         ViewGroup parent = (ViewGroup) content.getParent();
339         mNonVrViews = new EmptySniffingVrViewContainer(mActivity, this);
340         parent.removeView(content);
341         parent.addView(mNonVrViews);
342         // Some views in Clank are just added next to the content view, like the 2D tab switcher.
343         // We need to create a parent to contain the content view and all of its siblings so that
344         // the VrViewContainer can inject input into the parent and not care about how to do its own
345         // input targeting.
346         FrameLayout childHolder = new FrameLayout(mActivity);
347         mNonVrViews.addView(childHolder);
348         childHolder.addView(content);
349     }
350 
injectVrHostedUiView()351     private void injectVrHostedUiView() {
352         mNonVrModalDialogManager = mActivity.getModalDialogManager();
353         mNonVrModalDialogManager.dismissAllDialogs(DialogDismissalCause.UNKNOWN);
354         mVrModalPresenter = new VrModalPresenter(mActivity, this);
355         mVrModalDialogManager =
356                 new ModalDialogManager(mVrModalPresenter, ModalDialogManager.ModalDialogType.APP);
357         setModalDialogManager(mVrModalDialogManager);
358 
359         ViewGroup decor = (ViewGroup) mActivity.getWindow().getDecorView();
360         mUiView = new FrameLayout(decor.getContext());
361         LayoutParams params = new FrameLayout.LayoutParams(
362                 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
363         decor.addView(mUiView, params);
364         mVrUiViewContainer = new VrViewContainer(mActivity);
365         mUiView.addView(mVrUiViewContainer);
366     }
367 
setModalDialogManager(ModalDialogManager modalDialogManager)368     private void setModalDialogManager(ModalDialogManager modalDialogManager) {
369         ((ObservableSupplierImpl) mActivity.getModalDialogManagerSupplier())
370                 .set(mVrModalDialogManager);
371     }
372 
removeVrRootView()373     private void removeVrRootView() {
374         ViewGroup contentViewParent = (ViewGroup) mNonVrViews.getParent();
375         assert mNonVrViews.getChildCount() == 1;
376         ViewGroup childHolder = (ViewGroup) mNonVrViews.getChildAt(0);
377         mNonVrViews.removeAllViews();
378         contentViewParent.removeView(mNonVrViews);
379         int children = childHolder.getChildCount();
380         assert children >= 1;
381         for (int i = 0; i < children; ++i) {
382             View child = childHolder.getChildAt(0);
383             childHolder.removeView(child);
384             contentViewParent.addView(child);
385         }
386         // Ensure the omnibox doesn't get initial focus (as it would when re-attaching the views
387         // to a window), and immediately bring up the keyboard.
388         if (mActivity.getCompositorViewHolder() != null) {
389             mActivity.getCompositorViewHolder().requestFocus();
390         }
391     }
392 
393     @TargetApi(Build.VERSION_CODES.N)
initializeNative(boolean forWebVr, boolean isStandaloneVrDevice)394     public void initializeNative(boolean forWebVr, boolean isStandaloneVrDevice) {
395         Tab tab = mActivity.getActivityTab();
396         if (mActivity.isInOverviewMode() || tab == null) {
397             openNewTab(false /*incognito*/);
398             tab = mActivity.getActivityTab();
399         }
400 
401         // Start with content rendering paused if the renderer-drawn controls are visible, as this
402         // would cause the in-content omnibox to be shown to users.
403         boolean pauseContent = mActivity.getBrowserControlsManager().getContentOffset() > 0;
404 
405         // Get physical and pixel size of the display, which is needed by native
406         // to dynamically calculate the content's resolution and window size.
407         DisplayMetrics dm = new DisplayMetrics();
408         DisplayAndroidManager.getDefaultDisplayForContext(mActivity).getRealMetrics(dm);
409         // We're supposed to be in landscape at this point, but it's possible for us to get here
410         // before the change has fully propagated. In this case, the width and height are swapped,
411         // which causes an incorrect display size to be used, and the page to appear zoomed in.
412         if (dm.widthPixels < dm.heightPixels) {
413             int tempWidth = dm.heightPixels;
414             dm.heightPixels = dm.widthPixels;
415             dm.widthPixels = tempWidth;
416             float tempXDpi = dm.ydpi;
417             dm.xdpi = dm.ydpi;
418             dm.ydpi = tempXDpi;
419             // In the case where we're still in portrait, keep the black overlay visible until the
420             // GvrLayout is in the correct orientation.
421         } else {
422             VrModuleProvider.getDelegate().removeBlackOverlayView(mActivity, false /* animate */);
423         }
424         float displayWidthMeters = (dm.widthPixels / dm.xdpi) * INCHES_TO_METERS;
425         float displayHeightMeters = (dm.heightPixels / dm.ydpi) * INCHES_TO_METERS;
426 
427         // Semi-arbitrary resolution cutoff that determines how much we scale our default buffer
428         // size in VR. This is so we can make the right performance/quality tradeoff for both the
429         // relatively low-res Pixel, and higher-res Pixel XL and other devices.
430         boolean lowDensity = dm.densityDpi <= DisplayMetrics.DENSITY_XXHIGH;
431 
432         boolean hasOrCanRequestRecordAudioPermission =
433                 hasRecordAudioPermission() || canRequestRecordAudioPermission();
434         boolean supportsRecognition = VoiceRecognitionUtil.isRecognitionIntentPresent(false);
435         mNativeVrShell = VrShellJni.get().init(VrShell.this, mDelegate, forWebVr,
436                 !mVrBrowsingEnabled, hasOrCanRequestRecordAudioPermission && supportsRecognition,
437                 getGvrApi().getNativeGvrContext(), mReprojectedRendering, displayWidthMeters,
438                 displayHeightMeters, dm.widthPixels, dm.heightPixels, pauseContent, lowDensity,
439                 isStandaloneVrDevice);
440 
441         swapToTab(tab);
442         createTabList();
443         mActivity.getTabModelSelector().addObserver(mTabModelSelectorObserver);
444         attachTabModelSelectorTabObserver();
445         updateHistoryButtonsVisibility();
446 
447         mPresentationView.setOnTouchListener(mTouchListener);
448 
449         if (mVrBrowsingEnabled) {
450             mAndroidUiGestureTarget = new AndroidUiGestureTarget(mNonVrViews.getInputTarget(),
451                     mContentVrWindowAndroid.getDisplay().getDipScale(), getNativePageScrollRatio(),
452                     getTouchSlop());
453             VrShellJni.get().setAndroidGestureTarget(
454                     mNativeVrShell, VrShell.this, mAndroidUiGestureTarget);
455         }
456     }
457 
createTabList()458     private void createTabList() {
459         assert mNativeVrShell != 0;
460         TabModel main = mTabModelSelector.getModel(false);
461         int count = main.getCount();
462         Tab[] mainTabs = new Tab[count];
463         for (int i = 0; i < count; ++i) {
464             mainTabs[i] = main.getTabAt(i);
465         }
466         TabModel incognito = mTabModelSelector.getModel(true);
467         count = incognito.getCount();
468         Tab[] incognitoTabs = new Tab[count];
469         for (int i = 0; i < count; ++i) {
470             incognitoTabs[i] = incognito.getTabAt(i);
471         }
472         VrShellJni.get().onTabListCreated(mNativeVrShell, VrShell.this, mainTabs, incognitoTabs);
473     }
474 
swapToForegroundTab()475     private void swapToForegroundTab() {
476         Tab tab = mActivity.getActivityTab();
477         if (tab == mTab) return;
478         swapToTab(tab);
479     }
480 
swapToTab(Tab tab)481     private void swapToTab(Tab tab) {
482         if (mTab != null) {
483             mTab.removeObserver(mTabObserver);
484             restoreTabFromVR();
485         }
486 
487         mTab = tab;
488         if (mTab != null) {
489             initializeTabForVR();
490             mTab.addObserver(mTabObserver);
491             TabBrowserControlsConstraintsHelper.update(mTab, BrowserControlsState.HIDDEN, false);
492         }
493         mTabObserver.onContentChanged(mTab);
494     }
495 
configWebContentsImeForVr(WebContents webContents)496     private void configWebContentsImeForVr(WebContents webContents) {
497         if (webContents == null) return;
498 
499         ImeAdapter imeAdapter = ImeAdapter.fromWebContents(webContents);
500         if (imeAdapter == null) return;
501 
502         mInputMethodManagerWrapper = new VrInputMethodManagerWrapper(mActivity, this);
503         imeAdapter.setInputMethodManagerWrapper(mInputMethodManagerWrapper);
504     }
505 
restoreWebContentsImeFromVr(WebContents webContents)506     private void restoreWebContentsImeFromVr(WebContents webContents) {
507         if (webContents == null) return;
508 
509         ImeAdapter imeAdapter = ImeAdapter.fromWebContents(webContents);
510         if (imeAdapter == null) return;
511 
512         // Use application context here to avoid leaking the activity context.
513         imeAdapter.setInputMethodManagerWrapper(ImeAdapter.createDefaultInputMethodManagerWrapper(
514                 mActivity.getApplicationContext(), mContentVrWindowAndroid, null));
515         mInputMethodManagerWrapper = null;
516     }
517 
initializeTabForVR()518     private void initializeTabForVR() {
519         if (mTab == null) return;
520         // Make sure we are not redirecting to another app, i.e. out of VR mode.
521         mNonVrRedirectHandler = RedirectHandlerTabHelper.swapHandlerFor(mTab, mRedirectHandler);
522         assert mTab.getWindowAndroid() == mContentVrWindowAndroid;
523         configWebContentsImeForVr(mTab.getWebContents());
524     }
525 
restoreTabFromVR()526     private void restoreTabFromVR() {
527         if (mTab == null) return;
528         RedirectHandlerTabHelper.swapHandlerFor(mTab, mNonVrRedirectHandler);
529         mNonVrRedirectHandler = null;
530         restoreWebContentsImeFromVr(mTab.getWebContents());
531     }
532 
reparentAllTabs(WindowAndroid window)533     private void reparentAllTabs(WindowAndroid window) {
534         // Ensure new tabs are created with the correct window.
535         boolean[] values = {true, false};
536         for (boolean incognito : values) {
537             TabCreator tabCreator = mActivity.getTabCreator(incognito);
538             if (tabCreator instanceof ChromeTabCreator) {
539                 ((ChromeTabCreator) tabCreator).setWindowAndroid(window);
540             }
541         }
542 
543         // Reparent all existing tabs.
544         for (TabModel model : mActivity.getTabModelSelector().getModels()) {
545             for (int i = 0; i < model.getCount(); ++i) {
546                 model.getTabAt(i).updateAttachment(window, null);
547             }
548         }
549     }
550 
551     // Returns true if Chrome has permission to use audio input.
552     @CalledByNative
hasRecordAudioPermission()553     public boolean hasRecordAudioPermission() {
554         return mDelegate.hasRecordAudioPermission();
555     }
556 
557     // Returns true if Chrome has not been permanently denied audio input permission.
558     @CalledByNative
canRequestRecordAudioPermission()559     public boolean canRequestRecordAudioPermission() {
560         return mDelegate.canRequestRecordAudioPermission();
561     }
562 
563     // Exits VR, telling the user to remove their headset, and returning to Chromium.
564     @CalledByNative
forceExitVr()565     public void forceExitVr() {
566         mDelegate.showDoff(false);
567     }
568 
569     // Called when the user clicks on the security icon in the URL bar.
570     @CalledByNative
showPageInfo()571     public void showPageInfo() {
572         Tab tab = mActivity.getActivityTab();
573         if (tab == null) return;
574         WebContents webContents = tab.getWebContents();
575         PageInfoController.show(mActivity, webContents, null,
576                 PageInfoController.OpenedFromSource.VR,
577                 new ChromePageInfoControllerDelegate(mActivity, webContents,
578                         mActivity::getModalDialogManager,
579                         /*offlinePageLoadUrlDelegate=*/
580                         new OfflinePageUtils.TabOfflinePageLoadUrlDelegate(tab)),
581                 new ChromePermissionParamsListBuilderDelegate());
582     }
583 
584     // Called because showing audio permission dialog isn't supported in VR. This happens when
585     // the user wants to do a voice search.
586     @CalledByNative
onUnhandledPermissionPrompt()587     public void onUnhandledPermissionPrompt() {
588         VrShellDelegate.requestToExitVr(new OnExitVrRequestListener() {
589             @Override
590             public void onSucceeded() {
591                 PermissionCallback callback = new PermissionCallback() {
592                     @Override
593                     public void onRequestPermissionsResult(
594                             String[] permissions, int[] grantResults) {
595                         PostTask.postTask(UiThreadTaskTraits.DEFAULT, new Runnable() {
596                             @Override
597                             public void run() {
598                                 VrShellDelegate.enterVrIfNecessary();
599 
600                                 // In SVR, the native VR UI is destroyed when
601                                 // exiting VR (mNativeVrShell == 0), so
602                                 // permission changes will be detected when the
603                                 // VR UI is reconstructed. For AIO devices this
604                                 // doesn't happen, so we need to notify native
605                                 // UI of the permission change immediately.
606                                 if (mNativeVrShell != 0) {
607                                     VrShellJni.get().requestRecordAudioPermissionResult(
608                                             mNativeVrShell, VrShell.this,
609                                             grantResults[0] == PackageManager.PERMISSION_GRANTED);
610                                 }
611                             }
612                         });
613                     }
614                 };
615                 String[] permissionArray = new String[1];
616                 permissionArray[0] = android.Manifest.permission.RECORD_AUDIO;
617                 mActivity.getWindowAndroid().requestPermissions(permissionArray, callback);
618             }
619 
620             @Override
621             public void onDenied() {}
622         }, UiUnsupportedMode.VOICE_SEARCH_NEEDS_RECORD_AUDIO_OS_PERMISSION);
623     }
624 
625     // Called when the user has an older GVR Keyboard installed on their device and we need them to
626     // have a newer one.
627     @CalledByNative
onNeedsKeyboardUpdate()628     public void onNeedsKeyboardUpdate() {
629         VrShellDelegate.requestToExitVr(new OnExitVrRequestListener() {
630             @Override
631             public void onSucceeded() {
632                 mDelegate.promptForKeyboardUpdate();
633             }
634 
635             @Override
636             public void onDenied() {}
637         }, UiUnsupportedMode.NEEDS_KEYBOARD_UPDATE);
638     }
639 
640     // Close the current hosted Dialog in VR
641     @CalledByNative
closeCurrentDialog()642     public void closeCurrentDialog() {
643         mVrModalPresenter.closeCurrentDialog();
644         if (mVrDialogDismissHandler != null) {
645             mVrDialogDismissHandler.run();
646             mVrDialogDismissHandler = null;
647         }
648     }
649 
650     /**
651      * @param vrDialogDismissHandler the mVrDialogDismissHandler to set
652      */
653     @Override
setVrDialogDismissHandler(Runnable vrDialogDismissHandler)654     public void setVrDialogDismissHandler(Runnable vrDialogDismissHandler) {
655         mVrDialogDismissHandler = vrDialogDismissHandler;
656     }
657 
658     @Override
onWindowFocusChanged(boolean focused)659     public void onWindowFocusChanged(boolean focused) {
660         // This handles the case where we open 2D popups in 2D-in-VR. We lose window focus, but stay
661         // resumed, so we have to listen for focus gain to know when the popup was closed. However,
662         // we pause VrShellImpl so that we don't react to input from the controller nor do any
663         // rendering. This also handles the case where we're launched via intent and turn VR mode on
664         // with a popup open. We'll lose window focus when the popup 'gets shown' and know to turn
665         // VR mode off.
666         // TODO(asimjour): Focus is a bad signal. We should listen for windows being created and
667         // destroyed if possible.
668         if (VrModuleProvider.getDelegate().bootsToVr()) {
669             if (focused) {
670                 resume();
671             } else {
672                 pause();
673             }
674             VrShellDelegate.setVrModeEnabled(mActivity, focused);
675             setVisibility(focused ? View.VISIBLE : View.INVISIBLE);
676         }
677     }
678 
679     @CalledByNative
setContentCssSize(float width, float height, float dpr)680     public void setContentCssSize(float width, float height, float dpr) {
681         ThreadUtils.assertOnUiThread();
682         boolean surfaceUninitialized = mLastContentWidth == 0;
683         mLastContentWidth = width;
684         mLastContentHeight = height;
685         mLastContentDpr = dpr;
686 
687         // Java views don't listen to our DPR changes, so to get them to render at the correct
688         // size we need to make them larger.
689         DisplayAndroid primaryDisplay = DisplayAndroid.getNonMultiDisplay(mActivity);
690         float dip = primaryDisplay.getDipScale();
691 
692         int contentWidth = (int) Math.ceil(width * dpr);
693         int contentHeight = (int) Math.ceil(height * dpr);
694 
695         int overlayWidth = (int) Math.ceil(width * dip);
696         int overlayHeight = (int) Math.ceil(height * dip);
697 
698         VrShellJni.get().bufferBoundsChanged(mNativeVrShell, VrShell.this, contentWidth,
699                 contentHeight, overlayWidth, overlayHeight);
700         if (mContentSurface != null) {
701             if (surfaceUninitialized) {
702                 mVrCompositorSurfaceManager.setSurface(
703                         mContentSurface, PixelFormat.OPAQUE, contentWidth, contentHeight);
704             } else {
705                 mVrCompositorSurfaceManager.surfaceResized(contentWidth, contentHeight);
706             }
707         }
708         Point size = new Point(contentWidth, contentHeight);
709         mContentVirtualDisplay.update(
710                 size, dpr, dip / dpr, null, null, null, null, null, null, null, null);
711         if (mTab != null && mTab.getWebContents() != null) {
712             mTab.getWebContents().setSize(contentWidth, contentHeight);
713         }
714         if (mVrBrowsingEnabled) mNonVrViews.resize(overlayWidth, overlayHeight);
715     }
716 
717     @CalledByNative
contentSurfaceCreated(Surface surface)718     public void contentSurfaceCreated(Surface surface) {
719         mContentSurface = surface;
720         if (mLastContentWidth == 0) return;
721         int width = (int) Math.ceil(mLastContentWidth * mLastContentDpr);
722         int height = (int) Math.ceil(mLastContentHeight * mLastContentDpr);
723         mVrCompositorSurfaceManager.setSurface(mContentSurface, PixelFormat.OPAQUE, width, height);
724     }
725 
726     @CalledByNative
contentOverlaySurfaceCreated(Surface surface)727     public void contentOverlaySurfaceCreated(Surface surface) {
728         if (mVrBrowsingEnabled) mNonVrViews.setSurface(surface);
729     }
730 
731     @CalledByNative
dialogSurfaceCreated(Surface surface)732     public void dialogSurfaceCreated(Surface surface) {
733         if (mVrBrowsingEnabled && mVrUiViewContainer != null) {
734             mVrUiViewContainer.setSurface(surface);
735         }
736     }
737 
738     @Override
dispatchTouchEvent(MotionEvent event)739     public boolean dispatchTouchEvent(MotionEvent event) {
740         boolean parentConsumed = super.dispatchTouchEvent(event);
741         if (mOnDispatchTouchEventForTesting != null) {
742             mOnDispatchTouchEventForTesting.onDispatchTouchEvent(parentConsumed);
743         }
744         return parentConsumed;
745     }
746 
747     @Override
dispatchKeyEvent(KeyEvent event)748     public boolean dispatchKeyEvent(KeyEvent event) {
749         if (mTab != null && mTab.getWebContents() != null
750                 && mTab.getWebContents().getEventForwarder().dispatchKeyEvent(event)) {
751             return true;
752         }
753         return super.dispatchKeyEvent(event);
754     }
755 
756     @Override
onGenericMotionEvent(MotionEvent event)757     public boolean onGenericMotionEvent(MotionEvent event) {
758         if (mTab != null && mTab.getWebContents() != null
759                 && mTab.getWebContents().getEventForwarder().onGenericMotionEvent(event)) {
760             return true;
761         }
762         return super.onGenericMotionEvent(event);
763     }
764 
765     @Override
onResume()766     public void onResume() {
767         if (mPaused != null && !mPaused) return;
768         mPaused = false;
769         super.onResume();
770         if (mNativeVrShell != 0) {
771             // Refreshing the viewer profile may accesses disk under some circumstances outside of
772             // our control.
773             try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) {
774                 VrShellJni.get().onResume(mNativeVrShell, VrShell.this);
775             }
776         }
777     }
778 
779     @Override
onPause()780     public void onPause() {
781         if (mPaused != null && mPaused) return;
782         mPaused = true;
783         super.onPause();
784         if (mNativeVrShell != 0) VrShellJni.get().onPause(mNativeVrShell, VrShell.this);
785     }
786 
destroyWindowAndroid()787     public void destroyWindowAndroid() {
788         reparentAllTabs(mActivity.getWindowAndroid());
789         mCompositorView.onExitVr(mActivity.getWindowAndroid());
790         mContentVrWindowAndroid.destroy();
791     }
792 
793     @Override
shutdown()794     public void shutdown() {
795         if (mVrBrowsingEnabled) {
796             if (mVrModalDialogManager != null) {
797                 mVrModalDialogManager.dismissAllDialogs(DialogDismissalCause.UNKNOWN);
798                 setModalDialogManager(mNonVrModalDialogManager);
799                 mVrModalDialogManager = null;
800             }
801             mNonVrViews.destroy();
802             if (mVrUiViewContainer != null) mVrUiViewContainer.destroy();
803             removeVrRootView();
804         }
805 
806         if (!mActivity.isActivityFinishingOrDestroyed()) {
807             mActivity.getFullscreenManager().exitPersistentFullscreenMode();
808         }
809         reparentAllTabs(mActivity.getWindowAndroid());
810         if (mNativeVrShell != 0) {
811             VrShellJni.get().destroy(mNativeVrShell, VrShell.this);
812             mNativeVrShell = 0;
813         }
814         mTabModelSelector.removeObserver(mTabModelSelectorObserver);
815         mTabModelSelectorTabObserver.destroy();
816         if (mTab != null) {
817             mTab.removeObserver(mTabObserver);
818             restoreTabFromVR();
819             restoreWebContentsImeFromVr(mTab.getWebContents());
820             if (mTab.getWebContents() != null && mTab.getContentView() != null) {
821                 View parent = mTab.getContentView();
822                 mTab.getWebContents().setSize(parent.getWidth(), parent.getHeight());
823             }
824             TabBrowserControlsConstraintsHelper.update(mTab, BrowserControlsState.SHOWN, false);
825         }
826 
827         mContentVirtualDisplay.destroy();
828 
829         mCompositorView.onExitVr(mActivity.getWindowAndroid());
830         mContentVrWindowAndroid.destroy();
831 
832         if (mActivity.getToolbarManager() != null) {
833             mActivity.getToolbarManager().setProgressBarEnabled(true);
834         }
835 
836         if (mNonVrUiWidgetFactory != null) UiWidgetFactory.setInstance(mNonVrUiWidgetFactory);
837 
838         FrameLayout decor = (FrameLayout) mActivity.getWindow().getDecorView();
839         decor.removeView(mUiView);
840         super.shutdown();
841     }
842 
pause()843     public void pause() {
844         onPause();
845     }
846 
resume()847     public void resume() {
848         onResume();
849     }
850 
teardown()851     public void teardown() {
852         shutdown();
853     }
854 
hasUiFinishedLoading()855     public boolean hasUiFinishedLoading() {
856         return VrShellJni.get().hasUiFinishedLoading(mNativeVrShell, VrShell.this);
857     }
858 
859     /**
860      * Set View for the Dialog that should show up on top of the main content.
861      */
862     @Override
setDialogView(View view)863     public void setDialogView(View view) {
864         if (view == null) return;
865         assert mVrUiViewContainer.getChildCount() == 0;
866         mVrUiViewContainer.addView(view);
867     }
868 
869     /**
870      * Close the popup Dialog in VR.
871      */
872     @Override
closeVrDialog()873     public void closeVrDialog() {
874         VrShellJni.get().closeAlertDialog(mNativeVrShell, VrShell.this);
875         mVrUiViewContainer.removeAllViews();
876         mVrDialogDismissHandler = null;
877     }
878 
879     /**
880      * Set size of the Dialog in VR.
881      */
882     @Override
setDialogSize(int width, int height)883     public void setDialogSize(int width, int height) {
884         VrShellJni.get().setDialogBufferSize(mNativeVrShell, VrShell.this, width, height);
885         VrShellJni.get().setAlertDialogSize(mNativeVrShell, VrShell.this, width, height);
886     }
887 
888     /**
889      * Set size of the Dialog location in VR.
890      */
891     @Override
setDialogLocation(int x, int y)892     public void setDialogLocation(int x, int y) {
893         if (getWebVrModeEnabled()) return;
894         float dipScale = DisplayAndroid.getNonMultiDisplay(mActivity).getDipScale();
895         float w = mLastContentWidth * dipScale;
896         float h = mLastContentHeight * dipScale;
897         float scale = mContentVrWindowAndroid.getDisplay().getAndroidUIScaling();
898         VrShellJni.get().setDialogLocation(
899                 mNativeVrShell, VrShell.this, x * scale / w, y * scale / h);
900     }
901 
902     @Override
setDialogFloating(boolean floating)903     public void setDialogFloating(boolean floating) {
904         VrShellJni.get().setDialogFloating(mNativeVrShell, VrShell.this, floating);
905     }
906 
907     /**
908      * Initialize the Dialog in VR.
909      */
910     @Override
initVrDialog(int width, int height)911     public void initVrDialog(int width, int height) {
912         VrShellJni.get().setAlertDialog(mNativeVrShell, VrShell.this, width, height);
913         mAndroidDialogGestureTarget =
914                 new AndroidUiGestureTarget(mVrUiViewContainer.getInputTarget(), 1.0f,
915                         getNativePageScrollRatio(), getTouchSlop());
916         VrShellJni.get().setDialogGestureTarget(
917                 mNativeVrShell, VrShell.this, mAndroidDialogGestureTarget);
918     }
919 
920     /**
921      * Show a text only Toast.
922      */
923     @Override
showToast(CharSequence text)924     public void showToast(CharSequence text) {
925         VrShellJni.get().showToast(mNativeVrShell, VrShell.this, text.toString());
926     }
927 
928     /**
929      * Cancel a Toast.
930      */
931     @Override
cancelToast()932     public void cancelToast() {
933         VrShellJni.get().cancelToast(mNativeVrShell, VrShell.this);
934     }
935 
setWebVrModeEnabled(boolean enabled)936     public void setWebVrModeEnabled(boolean enabled) {
937         if (mNativeVrShell != 0) {
938             VrShellJni.get().setWebVrMode(mNativeVrShell, VrShell.this, enabled);
939         }
940         if (!enabled) {
941             mContentVrWindowAndroid.setVSyncPaused(false);
942             mPendingVSyncPause = false;
943             return;
944         }
945         // Wait for the compositor to produce a frame to allow the omnibox to start hiding
946         // before we pause VSync. Control heights may not be correct as the omnibox might
947         // animate, but this is handled when exiting VR.
948         mPendingVSyncPause = true;
949         mActivity.getCompositorViewHolder().getCompositorView().surfaceRedrawNeededAsync(() -> {
950             if (mPendingVSyncPause) {
951                 mContentVrWindowAndroid.setVSyncPaused(true);
952                 mPendingVSyncPause = false;
953                 if (mOnVSyncPausedForTesting != null) {
954                     mOnVSyncPausedForTesting.run();
955                 }
956             }
957         });
958     }
959 
getWebVrModeEnabled()960     public boolean getWebVrModeEnabled() {
961         if (mNativeVrShell == 0) return false;
962         return VrShellJni.get().getWebVrMode(mNativeVrShell, VrShell.this);
963     }
964 
isDisplayingUrlForTesting()965     public boolean isDisplayingUrlForTesting() {
966         assert mNativeVrShell != 0;
967         return PostTask.runSynchronously(UiThreadTaskTraits.DEFAULT, () -> {
968             return VrShellJni.get().isDisplayingUrlForTesting(mNativeVrShell, VrShell.this);
969         });
970     }
971 
972     @VisibleForTesting
getVrInputConnectionForTesting()973     public VrInputConnection getVrInputConnectionForTesting() {
974         assert mNativeVrShell != 0;
975         return VrShellJni.get().getVrInputConnectionForTesting(mNativeVrShell, VrShell.this);
976     }
977 
getContainer()978     public FrameLayout getContainer() {
979         return this;
980     }
981 
rawTopContentOffsetChanged(float topContentOffset)982     public void rawTopContentOffsetChanged(float topContentOffset) {
983         if (topContentOffset != 0) return;
984         // Wait until a new frame is definitely available.
985         mActivity.getCompositorViewHolder().getCompositorView().surfaceRedrawNeededAsync(() -> {
986             if (mNativeVrShell != 0) {
987                 VrShellJni.get().resumeContentRendering(mNativeVrShell, VrShell.this);
988             }
989         });
990     }
991 
992     @Override
surfaceCreated(SurfaceHolder holder)993     public void surfaceCreated(SurfaceHolder holder) {
994         if (mNativeVrShell == 0) return;
995         VrShellJni.get().setSurface(mNativeVrShell, VrShell.this, holder.getSurface());
996     }
997 
998     @Override
surfaceChanged(SurfaceHolder holder, int format, int width, int height)999     public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
1000         // No need to do anything here, we don't care about surface width/height.
1001     }
1002 
1003     @Override
surfaceDestroyed(SurfaceHolder holder)1004     public void surfaceDestroyed(SurfaceHolder holder) {
1005         mVrCompositorSurfaceManager.surfaceDestroyed();
1006         VrShellDelegate.forceExitVrImmediately();
1007     }
1008 
1009     /** Creates and attaches a TabModelSelectorTabObserver to the tab model selector. */
attachTabModelSelectorTabObserver()1010     private void attachTabModelSelectorTabObserver() {
1011         assert mTabModelSelectorTabObserver == null;
1012         mTabModelSelectorTabObserver = new TabModelSelectorTabObserver(mTabModelSelector) {
1013             @Override
1014             public void onTitleUpdated(Tab tab) {
1015                 if (mNativeVrShell == 0) return;
1016                 VrShellJni.get().onTabUpdated(mNativeVrShell, VrShell.this, tab.isIncognito(),
1017                         tab.getId(), tab.getTitle());
1018             }
1019 
1020             @Override
1021             public void onClosingStateChanged(Tab tab, boolean closing) {
1022                 if (mNativeVrShell == 0) return;
1023                 if (closing) {
1024                     VrShellJni.get().onTabRemoved(
1025                             mNativeVrShell, VrShell.this, tab.isIncognito(), tab.getId());
1026                 } else {
1027                     VrShellJni.get().onTabUpdated(mNativeVrShell, VrShell.this, tab.isIncognito(),
1028                             tab.getId(), tab.getTitle());
1029                 }
1030             }
1031 
1032             @Override
1033             public void onDestroyed(Tab tab) {
1034                 if (mNativeVrShell == 0) return;
1035                 VrShellJni.get().onTabRemoved(
1036                         mNativeVrShell, VrShell.this, tab.isIncognito(), tab.getId());
1037             }
1038         };
1039     }
1040 
1041     @CalledByNative
hasDaydreamSupport()1042     public boolean hasDaydreamSupport() {
1043         return VrCoreInstallUtils.hasDaydreamSupport();
1044     }
1045 
requestToExitVr(@iUnsupportedMode int reason, boolean showExitPromptBeforeDoff)1046     public void requestToExitVr(@UiUnsupportedMode int reason, boolean showExitPromptBeforeDoff) {
1047         if (mNativeVrShell == 0) return;
1048         if (showExitPromptBeforeDoff) {
1049             VrShellJni.get().requestToExitVr(mNativeVrShell, VrShell.this, reason);
1050         } else {
1051             mDelegate.onExitVrRequestResult(true);
1052         }
1053     }
1054 
1055     @CalledByNative
onExitVrRequestResult(boolean shouldExit)1056     private void onExitVrRequestResult(boolean shouldExit) {
1057         mDelegate.onExitVrRequestResult(shouldExit);
1058     }
1059 
1060     @CalledByNative
loadUrl(String url)1061     private void loadUrl(String url) {
1062         if (mTab == null) {
1063             mActivity.getCurrentTabCreator().createNewTab(
1064                     new LoadUrlParams(url), TabLaunchType.FROM_CHROME_UI, null);
1065         } else {
1066             mTab.loadUrl(new LoadUrlParams(url));
1067         }
1068     }
1069 
1070     @VisibleForTesting
1071     @CalledByNative
navigateForward()1072     public void navigateForward() {
1073         if (!mCanGoForward) return;
1074         mActivity.getToolbarManager().forward();
1075         updateHistoryButtonsVisibility();
1076     }
1077 
1078     @VisibleForTesting
1079     @CalledByNative
navigateBack()1080     public void navigateBack() {
1081         if (!mCanGoBack) return;
1082         if (mActivity instanceof ChromeTabbedActivity) {
1083             // TODO(mthiesse): We should do this for custom tabs as well, as back for custom tabs
1084             // is also expected to close tabs.
1085             ((ChromeTabbedActivity) mActivity).handleBackPressed();
1086         } else {
1087             mActivity.getToolbarManager().back();
1088         }
1089         updateHistoryButtonsVisibility();
1090     }
1091 
1092     @CalledByNative
reloadTab()1093     public void reloadTab() {
1094         mTab.reload();
1095     }
1096 
1097     @CalledByNative
openNewTab(boolean incognito)1098     public void openNewTab(boolean incognito) {
1099         mActivity.getTabCreator(incognito).launchNTP();
1100     }
1101 
1102     @CalledByNative
openBookmarks()1103     public void openBookmarks() {
1104         mActivity.onMenuOrKeyboardAction(R.id.all_bookmarks_menu_id, true);
1105     }
1106 
1107     @CalledByNative
openRecentTabs()1108     public void openRecentTabs() {
1109         mActivity.onMenuOrKeyboardAction(R.id.recent_tabs_menu_id, true);
1110     }
1111 
1112     @CalledByNative
openHistory()1113     public void openHistory() {
1114         mActivity.onMenuOrKeyboardAction(R.id.open_history_menu_id, true);
1115     }
1116 
1117     @CalledByNative
openDownloads()1118     public void openDownloads() {
1119         mActivity.onMenuOrKeyboardAction(R.id.downloads_menu_id, true);
1120     }
1121 
1122     @CalledByNative
openShare()1123     public void openShare() {
1124         mActivity.onMenuOrKeyboardAction(R.id.share_menu_id, true);
1125     }
1126 
1127     @CalledByNative
openSettings()1128     public void openSettings() {
1129         mActivity.onMenuOrKeyboardAction(R.id.preferences_id, true);
1130     }
1131 
1132     @CalledByNative
closeAllIncognitoTabs()1133     public void closeAllIncognitoTabs() {
1134         mTabModelSelector.getModel(true).closeAllTabs();
1135         if (mTabModelSelector.getTotalTabCount() == 0) openNewTab(false);
1136     }
1137 
1138     @CalledByNative
openFeedback()1139     public void openFeedback() {
1140         mActivity.onMenuOrKeyboardAction(R.id.help_id, true);
1141     }
1142 
updateHistoryButtonsVisibility()1143     private void updateHistoryButtonsVisibility() {
1144         if (mNativeVrShell == 0) return;
1145         if (mTab == null) {
1146             mCanGoBack = false;
1147             mCanGoForward = false;
1148             VrShellJni.get().setHistoryButtonsEnabled(
1149                     mNativeVrShell, VrShell.this, mCanGoBack, mCanGoForward);
1150             return;
1151         }
1152         boolean willCloseTab = false;
1153         if (mActivity instanceof ChromeTabbedActivity) {
1154             // If hitting back would minimize Chrome, disable the back button.
1155             // See ChromeTabbedActivity#handleBackPressed().
1156             willCloseTab = mActivity.backShouldCloseTab(mTab)
1157                     && !TabAssociatedApp.isOpenedFromExternalApp(mTab);
1158         }
1159         boolean canGoBack = mTab.canGoBack() || willCloseTab;
1160         boolean canGoForward = mTab.canGoForward();
1161         if ((mCanGoBack != null && canGoBack == mCanGoBack)
1162                 && (mCanGoForward != null && canGoForward == mCanGoForward)) {
1163             return;
1164         }
1165         mCanGoBack = canGoBack;
1166         mCanGoForward = canGoForward;
1167         VrShellJni.get().setHistoryButtonsEnabled(
1168                 mNativeVrShell, VrShell.this, mCanGoBack, mCanGoForward);
1169     }
1170 
getNativePageScrollRatio()1171     private float getNativePageScrollRatio() {
1172         return mActivity.getWindowAndroid().getDisplay().getDipScale()
1173                 / mContentVrWindowAndroid.getDisplay().getDipScale();
1174     }
1175 
getTouchSlop()1176     private int getTouchSlop() {
1177         ViewConfiguration vc = ViewConfiguration.get(mActivity);
1178         return vc.getScaledTouchSlop();
1179     }
1180 
1181     @Override
onVrViewEmpty()1182     public void onVrViewEmpty() {
1183         if (mNativeVrShell != 0) {
1184             VrShellJni.get().onOverlayTextureEmptyChanged(mNativeVrShell, VrShell.this, true);
1185         }
1186     }
1187 
1188     @Override
onVrViewNonEmpty()1189     public void onVrViewNonEmpty() {
1190         if (mNativeVrShell != 0) {
1191             VrShellJni.get().onOverlayTextureEmptyChanged(mNativeVrShell, VrShell.this, false);
1192         }
1193     }
1194 
1195     @Override
onSizeChanged(int width, int height, int oldWidth, int oldHeight)1196     protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
1197         super.onSizeChanged(width, height, oldWidth, oldHeight);
1198         if (width > height) {
1199             VrModuleProvider.getDelegate().removeBlackOverlayView(mActivity, true /* animate */);
1200         }
1201     }
1202 
1203     /**
1204      * Sets the callback that will be run when VrShellImpl's dispatchTouchEvent
1205      * is run and the parent consumed the event.
1206      * @param callback The Callback to be run.
1207      */
1208     @VisibleForTesting
setOnDispatchTouchEventForTesting(OnDispatchTouchEventCallback callback)1209     public void setOnDispatchTouchEventForTesting(OnDispatchTouchEventCallback callback) {
1210         mOnDispatchTouchEventForTesting = callback;
1211     }
1212 
1213     /**
1214      * Sets that callback that will be run when VrShellImpl has issued the request to pause the
1215      * Android Window's VSyncs.
1216      * @param callback The Runnable to be run.
1217      */
1218     @VisibleForTesting
setOnVSyncPausedForTesting(Runnable callback)1219     public void setOnVSyncPausedForTesting(Runnable callback) {
1220         mOnVSyncPausedForTesting = callback;
1221     }
1222 
1223     @VisibleForTesting
isBackButtonEnabled()1224     public Boolean isBackButtonEnabled() {
1225         return mCanGoBack;
1226     }
1227 
1228     @VisibleForTesting
isForwardButtonEnabled()1229     public Boolean isForwardButtonEnabled() {
1230         return mCanGoForward;
1231     }
1232 
1233     @VisibleForTesting
getContentWidthForTesting()1234     public float getContentWidthForTesting() {
1235         return mLastContentWidth;
1236     }
1237 
1238     @VisibleForTesting
getContentHeightForTesting()1239     public float getContentHeightForTesting() {
1240         return mLastContentHeight;
1241     }
1242 
1243     @VisibleForTesting
getPresentationViewForTesting()1244     public View getPresentationViewForTesting() {
1245         return mPresentationView;
1246     }
1247 
1248     @VisibleForTesting
isDisplayingDialogView()1249     public boolean isDisplayingDialogView() {
1250         return mVrUiViewContainer.getChildCount() > 0;
1251     }
1252 
1253     @VisibleForTesting
getVrViewContainerForTesting()1254     public VrViewContainer getVrViewContainerForTesting() {
1255         return mVrUiViewContainer;
1256     }
1257 
1258     @Override
showSoftInput(boolean show)1259     public void showSoftInput(boolean show) {
1260         assert mNativeVrShell != 0;
1261         VrShellJni.get().showSoftInput(mNativeVrShell, VrShell.this, show);
1262     }
1263 
1264     @Override
updateIndices( int selectionStart, int selectionEnd, int compositionStart, int compositionEnd)1265     public void updateIndices(
1266             int selectionStart, int selectionEnd, int compositionStart, int compositionEnd) {
1267         assert mNativeVrShell != 0;
1268         VrShellJni.get().updateWebInputIndices(mNativeVrShell, VrShell.this, selectionStart,
1269                 selectionEnd, compositionStart, compositionEnd);
1270     }
1271 
1272     @VisibleForTesting
getInputMethodManagerWrapperForTesting()1273     public VrInputMethodManagerWrapper getInputMethodManagerWrapperForTesting() {
1274         return mInputMethodManagerWrapper;
1275     }
1276 
acceptDoffPromptForTesting()1277     public void acceptDoffPromptForTesting() {
1278         VrShellJni.get().acceptDoffPromptForTesting(mNativeVrShell, VrShell.this);
1279     }
1280 
performControllerActionForTesting( int elementName, int actionType, PointF position)1281     public void performControllerActionForTesting(
1282             int elementName, int actionType, PointF position) {
1283         VrShellJni.get().performControllerActionForTesting(
1284                 mNativeVrShell, VrShell.this, elementName, actionType, position.x, position.y);
1285     }
1286 
performKeyboardInputForTesting(int inputType, String inputString)1287     public void performKeyboardInputForTesting(int inputType, String inputString) {
1288         PostTask.runSynchronously(UiThreadTaskTraits.DEFAULT, () -> {
1289             VrShellJni.get().performKeyboardInputForTesting(
1290                     mNativeVrShell, VrShell.this, inputType, inputString);
1291         });
1292     }
1293 
registerUiOperationCallbackForTesting(UiOperationData operationData)1294     public void registerUiOperationCallbackForTesting(UiOperationData operationData) {
1295         int actionType = operationData.actionType;
1296         assert actionType < UiTestOperationType.NUM_UI_TEST_OPERATION_TYPES;
1297         // Fill the ArrayLists if this is the first time the method has been called.
1298         if (mUiOperationResults == null) {
1299             mUiOperationResults =
1300                     new ArrayList<Integer>(UiTestOperationType.NUM_UI_TEST_OPERATION_TYPES);
1301             mUiOperationResultCallbacks =
1302                     new ArrayList<Runnable>(UiTestOperationType.NUM_UI_TEST_OPERATION_TYPES);
1303             for (int i = 0; i < UiTestOperationType.NUM_UI_TEST_OPERATION_TYPES; i++) {
1304                 mUiOperationResults.add(null);
1305                 mUiOperationResultCallbacks.add(null);
1306             }
1307         }
1308         mUiOperationResults.set(actionType, UiTestOperationResult.UNREPORTED);
1309         mUiOperationResultCallbacks.set(actionType, operationData.resultCallback);
1310 
1311         // In the case of the UI activity quiescence callback type, we need to let the native UI
1312         // know how long to wait before timing out.
1313         if (actionType == UiTestOperationType.UI_ACTIVITY_RESULT) {
1314             VrShellJni.get().setUiExpectingActivityForTesting(
1315                     mNativeVrShell, VrShell.this, operationData.timeoutMs);
1316         } else if (actionType == UiTestOperationType.ELEMENT_VISIBILITY_STATUS) {
1317             VrShellJni.get().watchElementForVisibilityStatusForTesting(mNativeVrShell, VrShell.this,
1318                     operationData.elementName, operationData.timeoutMs, operationData.visibility);
1319         }
1320     }
1321 
1322     public void saveNextFrameBufferToDiskForTesting(String filepathBase) {
1323         VrShellJni.get().saveNextFrameBufferToDiskForTesting(
1324                 mNativeVrShell, VrShell.this, filepathBase);
1325     }
1326 
1327     public int getLastUiOperationResultForTesting(int actionType) {
1328         return mUiOperationResults.get(actionType).intValue();
1329     }
1330 
1331     @CalledByNative
1332     public void reportUiOperationResultForTesting(int actionType, int result) {
1333         mUiOperationResults.set(actionType, result);
1334         mUiOperationResultCallbacks.get(actionType).run();
1335         mUiOperationResultCallbacks.set(actionType, null);
1336     }
1337 
1338     @NativeMethods
1339     interface Natives {
1340         long init(VrShell caller, VrShellDelegate delegate, boolean forWebVR,
1341                 boolean browsingDisabled, boolean hasOrCanRequestRecordAudioPermission, long gvrApi,
1342                 boolean reprojectedRendering, float displayWidthMeters, float displayHeightMeters,
1343                 int displayWidthPixels, int displayHeightPixels, boolean pauseContent,
1344                 boolean lowDensity, boolean isStandaloneVrDevice);
1345         boolean hasUiFinishedLoading(long nativeVrShell, VrShell caller);
1346         void setSurface(long nativeVrShell, VrShell caller, Surface surface);
1347         void swapContents(long nativeVrShell, VrShell caller, Tab tab);
1348         void setAndroidGestureTarget(
1349                 long nativeVrShell, VrShell caller, AndroidUiGestureTarget androidUiGestureTarget);
1350         void setDialogGestureTarget(
1351                 long nativeVrShell, VrShell caller, AndroidUiGestureTarget dialogGestureTarget);
1352         void destroy(long nativeVrShell, VrShell caller);
1353         void onTriggerEvent(long nativeVrShell, VrShell caller, boolean touched);
1354         void onPause(long nativeVrShell, VrShell caller);
1355         void onResume(long nativeVrShell, VrShell caller);
1356         void onLoadProgressChanged(long nativeVrShell, VrShell caller, double progress);
1357         void bufferBoundsChanged(long nativeVrShell, VrShell caller, int contentWidth,
1358                 int contentHeight, int overlayWidth, int overlayHeight);
1359         void setWebVrMode(long nativeVrShell, VrShell caller, boolean enabled);
1360         boolean getWebVrMode(long nativeVrShell, VrShell caller);
1361         boolean isDisplayingUrlForTesting(long nativeVrShell, VrShell caller);
1362         void onTabListCreated(
1363                 long nativeVrShell, VrShell caller, Tab[] mainTabs, Tab[] incognitoTabs);
1364         void onTabUpdated(
1365                 long nativeVrShell, VrShell caller, boolean incognito, int id, String title);
1366         void onTabRemoved(long nativeVrShell, VrShell caller, boolean incognito, int id);
1367         void closeAlertDialog(long nativeVrShell, VrShell caller);
1368         void setAlertDialog(long nativeVrShell, VrShell caller, float width, float height);
1369         void setDialogBufferSize(long nativeVrShell, VrShell caller, int width, int height);
1370         void setAlertDialogSize(long nativeVrShell, VrShell caller, float width, float height);
1371         void setDialogLocation(long nativeVrShell, VrShell caller, float x, float y);
1372         void setDialogFloating(long nativeVrShell, VrShell caller, boolean floating);
1373         void showToast(long nativeVrShell, VrShell caller, String text);
1374         void cancelToast(long nativeVrShell, VrShell caller);
1375         void setHistoryButtonsEnabled(
1376                 long nativeVrShell, VrShell caller, boolean canGoBack, boolean canGoForward);
1377         void requestToExitVr(long nativeVrShell, VrShell caller, @UiUnsupportedMode int reason);
1378         void showSoftInput(long nativeVrShell, VrShell caller, boolean show);
1379         void updateWebInputIndices(long nativeVrShell, VrShell caller, int selectionStart,
1380                 int selectionEnd, int compositionStart, int compositionEnd);
1381         VrInputConnection getVrInputConnectionForTesting(long nativeVrShell, VrShell caller);
1382         void acceptDoffPromptForTesting(long nativeVrShell, VrShell caller);
1383         void performControllerActionForTesting(long nativeVrShell, VrShell caller, int elementName,
1384                 int actionType, float x, float y);
1385         void performKeyboardInputForTesting(
1386                 long nativeVrShell, VrShell caller, int inputType, String inputString);
1387         void setUiExpectingActivityForTesting(
1388                 long nativeVrShell, VrShell caller, int quiescenceTimeoutMs);
1389         void saveNextFrameBufferToDiskForTesting(
1390                 long nativeVrShell, VrShell caller, String filepathBase);
1391         void watchElementForVisibilityStatusForTesting(long nativeVrShell, VrShell caller,
1392                 int elementName, int timeoutMs, boolean visibility);
1393         void resumeContentRendering(long nativeVrShell, VrShell caller);
1394         void onOverlayTextureEmptyChanged(long nativeVrShell, VrShell caller, boolean empty);
1395         void requestRecordAudioPermissionResult(
1396                 long nativeVrShell, VrShell caller, boolean canRecordAudio);
1397     }
1398 }
1399