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.app.Activity;
8 import android.app.ActivityManager;
9 import android.app.ActivityOptions;
10 import android.app.PendingIntent;
11 import android.content.BroadcastReceiver;
12 import android.content.Context;
13 import android.content.Intent;
14 import android.content.IntentFilter;
15 import android.content.pm.ActivityInfo;
16 import android.content.res.Configuration;
17 import android.net.Uri;
18 import android.os.Build;
19 import android.os.Bundle;
20 import android.os.Handler;
21 import android.provider.Settings;
22 import android.view.View;
23 import android.view.ViewGroup;
24 import android.view.ViewGroup.LayoutParams;
25 import android.view.WindowManager;
26 import android.widget.FrameLayout;
27 
28 import androidx.annotation.IntDef;
29 import androidx.annotation.VisibleForTesting;
30 
31 import com.google.vr.ndk.base.AndroidCompat;
32 import com.google.vr.ndk.base.DaydreamApi;
33 import com.google.vr.ndk.base.GvrUiLayout;
34 
35 import org.chromium.base.ActivityState;
36 import org.chromium.base.ApplicationStatus;
37 import org.chromium.base.ContextUtils;
38 import org.chromium.base.Log;
39 import org.chromium.base.PackageUtils;
40 import org.chromium.base.ThreadUtils;
41 import org.chromium.base.annotations.CalledByNative;
42 import org.chromium.base.annotations.JNINamespace;
43 import org.chromium.base.annotations.NativeMethods;
44 import org.chromium.base.library_loader.LibraryLoader;
45 import org.chromium.base.metrics.RecordUserAction;
46 import org.chromium.base.task.AsyncTask;
47 import org.chromium.chrome.R;
48 import org.chromium.chrome.browser.ApplicationLifetime;
49 import org.chromium.chrome.browser.ChromeTabbedActivity;
50 import org.chromium.chrome.browser.app.ChromeActivity;
51 import org.chromium.chrome.browser.customtabs.CustomTabActivity;
52 import org.chromium.chrome.browser.feedback.HelpAndFeedbackLauncherImpl;
53 import org.chromium.chrome.browser.flags.ChromeFeatureList;
54 import org.chromium.chrome.browser.infobar.InfoBarIdentifier;
55 import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
56 import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
57 import org.chromium.chrome.browser.profiles.Profile;
58 import org.chromium.chrome.browser.tab.Tab;
59 import org.chromium.chrome.browser.tab.TabUtils;
60 import org.chromium.chrome.browser.tabmodel.TabModelSelector;
61 import org.chromium.chrome.browser.ui.messages.infobar.SimpleConfirmInfoBarBuilder;
62 import org.chromium.chrome.browser.webapps.WebappActivity;
63 import org.chromium.content_public.browser.ScreenOrientationDelegate;
64 import org.chromium.content_public.browser.ScreenOrientationProvider;
65 
66 import java.lang.annotation.Retention;
67 import java.lang.annotation.RetentionPolicy;
68 import java.lang.ref.WeakReference;
69 import java.lang.reflect.Method;
70 import java.util.HashSet;
71 import java.util.Set;
72 import java.util.concurrent.RejectedExecutionException;
73 
74 /**
75  * Manages interactions with the VR Shell.
76  */
77 @JNINamespace("vr")
78 public class VrShellDelegate
79         implements View.OnSystemUiVisibilityChangeListener, ScreenOrientationDelegate {
80     private static final String TAG = "VrShellDelegate";
81 
82     // Pseudo-random number to avoid request id collisions. Result codes must fit in lower 16 bits
83     // when used with startActivityForResult...
84     /* package */ static final int EXIT_VR_RESULT = 7212;
85     private static final int GVR_KEYBOARD_UPDATE_RESULT = 7214;
86 
87     @IntDef({EnterVRResult.NOT_NECESSARY, EnterVRResult.CANCELLED, EnterVRResult.REQUESTED,
88             EnterVRResult.SUCCEEDED})
89     @Retention(RetentionPolicy.SOURCE)
90     private @interface EnterVRResult {
91         int NOT_NECESSARY = 0;
92         int CANCELLED = 1;
93         int REQUESTED = 2;
94         int SUCCEEDED = 3;
95     }
96 
97     private static final String VR_ENTRY_RESULT_ACTION =
98             "org.chromium.chrome.browser.vr.VrEntryResult";
99 
100     private static final long REENTER_VR_TIMEOUT_MS = 1000;
101 
102     private static final String FEEDBACK_REPORT_TYPE = "USER_INITIATED_FEEDBACK_REPORT_VR";
103 
104     private static final String GVR_KEYBOARD_PACKAGE_ID = "com.google.android.vr.inputmethod";
105     private static final String GVR_KEYBOARD_MARKET_URI =
106             "market://details?id=" + GVR_KEYBOARD_PACKAGE_ID;
107 
108     // This value is intentionally probably overkill. This is the time we need to wait from when
109     // Chrome is resumed, to when Chrome actually renders a black frame, so that we can cancel the
110     // stay_hidden animation and not see a white monoscopic frame in-headset. 150ms is definitely
111     // too short, 250ms is sometimes too short for debug builds. 500ms should hopefully be safe even
112     // under fairly exceptional conditions, and won't delay entering VR a noticeable amount given
113     // how slow it already is.
114     private static final int WINDOW_FADE_ANIMATION_DURATION_MS = 500;
115 
116     /** ID for SavedInstanceState Bundle for whether Chrome was in VR when killed. */
117     private static final String IN_VR = "in_vr";
118 
119     private static VrShellDelegate sInstance;
120     private static VrBroadcastReceiver sVrBroadcastReceiver;
121     private static VrLifecycleObserver sVrLifecycleObserver;
122     private static VrDaydreamApi sVrDaydreamApi;
123     private static Set<Activity> sVrModeEnabledActivitys = new HashSet<>();
124     private static boolean sRegisteredDaydreamHook;
125     private static boolean sRegisteredVrAssetsComponent;
126     private static boolean sTestVrShellDelegateOnStartup;
127 
128     private ChromeActivity mActivity;
129 
130     private int mCachedGvrKeyboardPackageVersion;
131 
132     // How often to prompt the user to enter VR feedback.
133     private int mFeedbackFrequency;
134 
135     private VrShell mVrShell;
136     private Boolean mIsDaydreamCurrentViewer;
137     private boolean mProbablyInDon;
138     private boolean mInVr;
139     private boolean mNeedsAnimationCancel;
140     private boolean mCancellingEntryAnimation;
141 
142     // Whether or not the VR Device ON flow succeeded. If this is true it means the user has a VR
143     // headset on, but we haven't switched into VR mode yet.
144     // See further documentation here: https://developers.google.com/vr/daydream/guides/vr-entry
145     private boolean mDonSucceeded;
146     private boolean mShowingDaydreamDoff;
147     private boolean mShowingExitVrPrompt;
148     private boolean mDoffOptional;
149     // Listener to be called once we exited VR due to to an unsupported mode, e.g. the user clicked
150     // the URL bar security icon.
151     private OnExitVrRequestListener mOnExitVrRequestListener;
152     private Runnable mPendingExitVrRequest;
153     private Boolean mShowVrServicesUpdatePrompt;
154     private boolean mShowingDoffForGvrUpdate;
155     private boolean mExitedDueToUnsupportedMode;
156     private boolean mPaused;
157     private boolean mVisible;
158     private boolean mRestoreSystemUiVisibility;
159     private Integer mRestoreOrientation;
160     private boolean mRequestedWebVr;
161     private boolean mStartedFromVrIntent;
162 
163     private boolean mInternalIntentUsedToStartVr;
164 
165     // Set to true if performed VR browsing at least once. That is, this was not simply a WebVr
166     // presentation experience.
167     private boolean mVrBrowserUsed;
168 
169     private int mExpectedDensityChange;
170 
171     // Gets run when the user exits VR mode by clicking the 'x' button or system UI back button.
172     private Runnable mCloseButtonListener;
173 
174     // Gets run when the user exits VR mode by clicking the Gear button.
175     private Runnable mSettingsButtonListener;
176 
177     @VisibleForTesting
178     protected boolean mTestWorkaroundDontCancelVrEntryOnResume;
179 
180     private long mNativeVrShellDelegate;
181 
182     /* package */ static final class VrUnsupportedException extends RuntimeException {}
183 
184     private static final class VrLifecycleObserver
185             implements ApplicationStatus.ActivityStateListener {
186         @Override
onActivityStateChange(Activity activity, int newState)187         public void onActivityStateChange(Activity activity, int newState) {
188             switch (newState) {
189                 case ActivityState.DESTROYED:
190                     if (sVrBroadcastReceiver != null
191                             && sVrBroadcastReceiver.targetActivity().get() == activity) {
192                         sVrBroadcastReceiver.unregister();
193                         sVrBroadcastReceiver = null;
194                     }
195                     sVrModeEnabledActivitys.remove(activity);
196                     break;
197                 default:
198                     break;
199             }
200             if (sInstance != null) sInstance.onActivityStateChange(activity, newState);
201         }
202     }
203 
204     private static final class VrBroadcastReceiver extends BroadcastReceiver {
205         private final WeakReference<ChromeActivity> mTargetActivity;
206 
VrBroadcastReceiver(ChromeActivity activity)207         public VrBroadcastReceiver(ChromeActivity activity) {
208             ensureLifecycleObserverInitialized();
209             mTargetActivity = new WeakReference<ChromeActivity>(activity);
210         }
211 
212         @Override
onReceive(Context context, Intent intent)213         public void onReceive(Context context, Intent intent) {
214             ChromeActivity activity = mTargetActivity.get();
215             if (activity == null) return;
216             getInstance(activity);
217             assert sInstance != null;
218             if (sInstance == null) return;
219             sInstance.onBroadcastReceived();
220 
221             // Note that even though we are definitely entering VR here, we don't want to set
222             // the window mode yet, as setting the window mode while we're in the background can
223             // racily lead to that window mode change essentially being ignored, with future
224             // attempts to set the same window mode also being ignored.
225 
226             sInstance.mDonSucceeded = true;
227             sInstance.mProbablyInDon = false;
228             setVrModeEnabled(sInstance.mActivity, true);
229             if (VrDelegate.DEBUG_LOGS) Log.i(TAG, "VrBroadcastReceiver onReceive");
230 
231             // We add a black overlay view so that we can show black while the VR UI is loading.
232             if (!sInstance.mInVr) {
233                 VrModuleProvider.getDelegate().addBlackOverlayViewForActivity(sInstance.mActivity);
234             }
235 
236             if (sInstance.mPaused) {
237                 if (activity instanceof ChromeTabbedActivity) {
238                     // We can special case singleInstance activities like CTA to avoid having to use
239                     // moveTaskToFront. Using moveTaskToFront prevents us from disabling window
240                     // animations, and causes the system UI to show up during the preview window and
241                     // window animations.
242                     Intent launchIntent = new Intent(activity, activity.getClass());
243                     launchIntent = VrModuleProvider.getIntentDelegate().setupVrIntent(launchIntent);
244                     sInstance.mInternalIntentUsedToStartVr = true;
245                     sInstance.setExpectingIntent(true);
246                     getVrDaydreamApi().launchInVr(PendingIntent.getActivity(
247                             activity, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT));
248                 } else {
249                     // We start the Activity with a custom animation that keeps it hidden while
250                     // starting up to avoid Android showing stale 2D screenshots when the user is in
251                     // their VR headset. The animation lasts up to 10 seconds, but is cancelled when
252                     // we're resumed as at that time we'll be showing the black overlay added above.
253                     int animation = !sInstance.mInVr && VrDelegate.USE_HIDE_ANIMATION
254                             ? R.anim.stay_hidden
255                             : 0;
256                     sInstance.mNeedsAnimationCancel = animation != 0;
257                     Bundle options =
258                             ActivityOptions.makeCustomAnimation(activity, animation, 0).toBundle();
259                     ((ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE))
260                             .moveTaskToFront(activity.getTaskId(), 0, options);
261                 }
262             } else {
263                 // If a WebVR app calls requestPresent in response to the displayactivate event
264                 // after the DON flow completes, the DON flow is skipped, meaning our app won't be
265                 // paused when daydream fires our BroadcastReceiver, so onResume won't be called.
266                 sInstance.handleDonFlowSuccess();
267             }
268         }
269 
270         /**
271          * Unregisters this {@link BroadcastReceiver} from the activity it's registered to.
272          */
unregister()273         public void unregister() {
274             ChromeActivity activity = mTargetActivity.get();
275             if (activity == null) return;
276             try {
277                 activity.unregisterReceiver(VrBroadcastReceiver.this);
278             } catch (IllegalArgumentException e) {
279                 // Ignore this. This means our receiver was already unregistered somehow.
280             }
281         }
282 
targetActivity()283         WeakReference<ChromeActivity> targetActivity() {
284             return mTargetActivity;
285         }
286     }
287 
288     /**
289      * Immediately exits VR. If the user is in headset, they will see monoscopic UI while in the
290      * headset, so use with caution.
291      */
forceExitVrImmediately()292     public static void forceExitVrImmediately() {
293         if (sInstance == null) return;
294         sInstance.shutdownVr(true, true);
295     }
296 
297     /**
298      * See {@link Activity#onActivityResult}.
299      */
onActivityResultWithNative(int requestCode, int resultCode)300     public static boolean onActivityResultWithNative(int requestCode, int resultCode) {
301         // Handles the result of the exit VR flow (DOFF).
302         if (requestCode == EXIT_VR_RESULT) {
303             if (sInstance != null) sInstance.onExitVrResult(resultCode == Activity.RESULT_OK);
304             return true;
305         }
306         // Handles the result of requesting to update GVR Keyboard.
307         if (requestCode == GVR_KEYBOARD_UPDATE_RESULT) {
308             if (sInstance != null) sInstance.onGvrKeyboardMaybeUpdated();
309             return true;
310         }
311         return false;
312     }
313 
314     /**
315      * Called when the native library is first available.
316      */
onNativeLibraryAvailable()317     public static void onNativeLibraryAvailable() {
318         VrModule.ensureNativeLoaded();
319         VrModuleProvider.registerJni();
320         VrShellDelegateJni.get().onLibraryAvailable();
321     }
322 
323     /**
324      * Whether or not we are currently in VR.
325      */
isInVr()326     public static boolean isInVr() {
327         if (sInstance == null) return false;
328         return sInstance.mInVr;
329     }
330 
331     /**
332      * @return Whether 2D intents can safely be launched without showing non-VR UI to users in VR
333      *         headsets.
334      */
canLaunch2DIntents()335     public static boolean canLaunch2DIntents() {
336         if (!isInVr()) return true;
337         return sInstance.canLaunch2DIntentsInternal();
338     }
339 
340     /**
341      * See {@link ChromeActivity#handleBackPressed}
342      * Only handles the back press while in VR.
343      */
onBackPressed()344     public static boolean onBackPressed() {
345         if (sInstance == null) return false;
346         return sInstance.onBackPressedInternal();
347     }
348 
349     /**
350      * Enters VR on the current tab if possible.
351      *
352      * @return Whether VR entry succeeded (or is in progress).
353      */
enterVrIfNecessary()354     public static boolean enterVrIfNecessary() {
355         boolean created_delegate = sInstance == null;
356         VrShellDelegate instance = getInstance();
357         if (instance == null) return false;
358         int result = instance.enterVrInternal();
359         if (result == EnterVRResult.CANCELLED && created_delegate) instance.destroy();
360         return result != EnterVRResult.CANCELLED;
361     }
362 
363     /**
364      * If VR Shell is enabled, and the activity is supported, register with the Daydream
365      * platform that this app would like to be launched in VR when the device enters VR.
366      */
maybeRegisterVrEntryHook(final ChromeActivity activity)367     public static void maybeRegisterVrEntryHook(final ChromeActivity activity) {
368         // Daydream is not supported on pre-N devices.
369         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return;
370         if (sInstance != null) return; // Will be handled in onResume.
371         if (!VrModuleProvider.getDelegate().activitySupportsVrBrowsing(activity)
372                 && sRegisteredVrAssetsComponent) {
373             return;
374         }
375 
376         // Short-circuit the asnyc task if we've already queried support level previously. Creating
377         // the async task takes ~1ms on my Android Go device.
378         Integer vrSupportLevel = VrCoreInstallUtils.getCachedVrSupportLevel();
379         if (vrSupportLevel != null && vrSupportLevel != VrSupportLevel.VR_DAYDREAM) return;
380 
381         try {
382             // Reading VR support level and version can be slow, so do it asynchronously.
383             new AsyncTask<Integer>() {
384                 @Override
385                 protected Integer doInBackground() {
386                     return VrCoreInstallUtils.getVrSupportLevel();
387                 }
388 
389                 @Override
390                 protected void onPostExecute(Integer vrSupportLevel) {
391                     if (vrSupportLevel != VrSupportLevel.VR_DAYDREAM) return;
392 
393                     if (!sRegisteredVrAssetsComponent) {
394                         registerVrAssetsComponentIfDaydreamUser(isDaydreamCurrentViewer());
395                     }
396                 }
397             }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
398         } catch (RejectedExecutionException ex) {
399             // This isn't critical work, so it's okay to fail silently. If the user does try to
400             // enter VR the asset component may not be available, and headset insertion will go to
401             // Daydream rather than Chrome.
402         }
403     }
404 
405     /**
406      * When the app is pausing we need to unregister with the Daydream platform to prevent this app
407      * from being launched from the background when the device enters VR.
408      */
maybeUnregisterVrEntryHook()409     public static void maybeUnregisterVrEntryHook() {
410     }
411 
onMultiWindowModeChanged(boolean isInMultiWindowMode)412     public static void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
413         if (isInMultiWindowMode && isInVr()) {
414             sInstance.shutdownVr(true /* disableVrMode */, true /* stayingInChrome */);
415         }
416     }
417 
requestToExitVrForSearchEnginePromoDialog( OnExitVrRequestListener listener, Activity activity)418     public static void requestToExitVrForSearchEnginePromoDialog(
419             OnExitVrRequestListener listener, Activity activity) {
420         // When call site requests to exit VR, depend on the timing, Chrome may not in VR yet
421         // (Chrome only enter VR after onNewIntentWithNative is called in the cold start case).
422         // While not in VR, calling requestToExitVr would immediately notify listener that exit VR
423         // succeed (without showing DOFF screen). If call site decide to show 2D UI when exit VR
424         // succeeded, it leads to case that 2D UI is showing on top of VR when Chrome eventually
425         // enters VR. To prevent this from happening, we set mPendingExitVrRequest which should be
426         // executed at runPendingExitVrTask. runPendingExitVrTask is called after it is safe to
427         // request exit VR.
428         if (isInVr()) {
429             sInstance.requestToExitVrInternal(
430                     listener, UiUnsupportedMode.SEARCH_ENGINE_PROMO, false);
431         } else {
432             // Making sure that we response to this request as it is very important that search
433             // engine promo dialog isn't ignored due to VR.
434             assert VrModuleProvider.getIntentDelegate().isVrIntent(activity.getIntent());
435             VrShellDelegate instance = getInstance();
436             if (instance == null) {
437                 listener.onDenied();
438                 return;
439             }
440             sInstance.mPendingExitVrRequest = () -> {
441                 VrShellDelegate.requestToExitVr(listener, UiUnsupportedMode.SEARCH_ENGINE_PROMO);
442             };
443         }
444     }
445 
requestToExitVr(OnExitVrRequestListener listener)446     public static void requestToExitVr(OnExitVrRequestListener listener) {
447         requestToExitVr(listener, UiUnsupportedMode.GENERIC_UNSUPPORTED_FEATURE);
448     }
449 
requestToExitVr( OnExitVrRequestListener listener, @UiUnsupportedMode int reason)450     public static void requestToExitVr(
451             OnExitVrRequestListener listener, @UiUnsupportedMode int reason) {
452         // If we're not in VR, just say that we've successfully exited VR.
453         if (sInstance == null || !sInstance.mInVr) {
454             listener.onSucceeded();
455             return;
456         }
457         sInstance.requestToExitVrInternal(listener, reason, !supports2dInVr());
458     }
459 
requestToExitVrAndRunOnSuccess(Runnable onSuccess)460     public static void requestToExitVrAndRunOnSuccess(Runnable onSuccess) {
461         requestToExitVrAndRunOnSuccess(onSuccess, UiUnsupportedMode.GENERIC_UNSUPPORTED_FEATURE);
462     }
463 
requestToExitVrAndRunOnSuccess( Runnable onSuccess, @UiUnsupportedMode int reason)464     public static void requestToExitVrAndRunOnSuccess(
465             Runnable onSuccess, @UiUnsupportedMode int reason) {
466         requestToExitVr(new OnExitVrRequestListener() {
467             @Override
468             public void onSucceeded() {
469                 onSuccess.run();
470             }
471 
472             @Override
473             public void onDenied() {}
474         }, reason);
475     }
476 
477     /**
478      * Called when the {@link ChromeActivity} becomes visible.
479      */
onActivityShown(ChromeActivity activity)480     public static void onActivityShown(ChromeActivity activity) {
481         if (sInstance != null && sInstance.mActivity == activity) sInstance.onActivityShown();
482     }
483 
484     /**
485      * Called when the {@link ChromeActivity} is hidden.
486      */
onActivityHidden(ChromeActivity activity)487     public static void onActivityHidden(ChromeActivity activity) {
488         if (sInstance != null && sInstance.mActivity == activity) sInstance.onActivityHidden();
489     }
490 
491     /**
492      * @return Whether VrShellDelegate handled the density change. If the density change is
493      * unhandled, the Activity should be recreated in order to handle the change.
494      */
onDensityChanged(int oldDpi, int newDpi)495     public static boolean onDensityChanged(int oldDpi, int newDpi) {
496         if (VrDelegate.DEBUG_LOGS) Log.i(TAG, "onDensityChanged [%d]->[%d] ", oldDpi, newDpi);
497         if (sInstance == null) return false;
498         // If density changed while in VR, we expect a second density change to restore the density
499         // to what it previously was when we exit VR. We shouldn't have to recreate the activity as
500         // all non-VR UI is still using the old density.
501         if (sInstance.mExpectedDensityChange != 0) {
502             assert !sInstance.mInVr && !sInstance.mDonSucceeded;
503             int expectedDensity = sInstance.mExpectedDensityChange;
504             sInstance.mExpectedDensityChange = 0;
505             return (newDpi == expectedDensity);
506         }
507         if (sInstance.mInVr || sInstance.mDonSucceeded) {
508             sInstance.mExpectedDensityChange = oldDpi;
509             return true;
510         }
511         return false;
512     }
513 
514     /**
515      * @param topContentOffset The top content offset (usually applied by the omnibox).
516      */
rawTopContentOffsetChanged(float topContentOffset)517     public static void rawTopContentOffsetChanged(float topContentOffset) {
518         assert isInVr();
519         sInstance.mVrShell.rawTopContentOffsetChanged(topContentOffset);
520     }
521 
522     /**
523      * This is called every time ChromeActivity gets a new intent.
524      */
onNewIntentWithNative(ChromeActivity activity, Intent intent)525     public static void onNewIntentWithNative(ChromeActivity activity, Intent intent) {
526         if (activity.isFinishing()) return;
527         if (!VrModuleProvider.getIntentDelegate().isLaunchingIntoVr(activity, intent)) return;
528 
529         VrShellDelegate instance = getInstance(activity);
530         if (instance == null) return;
531         instance.onNewVrIntent();
532     }
533 
534     /**
535      * This is called when ChromeTabbedActivity gets a new intent before native is initialized.
536      */
maybeHandleVrIntentPreNative(ChromeActivity activity, Intent intent)537     public static void maybeHandleVrIntentPreNative(ChromeActivity activity, Intent intent) {
538         boolean launchingIntoVr =
539                 VrModuleProvider.getIntentDelegate().isLaunchingIntoVr(activity, intent);
540 
541         if (!launchingIntoVr) {
542             // We trust that if an intent is targeted for 2D, that Chrome should switch to 2D
543             // regardless of whether the user is in headset.
544             if (VrShellDelegate.isInVr()) VrShellDelegate.forceExitVrImmediately();
545             return;
546         }
547 
548         if (VrModuleProvider.getDelegate().bootsToVr() && launchingIntoVr) {
549             if (VrModuleProvider.getDelegate().relaunchOnMainDisplayIfNecessary(activity, intent)) {
550                 return;
551             }
552         }
553 
554         if (sInstance != null && !sInstance.mInternalIntentUsedToStartVr) {
555             sInstance.swapHostActivity(activity, false /* disableVrMode */);
556             // If the user has launched Chrome from the launcher, rather than resuming from the
557             // dashboard, we don't want to launch into presentation.
558             sInstance.exitWebVRAndClearState();
559         }
560 
561         if (sInstance != null) sInstance.setExpectingIntent(false);
562 
563         if (VrDelegate.DEBUG_LOGS) {
564             Log.i(TAG, "maybeHandleVrIntentPreNative: preparing for transition");
565         }
566 
567         // We add a black overlay view so that we can show black while the VR UI is loading.
568         // Note that this alone isn't sufficient to prevent 2D UI from showing when
569         // auto-presenting WebVR. See comment about the custom animation in {@link
570         // getVrIntentOptions}.
571         // TODO(crbug.com/775574): This hack doesn't really work to hide the 2D UI on Samsung
572         // devices since Chrome gets paused and we prematurely remove the overlay.
573         if (sInstance == null || !sInstance.mInVr) {
574             VrModuleProvider.getDelegate().addBlackOverlayViewForActivity(activity);
575         }
576 
577         // Enable VR mode and hide system UI. We do this here so we don't get kicked out of
578         // VR mode and to prevent seeing a flash of system UI.
579         setVrModeEnabled(activity, true);
580         VrModuleProvider.getDelegate().setSystemUiVisibilityForVr(activity);
581     }
582 
583     /**
584      * Asynchronously enable VR mode.
585      */
setVrModeEnabled(Activity activity, boolean enabled)586     public static void setVrModeEnabled(Activity activity, boolean enabled) {
587         ensureLifecycleObserverInitialized();
588         if (enabled) {
589             if (sVrModeEnabledActivitys.contains(activity)) return;
590             AndroidCompat.setVrModeEnabled(activity, true);
591             sVrModeEnabledActivitys.add(activity);
592         } else {
593             if (!sVrModeEnabledActivitys.contains(activity)) return;
594             AndroidCompat.setVrModeEnabled(activity, false);
595             sVrModeEnabledActivitys.remove(activity);
596         }
597     }
598 
599     /**
600      * Performs pre-inflation VR-related startup.
601      */
doPreInflationStartup(ChromeActivity activity, Bundle savedInstanceState)602     public static void doPreInflationStartup(ChromeActivity activity, Bundle savedInstanceState) {
603         // We need to explicitly enable VR mode here so that the system doesn't kick us out of VR,
604         // or drop us into the 2D-in-VR rendering mode, while we prepare for VR rendering.
605         if (VrModuleProvider.getIntentDelegate().isLaunchingIntoVr(
606                     activity, activity.getIntent())) {
607             setVrModeEnabled(activity, true);
608         } else if (savedInstanceState != null && savedInstanceState.getBoolean(IN_VR, false)) {
609             // When Chrome is restored from a SavedInstanceState with VR mode still on we need to
610             // Explicitly turn VR mode off even though we can't really know for sure whether or not
611             // it's currently on.
612             AndroidCompat.setVrModeEnabled(activity, false);
613             sVrModeEnabledActivitys.remove(activity);
614         }
615     }
616 
617     /**
618      * See {@link Activity#onSaveInstanceState(Bundle)}
619      */
onSaveInstanceState(Bundle outState)620     public static void onSaveInstanceState(Bundle outState) {
621         if (isInVr()) outState.putBoolean(IN_VR, true);
622     }
623 
initAfterModuleInstall()624     public static void initAfterModuleInstall() {
625         if (!LibraryLoader.getInstance().isInitialized()) return;
626         onNativeLibraryAvailable();
627         Activity activity = ApplicationStatus.getLastTrackedFocusedActivity();
628         if (activity instanceof ChromeActivity
629                 && ApplicationStatus.getStateForActivity(activity) == ActivityState.RESUMED) {
630             maybeRegisterVrEntryHook((ChromeActivity) activity);
631         }
632     }
633 
634     /**
635      * @return A Daydream Api instance, for interacting with Daydream platform features.
636      */
getVrDaydreamApi()637     public static VrDaydreamApi getVrDaydreamApi() {
638         if (sVrDaydreamApi == null) sVrDaydreamApi = new VrDaydreamApi();
639         return sVrDaydreamApi;
640     }
641 
isDaydreamCurrentViewer()642     public static boolean isDaydreamCurrentViewer() {
643         if (sInstance != null) return sInstance.isDaydreamCurrentViewerInternal();
644         return getVrDaydreamApi().isDaydreamCurrentViewer();
645     }
646 
supports2dInVr()647     public static boolean supports2dInVr() {
648         Context context = ContextUtils.getApplicationContext();
649         return VrCoreInstallUtils.isDaydreamReadyDevice() && DaydreamApi.supports2dInVr(context);
650     }
651 
enableTestVrShellDelegateOnStartupForTesting()652     protected static void enableTestVrShellDelegateOnStartupForTesting() {
653         sTestVrShellDelegateOnStartup = true;
654     }
655 
isVrModeEnabled(Activity activity)656     /* package */ static boolean isVrModeEnabled(Activity activity) {
657         return sVrModeEnabledActivitys.contains(activity);
658     }
659 
expectedDensityChange()660     /* package */ static boolean expectedDensityChange() {
661         return sInstance != null && sInstance.mExpectedDensityChange != 0;
662     }
663 
activitySupportsPresentation(Activity activity)664     private static boolean activitySupportsPresentation(Activity activity) {
665         return activity instanceof ChromeTabbedActivity || activity instanceof CustomTabActivity
666                 || activity instanceof WebappActivity;
667     }
668 
activitySupportsExitFeedback(Activity activity)669     private static boolean activitySupportsExitFeedback(Activity activity) {
670         return activity instanceof ChromeTabbedActivity
671                 && ChromeFeatureList.isEnabled(ChromeFeatureList.VR_BROWSING_FEEDBACK);
672     }
673 
registerVrAssetsComponentIfDaydreamUser(boolean isDaydreamCurrentViewer)674     private static void registerVrAssetsComponentIfDaydreamUser(boolean isDaydreamCurrentViewer) {
675         assert !sRegisteredVrAssetsComponent;
676         if (isDaydreamCurrentViewer) {
677             VrShellDelegateJni.get().registerVrAssetsComponent();
678             sRegisteredVrAssetsComponent = true;
679         }
680         SharedPreferencesManager.getInstance().writeBoolean(
681                 ChromePreferenceKeys.VR_SHOULD_REGISTER_ASSETS_COMPONENT_ON_STARTUP,
682                 isDaydreamCurrentViewer);
683     }
684 
685     // We need a custom Intent for entering VR in order to support VR in Custom Tabs. Custom Tabs
686     // are not a singleInstance activity, so they cannot be resumed through Activity PendingIntents,
687     // which is the typical way Daydream resumes your Activity. Instead, we use a broadcast intent
688     // and then use the broadcast to bring ourselves back to the foreground.
getEnterVrPendingIntent(ChromeActivity activity)689     /* package */ static PendingIntent getEnterVrPendingIntent(ChromeActivity activity) {
690         if (sVrBroadcastReceiver != null) sVrBroadcastReceiver.unregister();
691         IntentFilter filter = new IntentFilter(VR_ENTRY_RESULT_ACTION);
692         VrBroadcastReceiver receiver = new VrBroadcastReceiver(activity);
693         // If we set sVrBroadcastReceiver then use it in registerReceiver, findBugs considers this
694         // a thread-safety issue since it thinks the receiver isn't fully initialized before being
695         // exposed to other threads. This isn't actually an issue in this case, but we need to set
696         // sVrBroadcastReceiver after we're done using it here to fix the compile error.
697         activity.registerReceiver(receiver, filter);
698         sVrBroadcastReceiver = receiver;
699         Intent vrIntent = new Intent(VR_ENTRY_RESULT_ACTION);
700         vrIntent.setPackage(activity.getPackageName());
701         return PendingIntent.getBroadcast(activity, 0, vrIntent, PendingIntent.FLAG_UPDATE_CURRENT);
702     }
703 
isVrBrowsingSupported(ChromeActivity activity)704     private static boolean isVrBrowsingSupported(ChromeActivity activity) {
705         return false;
706     }
707 
708     /**
709      * @return Whether or not VR Browsing is currently enabled for the given Activity.
710      */
isVrBrowsingEnabled(ChromeActivity activity, int vrSupportLevel)711     /* package */ static boolean isVrBrowsingEnabled(ChromeActivity activity, int vrSupportLevel) {
712         return isVrBrowsingSupported(activity) && vrSupportLevel == VrSupportLevel.VR_DAYDREAM;
713     }
714 
isInVrSession()715     /* package */ static boolean isInVrSession() {
716         Context context = ContextUtils.getApplicationContext();
717         // The call to isInVrSession crashes when called on a non-Daydream ready device, so we add
718         // the device check (b/77268533).
719         try {
720             return VrCoreInstallUtils.isDaydreamReadyDevice() && DaydreamApi.isInVrSession(context);
721         } catch (Exception ex) {
722             Log.e(TAG, "Unable to check if in VR session", ex);
723             return false;
724         }
725     }
726 
startFeedback(Tab tab)727     private static void startFeedback(Tab tab) {
728         // TODO(ymalik): This call will connect to the Google Services api which can be slow. Can we
729         // connect to it beforehand when we know that we'll be prompting for feedback?
730         HelpAndFeedbackLauncherImpl.getInstance().showFeedback(TabUtils.getActivity(tab),
731                 Profile.fromWebContents(tab.getWebContents()), tab.getUrlString(),
732                 ContextUtils.getApplicationContext().getPackageName() + "." + FEEDBACK_REPORT_TYPE);
733     }
734 
promptForFeedback(final Tab tab)735     private static void promptForFeedback(final Tab tab) {
736         if (tab == null) return;
737         SimpleConfirmInfoBarBuilder.Listener listener = new SimpleConfirmInfoBarBuilder.Listener() {
738             @Override
739             public void onInfoBarDismissed() {}
740 
741             @Override
742             public boolean onInfoBarButtonClicked(boolean isPrimary) {
743                 if (isPrimary) {
744                     startFeedback(tab);
745                 } else {
746                     VrFeedbackStatus.setFeedbackOptOut(true);
747                 }
748                 return false;
749             }
750 
751             @Override
752             public boolean onInfoBarLinkClicked() {
753                 return false;
754             }
755         };
756 
757         SimpleConfirmInfoBarBuilder.create(tab.getWebContents(), listener,
758                 InfoBarIdentifier.VR_FEEDBACK_INFOBAR_ANDROID, tab.getContext(),
759                 org.chromium.chrome.vr.R.drawable.vr_services,
760                 ContextUtils.getApplicationContext().getString(
761                         org.chromium.chrome.vr.R.string.vr_shell_feedback_infobar_description),
762                 ContextUtils.getApplicationContext().getString(
763                         org.chromium.chrome.vr.R.string.vr_shell_feedback_infobar_feedback_button),
764                 tab.getContext().getString(R.string.no_thanks), null /* linkText */,
765                 true /* autoExpire  */);
766     }
767 
ensureLifecycleObserverInitialized()768     private static void ensureLifecycleObserverInitialized() {
769         if (sVrLifecycleObserver != null) return;
770         sVrLifecycleObserver = new VrLifecycleObserver();
771         ApplicationStatus.registerStateListenerForAllActivities(sVrLifecycleObserver);
772     }
773 
774     @CalledByNative
getInstance()775     private static VrShellDelegate getInstance() {
776         Activity activity = ApplicationStatus.getLastTrackedFocusedActivity();
777         if (!(activity instanceof ChromeActivity)) return null;
778         return getInstance((ChromeActivity) activity);
779     }
780 
781     @SuppressWarnings("unchecked")
getInstance(ChromeActivity activity)782     private static VrShellDelegate getInstance(ChromeActivity activity) {
783         if (!LibraryLoader.getInstance().isInitialized()) return null;
784         if (activity == null || !activitySupportsPresentation(activity)) return null;
785         if (sInstance != null) return sInstance;
786         ThreadUtils.assertOnUiThread();
787         if (sTestVrShellDelegateOnStartup) {
788             try {
789                 // This should only ever be run during tests on standalone devices. Normally, we
790                 // create a TestVrShellDelegate during pre-test setup after Chrome has started.
791                 // However, since Chrome is started in VR on standalones, creating a
792                 // TestVrShellDelegate after startup discards the existing VrShellDelegate instance
793                 // that's in use, which is bad. So, in those cases, create a TestVrShellDelegate
794                 // instead of the production version.
795                 Class clazz = Class.forName("org.chromium.chrome.browser.vr.TestVrShellDelegate");
796                 Method method = clazz.getMethod("createTestVrShellDelegate", ChromeActivity.class);
797                 method.invoke(null, activity);
798             } catch (Exception e) {
799                 assert false;
800             }
801         } else {
802             sInstance = new VrShellDelegate(activity);
803         }
804         return sInstance;
805     }
806 
VrShellDelegate(ChromeActivity activity)807     protected VrShellDelegate(ChromeActivity activity) {
808         mActivity = activity;
809         // If an activity isn't resumed at the point, it must have been paused.
810         mPaused = ApplicationStatus.getStateForActivity(activity) != ActivityState.RESUMED;
811         mVisible = activity.hasWindowFocus();
812         mNativeVrShellDelegate = VrShellDelegateJni.get().init(VrShellDelegate.this);
813         mFeedbackFrequency = VrFeedbackStatus.getFeedbackFrequency();
814         ensureLifecycleObserverInitialized();
815         if (!mPaused) onResume();
816 
817         sInstance = this;
818     }
819 
onActivityStateChange(Activity activity, int newState)820     public void onActivityStateChange(Activity activity, int newState) {
821         switch (newState) {
822             case ActivityState.DESTROYED:
823                 if (activity == mActivity) destroy();
824                 break;
825             case ActivityState.PAUSED:
826                 if (activity == mActivity) onPause();
827                 // Other activities should only pause while we're paused due to Android lifecycle.
828                 assert mPaused;
829                 break;
830             case ActivityState.STOPPED:
831                 if (activity == mActivity) onStop();
832                 break;
833             case ActivityState.STARTED:
834                 if (activity == mActivity) onStart();
835                 break;
836             case ActivityState.RESUMED:
837                 if (!activitySupportsPresentation(activity)) return;
838                 if (!(activity instanceof ChromeActivity)) return;
839                 swapHostActivity((ChromeActivity) activity, true /* disableVrMode */);
840                 onResume();
841                 break;
842             default:
843                 break;
844         }
845     }
846 
847     // Called when an activity that supports VR is resumed, and attaches VrShellDelegate to that
848     // activity.
swapHostActivity(ChromeActivity activity, boolean disableVrMode)849     private void swapHostActivity(ChromeActivity activity, boolean disableVrMode) {
850         assert mActivity != null;
851         if (mActivity == activity) return;
852         if (mInVr) shutdownVr(disableVrMode, false /* stayingInChrome */);
853         mActivity = activity;
854     }
855 
getGvrKeyboardPackageVersion()856     private int getGvrKeyboardPackageVersion() {
857         return PackageUtils.getPackageVersion(
858                 ContextUtils.getApplicationContext(), GVR_KEYBOARD_PACKAGE_ID);
859     }
860 
isVrBrowsingEnabled()861     protected boolean isVrBrowsingEnabled() {
862         return isVrBrowsingEnabled(mActivity, VrCoreInstallUtils.getVrSupportLevel());
863     }
864 
onGvrKeyboardMaybeUpdated()865     private void onGvrKeyboardMaybeUpdated() {
866         if (mCachedGvrKeyboardPackageVersion == getGvrKeyboardPackageVersion()) return;
867         ApplicationLifetime.terminate(true);
868     }
869 
maybeSetPresentResult(boolean result)870     private void maybeSetPresentResult(boolean result) {
871         if (mNativeVrShellDelegate == 0 || !mRequestedWebVr) return;
872         VrShellDelegateJni.get().setPresentResult(
873                 mNativeVrShellDelegate, VrShellDelegate.this, result);
874         mRequestedWebVr = false;
875     }
876 
877     /**
878      * Handle a successful VR DON flow, entering VR in the process unless we're unable to.
879      * @return False if VR entry failed.
880      */
enterVrAfterDon()881     private boolean enterVrAfterDon() {
882         if (mNativeVrShellDelegate == 0) return false;
883         if (!canEnterVr()) return false;
884 
885         enterVr();
886 
887         // The user has successfully completed a DON flow.
888         RecordUserAction.record("VR.DON");
889 
890         return true;
891     }
892 
enterVr()893     private void enterVr() {
894         // We should only enter VR when we're the resumed Activity or our changes to things like
895         // system UI flags might get lost.
896         assert !mPaused;
897         assert mNativeVrShellDelegate != 0;
898         if (mInVr) return;
899         mInVr = true;
900         setVrModeEnabled(mActivity, true);
901 
902         setWindowModeForVr();
903 
904         // We assume that we triggered the DON flow already for Daydream viewers. If that changes,
905         // we need to make sure not to report success/fail to WebXR until after the DON flow runs.
906         assert mDonSucceeded || !isDaydreamCurrentViewerInternal();
907 
908         mDonSucceeded = false;
909         if (!createVrShell()) {
910             cancelPendingVrEntry();
911             mInVr = false;
912             getVrDaydreamApi().launchVrHomescreen();
913             return;
914         }
915         mExitedDueToUnsupportedMode = false;
916 
917         addVrViews();
918         // Make sure that assets component is registered when creating native VR shell.
919         if (!sRegisteredVrAssetsComponent) {
920             registerVrAssetsComponentIfDaydreamUser(isDaydreamCurrentViewer());
921         }
922         mVrShell.initializeNative(mRequestedWebVr, VrModuleProvider.getDelegate().bootsToVr());
923         mVrShell.setWebVrModeEnabled(mRequestedWebVr);
924 
925         // We're entering VR, but not in WebVr mode.
926         mVrBrowserUsed = !mRequestedWebVr;
927 
928         // resume needs to be called on GvrLayout after initialization to make sure DON flow works
929         // properly.
930         if (mVisible) mVrShell.resume();
931         mVrShell.getContainer().setOnSystemUiVisibilityChangeListener(this);
932 
933         maybeSetPresentResult(true);
934 
935         VrModuleProvider.onEnterVr();
936     }
937 
onVrIntentUnsupported()938     private void onVrIntentUnsupported() {
939         // If entering VR is unsupported for some reason, clean up what we did in
940         // maybeHandleVrIntentPreNative.
941         assert !mInVr;
942         mStartedFromVrIntent = false;
943         cancelPendingVrEntry();
944 
945         // Some Samsung devices change the screen density after exiting VR mode which causes
946         // us to restart Chrome with the VR intent that originally started it. We don't want to
947         // enable VR mode when the user opens Chrome again in 2D mode, so we remove VR specific
948         // extras.
949         VrModuleProvider.getIntentDelegate().removeVrExtras(mActivity.getIntent());
950 
951         // We may still be showing the STAY_HIDDEN animation, so cancel it if necessary.
952         cancelStartupAnimationIfNeeded();
953     }
954 
onNewVrIntent()955     private void onNewVrIntent() {
956         // We set the the system UI in maybeHandleVrIntentPreNative, so make sure we restore it when
957         // we exit VR, or cancel VR entry.
958         mRestoreSystemUiVisibility = true;
959 
960         // Nothing to do if we were launched by an internal intent.
961         if (mInternalIntentUsedToStartVr) {
962             mInternalIntentUsedToStartVr = false;
963 
964             // TODO(mthiesse): This shouldn't be necessary. This is another instance of b/65681875,
965             // where the intent is received after we're resumed.
966             if (mInVr) return;
967 
968             // This is extremely unlikely in practice. Some code must have called shutdownVR() while
969             // we were entering VR through NFC insertion.
970             if (!mDonSucceeded) cancelPendingVrEntry();
971             return;
972         }
973 
974         if (VrDelegate.USE_HIDE_ANIMATION) mNeedsAnimationCancel = true;
975 
976         if (!isVrBrowsingSupported(mActivity)) {
977             onVrIntentUnsupported();
978             return;
979         }
980 
981         mStartedFromVrIntent = true;
982         // Setting DON succeeded will cause us to enter VR when resuming.
983         mDonSucceeded = true;
984 
985         if (!mPaused) {
986             // Note that canceling the animation below is what causes us to enter VR mode. We start
987             // an intermediate activity to cancel the animation which causes onPause and onResume to
988             // be called and we enter VR mode in onResume (because we set the mEnterVrOnStartup bit
989             // above). If Chrome is already running, onResume which will be called after
990             // VrShellDelegate#onNewIntentWithNative which will cancel the animation and enter VR
991             // after that.
992             if (!cancelStartupAnimationIfNeeded()) {
993                 // If we didn't cancel the startup animation, we won't be getting another onResume
994                 // call, so enter VR here.
995                 handleDonFlowSuccess();
996                 runPendingExitVrTask();
997             }
998         }
999     }
1000 
runPendingExitVrTask()1001     private void runPendingExitVrTask() {
1002         if (mPendingExitVrRequest == null) return;
1003         new Handler().post(mPendingExitVrRequest);
1004         mPendingExitVrRequest = null;
1005     }
1006 
1007     @Override
onSystemUiVisibilityChange(int visibility)1008     public void onSystemUiVisibilityChange(int visibility) {
1009         if (mInVr && !isWindowModeCorrectForVr()) {
1010             setWindowModeForVr();
1011         }
1012     }
1013 
1014     @Override
canUnlockOrientation(Activity activity, int defaultOrientation)1015     public boolean canUnlockOrientation(Activity activity, int defaultOrientation) {
1016         if (mActivity == activity && mRestoreOrientation != null) {
1017             mRestoreOrientation = defaultOrientation;
1018             return false;
1019         }
1020         return true;
1021     }
1022 
1023     @Override
canLockOrientation()1024     public boolean canLockOrientation() {
1025         return false;
1026     }
1027 
hasRecordAudioPermission()1028     public boolean hasRecordAudioPermission() {
1029         return mActivity.getWindowAndroid().hasPermission(android.Manifest.permission.RECORD_AUDIO);
1030     }
1031 
canRequestRecordAudioPermission()1032     public boolean canRequestRecordAudioPermission() {
1033         return mActivity.getWindowAndroid().canRequestPermission(
1034                 android.Manifest.permission.RECORD_AUDIO);
1035     }
1036 
isWindowModeCorrectForVr()1037     private boolean isWindowModeCorrectForVr() {
1038         int flags = mActivity.getWindow().getDecorView().getSystemUiVisibility();
1039         int orientation = mActivity.getResources().getConfiguration().orientation;
1040         // Mask the flags to only those that we care about.
1041         return (flags & VrDelegate.VR_SYSTEM_UI_FLAGS) == VrDelegate.VR_SYSTEM_UI_FLAGS
1042                 && orientation == Configuration.ORIENTATION_LANDSCAPE;
1043     }
1044 
setWindowModeForVr()1045     private void setWindowModeForVr() {
1046         // Decouple the compositor size from the view size, or we'll get an unnecessary resize due
1047         // to the orientation change when entering VR, then another resize once VR has settled on
1048         // the content size.
1049         if (mActivity.getCompositorViewHolder() != null) {
1050             mActivity.getCompositorViewHolder().onEnterVr();
1051         }
1052         ScreenOrientationProvider.getInstance().setOrientationDelegate(this);
1053 
1054         // Hide system UI.
1055         VrModuleProvider.getDelegate().setSystemUiVisibilityForVr(mActivity);
1056 
1057         // Set correct orientation.
1058         if (mRestoreOrientation == null) {
1059             mRestoreOrientation = mActivity.getRequestedOrientation();
1060         }
1061 
1062         mRestoreSystemUiVisibility = true;
1063 
1064         mActivity.getWindow().getAttributes().rotationAnimation =
1065                 WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT;
1066         mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
1067     }
1068 
restoreWindowMode()1069     private void restoreWindowMode() {
1070         ScreenOrientationProvider.getInstance().setOrientationDelegate(null);
1071         mActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
1072 
1073         // Restore orientation.
1074         if (mRestoreOrientation != null) mActivity.setRequestedOrientation(mRestoreOrientation);
1075         mRestoreOrientation = null;
1076 
1077         // Restore system UI visibility.
1078         if (mRestoreSystemUiVisibility) {
1079             int flags = mActivity.getWindow().getDecorView().getSystemUiVisibility();
1080             mActivity.getWindow().getDecorView().setSystemUiVisibility(
1081                     flags & ~VrDelegate.VR_SYSTEM_UI_FLAGS);
1082         }
1083         mRestoreSystemUiVisibility = false;
1084         if (mActivity.getCompositorViewHolder() != null) {
1085             mActivity.getCompositorViewHolder().onExitVr();
1086         }
1087 
1088         mActivity.getWindow().getAttributes().rotationAnimation =
1089                 WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE;
1090     }
1091 
canEnterVr()1092     /* package */ boolean canEnterVr() {
1093         if (VrCoreInstallUtils.vrSupportNeedsUpdate()) return false;
1094 
1095         // If VR browsing is not enabled and this is not a WebXR request, then return false.
1096         if (!isVrBrowsingEnabled() && !mRequestedWebVr) return false;
1097         return true;
1098     }
1099 
1100     @CalledByNative
presentRequested()1101     private void presentRequested() {
1102         if (VrDelegate.DEBUG_LOGS) Log.i(TAG, "WebVR page requested presentation");
1103         mRequestedWebVr = true;
1104         if (VrModuleProvider.getDelegate().bootsToVr() && !mInVr) {
1105             maybeSetPresentResult(false);
1106             return;
1107         }
1108         switch (enterVrInternal()) {
1109             case EnterVRResult.NOT_NECESSARY:
1110                 mVrShell.setWebVrModeEnabled(true);
1111                 maybeSetPresentResult(true);
1112                 break;
1113             case EnterVRResult.CANCELLED:
1114                 maybeSetPresentResult(false);
1115                 break;
1116             case EnterVRResult.REQUESTED:
1117                 break;
1118             case EnterVRResult.SUCCEEDED:
1119                 maybeSetPresentResult(true);
1120                 break;
1121             default:
1122                 Log.e(TAG, "Unexpected enum.");
1123         }
1124     }
1125 
1126     /**
1127      * Enters VR Shell if necessary, displaying browser UI and tab contents in VR.
1128      */
1129     @EnterVRResult
enterVrInternal()1130     private int enterVrInternal() {
1131         if (mPaused) return EnterVRResult.CANCELLED;
1132         if (mInVr) return EnterVRResult.NOT_NECESSARY;
1133         if (!canEnterVr()) return EnterVRResult.CANCELLED;
1134 
1135         if (VrCoreInstallUtils.getVrSupportLevel() == VrSupportLevel.VR_DAYDREAM
1136                 && isDaydreamCurrentViewerInternal()) {
1137             // TODO(mthiesse): This is a workaround for b/66486878 (see also crbug.com/767594).
1138             // We have to trigger the DON flow before setting VR mode enabled to prevent the DON
1139             // flow from failing on the S8/S8+.
1140             // Due to b/66493165, we also can't create our VR UI before the density has changed,
1141             // so we can't trigger the DON flow by resuming the GvrLayout. This basically means that
1142             // calling launchInVr on ourself is the only viable option for getting into VR on the
1143             // S8/S8+.
1144             // This also fixes the issue tracked in crbug.com/767944, so this should not be removed
1145             // until the root cause of that has been found and fixed.
1146             getVrDaydreamApi().launchInVr(getEnterVrPendingIntent(mActivity));
1147             mProbablyInDon = true;
1148         } else {
1149             enterVr();
1150         }
1151         return EnterVRResult.REQUESTED;
1152     }
1153 
requestToExitVrInternal(OnExitVrRequestListener listener, @UiUnsupportedMode int reason, boolean showExitPromptBeforeDoff)1154     private void requestToExitVrInternal(OnExitVrRequestListener listener,
1155             @UiUnsupportedMode int reason, boolean showExitPromptBeforeDoff) {
1156         assert listener != null;
1157         if (VrModuleProvider.getDelegate().bootsToVr()) {
1158             setVrModeEnabled(mActivity, false);
1159             listener.onSucceeded();
1160             return;
1161         }
1162 
1163         // If we are currently processing another request, deny the request.
1164         if (mOnExitVrRequestListener != null) {
1165             listener.onDenied();
1166             return;
1167         }
1168         mOnExitVrRequestListener = listener;
1169         mShowingExitVrPrompt = showExitPromptBeforeDoff;
1170         mVrShell.requestToExitVr(reason, showExitPromptBeforeDoff);
1171     }
1172 
exitWebVRAndClearState()1173     private void exitWebVRAndClearState() {
1174         exitWebVRPresent();
1175         mRequestedWebVr = false;
1176     }
1177 
1178     @CalledByNative
exitWebVRPresent()1179     /* package */ void exitWebVRPresent() {
1180         if (!mInVr) return;
1181 
1182         // If we have previously used the VRBrowser this session, go back to it.
1183         // If not, and we're on Daydream go back to Daydream home.
1184         // Otherwise, exit VR.
1185         if (mVrBrowserUsed) {
1186             mVrShell.setWebVrModeEnabled(false);
1187         } else if (isDaydreamCurrentViewerInternal()) {
1188             getVrDaydreamApi().launchVrHomescreen();
1189         } else {
1190             shutdownVr(true /* disableVrMode */, true /* stayingInChrome */);
1191         }
1192     }
1193 
cancelStartupAnimationIfNeeded()1194     private boolean cancelStartupAnimationIfNeeded() {
1195         if (!mNeedsAnimationCancel) return false;
1196         if (VrDelegate.DEBUG_LOGS) Log.e(TAG, "canceling startup animation");
1197         mCancellingEntryAnimation = true;
1198         Bundle options = ActivityOptions.makeCustomAnimation(mActivity, 0, 0).toBundle();
1199         Intent intent = VrModuleProvider.getIntentDelegate().setupVrIntent(
1200                 new Intent(mActivity, VrCancelAnimationActivity.class));
1201         // We don't want this to run in a new task stack, or we may end up resuming the wrong
1202         // Activity when the VrCancelAnimationActivity finishes.
1203         intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
1204         mActivity.startActivity(intent, options);
1205         mNeedsAnimationCancel = false;
1206         return true;
1207     }
1208 
1209     @VisibleForTesting
onResume()1210     protected void onResume() {
1211         if (VrDelegate.DEBUG_LOGS) Log.i(TAG, "onResume");
1212         if (mNeedsAnimationCancel) {
1213             // At least on some devices, like the Samsung S8+, a Window animation is run after our
1214             // Activity is shown that fades between a stale screenshot from before pausing to the
1215             // currently rendered content. It's impossible to cancel window animations, and in order
1216             // to modify the animation we would need to set up the desired animations before
1217             // calling setContentView, which we can't do because it would affect non-VR usage.
1218             // To work around this, we keep the stay_hidden animation active until the window
1219             // animation of the stale screenshot finishes and our black overlay is shown. We then
1220             // cancel the stay_hidden animation, revealing our black overlay, which we then replace
1221             // with VR UI.
1222             if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return;
1223             // Just in case any platforms/users modify the window animation scale, we'll multiply
1224             // our wait time by that scale value.
1225             float scale = Settings.Global.getFloat(
1226                     mActivity.getContentResolver(), Settings.Global.WINDOW_ANIMATION_SCALE, 1.0f);
1227             new Handler().postDelayed(new Runnable() {
1228                 @Override
1229                 public void run() {
1230                     cancelStartupAnimationIfNeeded();
1231                 }
1232             }, (long) (WINDOW_FADE_ANIMATION_DURATION_MS * scale));
1233             return;
1234         }
1235 
1236         mPaused = false;
1237         mCancellingEntryAnimation = false;
1238 
1239         // We call resume here to be symmetric with onPause in case we get paused/resumed without
1240         // being hidden/shown. However, we still don't want to resume if we're not visible to avoid
1241         // doing VR rendering that won't be seen.
1242         if (mInVr && mVisible) mVrShell.resume();
1243 
1244         // Shouldn't handle VR Intents pre-Daydream.
1245         assert (VrCoreInstallUtils.getVrSupportLevel() == VrSupportLevel.VR_DAYDREAM
1246                 || !mStartedFromVrIntent);
1247 
1248         if (mNativeVrShellDelegate != 0) {
1249             VrShellDelegateJni.get().onResume(mNativeVrShellDelegate, VrShellDelegate.this);
1250         }
1251 
1252         // Perform slow initialization asynchronously.
1253         new Handler().post(new Runnable() {
1254             @Override
1255             public void run() {
1256                 if (!sRegisteredVrAssetsComponent) {
1257                     registerVrAssetsComponentIfDaydreamUser(isDaydreamCurrentViewerInternal());
1258                 }
1259             }
1260         });
1261 
1262         if (mDonSucceeded) {
1263             handleDonFlowSuccess();
1264         } else {
1265             if (mProbablyInDon && !mTestWorkaroundDontCancelVrEntryOnResume) {
1266                 // This means the user backed out of the DON flow, and we won't be entering VR.
1267                 maybeSetPresentResult(false);
1268 
1269                 shutdownVr(true, false);
1270             }
1271             // If we were resumed at the wrong density, we need to trigger activity recreation.
1272             if (!mInVr && mExpectedDensityChange != 0
1273                     && (mActivity.getResources().getConfiguration().densityDpi
1274                             != mExpectedDensityChange)) {
1275                 mActivity.recreate();
1276             }
1277         }
1278 
1279         mProbablyInDon = false;
1280         mShowVrServicesUpdatePrompt = null;
1281 
1282         runPendingExitVrTask();
1283     }
1284 
handleDonFlowSuccess()1285     private void handleDonFlowSuccess() {
1286         setWindowModeForVr();
1287         if (mInVr) {
1288             maybeSetPresentResult(true);
1289             mDonSucceeded = false;
1290             return;
1291         }
1292         // If we fail to enter VR when we should have entered VR, return to the home screen.
1293         if (!enterVrAfterDon()) {
1294             cancelPendingVrEntry();
1295             getVrDaydreamApi().launchVrHomescreen();
1296         }
1297     }
1298 
1299     // Android lifecycle doesn't guarantee that this will be called after onResume (though it
1300     // will usually be), so make sure anything we do here can happen before or after
1301     // onResume.
onActivityShown()1302     private void onActivityShown() {
1303         mVisible = true;
1304 
1305         // Only resume VrShell once we're visible so that we don't start rendering before being
1306         // visible and delaying startup.
1307         if (mInVr && !mPaused) mVrShell.resume();
1308     }
1309 
onActivityHidden()1310     private void onActivityHidden() {
1311         mVisible = false;
1312         // In case we're hidden before onPause is called, we pause here. Duplicate calls to pause
1313         // are safe.
1314         if (mInVr) mVrShell.pause();
1315     }
1316 
onPause()1317     private void onPause() {
1318         if (VrDelegate.DEBUG_LOGS) Log.i(TAG, "onPause");
1319         mPaused = true;
1320         if (mCancellingEntryAnimation) return;
1321         if (VrCoreInstallUtils.getVrSupportLevel() <= VrSupportLevel.VR_NEEDS_UPDATE) return;
1322 
1323         if (mInVr) mVrShell.pause();
1324         if (mNativeVrShellDelegate != 0) {
1325             VrShellDelegateJni.get().onPause(mNativeVrShellDelegate, VrShellDelegate.this);
1326         }
1327 
1328         mIsDaydreamCurrentViewer = null;
1329     }
1330 
onStart()1331     private void onStart() {
1332         if (mDonSucceeded) setWindowModeForVr();
1333 
1334         // This handles the case where Chrome was paused in VR (ie the user navigated to DD home or
1335         // something), then exited VR and resumed Chrome in 2D. Chrome is still showing VR UI but
1336         // the user is no longer in a VR session.
1337         if (mInVr && !isInVrSession()) {
1338             shutdownVr(true, false);
1339         }
1340 
1341         // Note that we do not turn VR mode on here for two reasons.
1342         // 1. If we're in VR, it should already be on and won't get turned off until we explicitly
1343         // turn it off for this Activity.
1344         // 2. Turning VR mode on breaks popup showing code, which relies on VR mode sometimes being
1345         // off while in VR.
1346     }
1347 
onStop()1348     private void onStop() {
1349         if (VrDelegate.DEBUG_LOGS) Log.i(TAG, "onStop");
1350         assert !mCancellingEntryAnimation;
1351     }
1352 
onBackPressedInternal()1353     private boolean onBackPressedInternal() {
1354         if (VrCoreInstallUtils.getVrSupportLevel() <= VrSupportLevel.VR_NEEDS_UPDATE) return false;
1355         cancelPendingVrEntry();
1356         if (!mInVr) return false;
1357         // Back button should be handled the same way as the close button.
1358         getVrCloseButtonListener().run();
1359         return true;
1360     }
1361 
1362     /**
1363      * @return Whether the user is currently seeing the DOFF screen.
1364      */
showDoff(boolean optional)1365     /* package */ boolean showDoff(boolean optional) {
1366         assert !mShowingDaydreamDoff;
1367         if (!isDaydreamCurrentViewerInternal()) return false;
1368 
1369         if (supports2dInVr()) {
1370             setVrModeEnabled(mActivity, false);
1371             callOnExitVrRequestListener(true);
1372             return true;
1373         }
1374 
1375         try {
1376             if (getVrDaydreamApi().exitFromVr(mActivity, EXIT_VR_RESULT, new Intent())) {
1377                 mShowingDaydreamDoff = true;
1378                 mDoffOptional = optional;
1379                 return true;
1380             }
1381         } catch (IllegalArgumentException | SecurityException e) {
1382             // DOFF calls can unpredictably throw exceptions if VrCore doesn't think Chrome is
1383             // the active component, for example.
1384         }
1385         if (!optional) getVrDaydreamApi().launchVrHomescreen();
1386         return false;
1387     }
1388 
onExitVrResult(boolean success)1389     private void onExitVrResult(boolean success) {
1390         if (VrDelegate.DEBUG_LOGS) Log.i(TAG, "returned from DOFF, success: " + success);
1391 
1392         // We may have manually handled the exit early by swapping to another Chrome activity that
1393         // supports VR while in the DOFF activity. If that happens we want to exit early when the
1394         // real DOFF flow calls us back.
1395         if (!mShowingDaydreamDoff) return;
1396 
1397         // If Doff is not optional and user backed out, launch DD home. We can't re-trigger doff
1398         // here because we're not yet the active VR component and Daydream will throw a Security
1399         // Exception.
1400         if (!mDoffOptional && !success) getVrDaydreamApi().launchVrHomescreen();
1401 
1402         mShowingDaydreamDoff = false;
1403 
1404         if (mShowingDoffForGvrUpdate) mShowVrServicesUpdatePrompt = success;
1405 
1406         if (success) shutdownVr(true /* disableVrMode */, true /* stayingInChrome */);
1407 
1408         callOnExitVrRequestListener(success);
1409         mShowingDoffForGvrUpdate = false;
1410     }
1411 
1412     // Caches whether the current viewer is Daydream for performance.
isDaydreamCurrentViewerInternal()1413     private boolean isDaydreamCurrentViewerInternal() {
1414         if (mIsDaydreamCurrentViewer == null) {
1415             mIsDaydreamCurrentViewer = getVrDaydreamApi().isDaydreamCurrentViewer();
1416         }
1417         return mIsDaydreamCurrentViewer;
1418     }
1419 
cancelPendingVrEntry()1420     private void cancelPendingVrEntry() {
1421         if (VrDelegate.DEBUG_LOGS) Log.i(TAG, "cancelPendingVrEntry");
1422         VrModuleProvider.getDelegate().removeBlackOverlayView(mActivity, false /* animate */);
1423         mDonSucceeded = false;
1424         maybeSetPresentResult(false);
1425         if (!mShowingDaydreamDoff) {
1426             setVrModeEnabled(mActivity, false);
1427             restoreWindowMode();
1428         }
1429     }
1430 
1431     /**
1432      * Exits VR Shell, performing all necessary cleanup.
1433      */
shutdownVr(boolean disableVrMode, boolean stayingInChrome)1434     private void shutdownVr(boolean disableVrMode, boolean stayingInChrome) {
1435         if (VrDelegate.DEBUG_LOGS) Log.i(TAG, "shuttdown VR");
1436         cancelPendingVrEntry();
1437 
1438         if (!mInVr) return;
1439         if (mShowingDaydreamDoff) {
1440             onExitVrResult(true);
1441             return;
1442         }
1443         mInVr = false;
1444 
1445         // Some Samsung devices change the screen density after exiting VR mode which causes
1446         // us to restart Chrome with the VR intent that originally started it. We don't want to
1447         // enable VR mode again, so we remove VR specific extras.
1448         VrModuleProvider.getIntentDelegate().removeVrExtras(mActivity.getIntent());
1449 
1450         // The user has exited VR.
1451         RecordUserAction.record("VR.DOFF");
1452 
1453         if (disableVrMode) setVrModeEnabled(mActivity, false);
1454 
1455         // We get crashes on Android K related to surfaces if we manipulate the view hierarchy while
1456         // finishing.
1457         if (mActivity.isFinishing()) {
1458             if (mVrShell != null) mVrShell.destroyWindowAndroid();
1459             return;
1460         }
1461 
1462         restoreWindowMode();
1463         mVrShell.pause();
1464         removeVrViews();
1465         destroyVrShell();
1466 
1467         promptForFeedbackIfNeeded(stayingInChrome);
1468 
1469         // User exited VR (via something like the system back button) while looking at the exit VR
1470         // prompt.
1471         if (mShowingExitVrPrompt) callOnExitVrRequestListener(true);
1472 
1473         VrModuleProvider.onExitVr();
1474     }
1475 
callOnExitVrRequestListener(boolean success)1476     private void callOnExitVrRequestListener(boolean success) {
1477         if (mOnExitVrRequestListener != null) {
1478             if (success) {
1479                 mOnExitVrRequestListener.onSucceeded();
1480             } else {
1481                 mOnExitVrRequestListener.onDenied();
1482             }
1483         }
1484         mOnExitVrRequestListener = null;
1485     }
1486 
onExitVrRequestResult(boolean shouldExit)1487     /* package */ void onExitVrRequestResult(boolean shouldExit) {
1488         assert mOnExitVrRequestListener != null;
1489         mShowingExitVrPrompt = false;
1490         if (shouldExit) {
1491             mExitedDueToUnsupportedMode = true;
1492             if (!showDoff(true /* optional */)) callOnExitVrRequestListener(false);
1493         } else {
1494             callOnExitVrRequestListener(false);
1495         }
1496     }
1497 
1498     /**
1499      * Returns the callback for the user-triggered close button to exit VR mode.
1500      */
getVrCloseButtonListener()1501     /* package */ Runnable getVrCloseButtonListener() {
1502         if (mCloseButtonListener != null) return mCloseButtonListener;
1503         mCloseButtonListener = new Runnable() {
1504             @Override
1505             public void run() {
1506                 shutdownVr(true /* disableVrMode */, true /* stayingInChrome */);
1507             }
1508         };
1509         return mCloseButtonListener;
1510     }
1511 
1512     /**
1513      * Returns the callback for the user-triggered close button to exit VR mode.
1514      */
getVrSettingsButtonListener()1515     /* package */ Runnable getVrSettingsButtonListener() {
1516         if (mSettingsButtonListener != null) return mSettingsButtonListener;
1517         mSettingsButtonListener = new Runnable() {
1518             @Override
1519             public void run() {
1520                 shutdownVr(true /* disableVrMode */, false /* stayingInChrome */);
1521 
1522                 // Launch Daydream settings.
1523                 GvrUiLayout.launchOrInstallGvrApp(mActivity);
1524             }
1525         };
1526         return mSettingsButtonListener;
1527     }
1528 
1529     /**
1530      * Prompts the user to enter feedback for their VR Browsing experience.
1531      */
promptForFeedbackIfNeeded(boolean stayingInChrome)1532     private void promptForFeedbackIfNeeded(boolean stayingInChrome) {
1533         // We only prompt for feedback if:
1534         // 1) The user hasn't explicitly opted-out of it in the past
1535         // 2) The user has performed VR browsing
1536         // 3) The user is exiting VR and going back into 2D Chrome
1537         // 4) We're not exiting to complete an unsupported VR action in 2D (e.g. viewing PageInfo)
1538         // 5) Every n'th visit (where n = mFeedbackFrequency)
1539 
1540         if (!activitySupportsExitFeedback(mActivity)) return;
1541         if (!stayingInChrome) return;
1542         if (VrFeedbackStatus.getFeedbackOptOut()) return;
1543         if (!mVrBrowserUsed) return;
1544         if (mExitedDueToUnsupportedMode) return;
1545 
1546         int exitCount = VrFeedbackStatus.getUserExitedAndEntered2DCount();
1547         VrFeedbackStatus.setUserExitedAndEntered2DCount((exitCount + 1) % mFeedbackFrequency);
1548 
1549         if (exitCount > 0) return;
1550 
1551         promptForFeedback(mActivity.getActivityTab());
1552     }
1553 
promptForKeyboardUpdate()1554     /* package */ void promptForKeyboardUpdate() {
1555         mCachedGvrKeyboardPackageVersion = getGvrKeyboardPackageVersion();
1556         mActivity.startActivityForResult(
1557                 new Intent(Intent.ACTION_VIEW, Uri.parse(GVR_KEYBOARD_MARKET_URI)),
1558                 GVR_KEYBOARD_UPDATE_RESULT);
1559     }
1560 
1561     @VisibleForTesting
canLaunch2DIntentsInternal()1562     protected boolean canLaunch2DIntentsInternal() {
1563         return supports2dInVr() && !sVrModeEnabledActivitys.contains(sInstance.mActivity);
1564     }
1565 
1566     @VisibleForTesting
createVrShell()1567     protected boolean createVrShell() {
1568         assert mVrShell == null;
1569         if (mActivity.getCompositorViewHolder() == null) return false;
1570         TabModelSelector tabModelSelector = mActivity.getTabModelSelector();
1571         if (tabModelSelector == null) return false;
1572         try {
1573             mVrShell = new VrShell(mActivity, this, tabModelSelector);
1574         } catch (VrUnsupportedException e) {
1575             return false;
1576         } finally {
1577         }
1578         return true;
1579     }
1580 
addVrViews()1581     private void addVrViews() {
1582         FrameLayout decor = (FrameLayout) mActivity.getWindow().getDecorView();
1583         LayoutParams params = new FrameLayout.LayoutParams(
1584                 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
1585         decor.addView(mVrShell.getContainer(), params);
1586         // If the overlay exists, make sure to hide the GvrLayout behind it.
1587         View overlay = mActivity.getWindow().findViewById(R.id.vr_overlay_view);
1588         if (overlay != null) overlay.bringToFront();
1589         mActivity.onEnterVr();
1590     }
1591 
1592     @VisibleForTesting
isBlackOverlayVisible()1593     protected boolean isBlackOverlayVisible() {
1594         View overlay = mActivity.getWindow().findViewById(R.id.vr_overlay_view);
1595         return overlay != null;
1596     }
1597 
removeVrViews()1598     private void removeVrViews() {
1599         mActivity.onExitVr();
1600         FrameLayout decor = (FrameLayout) mActivity.getWindow().getDecorView();
1601         decor.removeView(mVrShell.getContainer());
1602     }
1603 
1604     /**
1605      * Clean up VrShell, and associated native objects.
1606      */
destroyVrShell()1607     private void destroyVrShell() {
1608         if (mVrShell != null) {
1609             mVrShell.getContainer().setOnSystemUiVisibilityChangeListener(null);
1610             mVrShell.teardown();
1611             mVrShell = null;
1612         }
1613     }
1614 
1615     /**
1616      * @param api The VrDaydreamApi object this delegate will use instead of the default one
1617      */
1618     @VisibleForTesting
overrideDaydreamApi(VrDaydreamApi api)1619     protected void overrideDaydreamApi(VrDaydreamApi api) {
1620         sVrDaydreamApi = api;
1621     }
1622 
1623     /**
1624      * @return The VrShell for the VrShellDelegate instance
1625      */
1626     @VisibleForTesting
getVrShell()1627     protected VrShell getVrShell() {
1628         return mVrShell;
1629     }
1630 
1631     /**
1632      * @param frequency Sets how often to show the feedback prompt.
1633      */
1634     @VisibleForTesting
setFeedbackFrequency(int frequency)1635     protected void setFeedbackFrequency(int frequency) {
1636         mFeedbackFrequency = frequency;
1637     }
1638 
1639     @VisibleForTesting
isVrEntryComplete()1640     protected boolean isVrEntryComplete() {
1641         return mInVr && !mProbablyInDon && getVrShell().hasUiFinishedLoading();
1642     }
1643 
1644     @VisibleForTesting
isShowingDoff()1645     protected boolean isShowingDoff() {
1646         return mShowingDaydreamDoff;
1647     }
1648 
1649     @VisibleForTesting
onBroadcastReceived()1650     protected void onBroadcastReceived() {}
1651 
1652     @VisibleForTesting
setExpectingIntent(boolean expectingIntent)1653     protected void setExpectingIntent(boolean expectingIntent) {}
1654 
1655     /**
1656      * @return Pointer to the native VrShellDelegate object.
1657      */
1658     @CalledByNative
getNativePointer()1659     private long getNativePointer() {
1660         return mNativeVrShellDelegate;
1661     }
1662 
destroy()1663     private void destroy() {
1664         if (sInstance == null) return;
1665         shutdownVr(false /* disableVrMode */, false /* stayingInChrome */);
1666         if (mNativeVrShellDelegate != 0) {
1667             VrShellDelegateJni.get().destroy(mNativeVrShellDelegate, VrShellDelegate.this);
1668         }
1669         mNativeVrShellDelegate = 0;
1670         sInstance = null;
1671     }
1672 
1673     @NativeMethods
1674     interface Natives {
init(VrShellDelegate caller)1675         long init(VrShellDelegate caller);
onLibraryAvailable()1676         void onLibraryAvailable();
setPresentResult(long nativeVrShellDelegate, VrShellDelegate caller, boolean result)1677         void setPresentResult(long nativeVrShellDelegate, VrShellDelegate caller, boolean result);
onPause(long nativeVrShellDelegate, VrShellDelegate caller)1678         void onPause(long nativeVrShellDelegate, VrShellDelegate caller);
onResume(long nativeVrShellDelegate, VrShellDelegate caller)1679         void onResume(long nativeVrShellDelegate, VrShellDelegate caller);
destroy(long nativeVrShellDelegate, VrShellDelegate caller)1680         void destroy(long nativeVrShellDelegate, VrShellDelegate caller);
registerVrAssetsComponent()1681         void registerVrAssetsComponent();
1682     }
1683 }
1684