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