Commit 907652b8 authored by Michael Thiessen's avatar Michael Thiessen Committed by Commit Bot

VR: Hide 2D screenshot when resuming activities in VR and hide 2D UI while loading VR UI.

This CL adds an animation that keeps the resuming Activity hidden for 500ms while 
starting up to allow time for us to draw the black overlay, also added in this CL,
and prevent 2D UI from being shown while entering VR.

This also performs some cleanup around VR entry (now that it's even more async than
before) to ensure that we don't continue trying to enter VR after we try to exit
VR (like when the activity is paused, etc.).

Bug: 728148
Change-Id: I344ad661b632bde74764b134e9d9f70f6258d68d
Reviewed-on: https://chromium-review.googlesource.com/519483Reviewed-by: default avatarDavid Trainor <dtrainor@chromium.org>
Reviewed-by: default avatarYash Malik <ymalik@chromium.org>
Commit-Queue: Michael Thiessen <mthiesse@chromium.org>
Cr-Commit-Position: refs/heads/master@{#476301}
parent f770fd9f
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<!-- The delay here must be long enough for the Activity being resumed to draw
its first frame of UI. This is to avoid showing stale 2D screenshots when
launching Chrome in VR. Note that the larger this value is, the more
latency we add to resuming Chrome after going through the Daydream Device
ON flow. -->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/linear_interpolator"
android:fromAlpha="0" android:toAlpha="0"
android:duration="500" />
...@@ -730,4 +730,10 @@ ...@@ -730,4 +730,10 @@
<item name="android:windowEnterAnimation">@anim/design_bottom_sheet_slide_in</item> <item name="android:windowEnterAnimation">@anim/design_bottom_sheet_slide_in</item>
<item name="android:windowExitAnimation">@null</item> <item name="android:windowExitAnimation">@null</item>
</style> </style>
<!-- VR animations -->
<style name="VrEntryAnimation">
<item name="android:windowEnterAnimation">@anim/stay_hidden</item>
<item name="android:windowExitAnimation">@null</item>
</style>
</resources> </resources>
...@@ -6,6 +6,7 @@ package org.chromium.chrome.browser.vr_shell; ...@@ -6,6 +6,7 @@ package org.chromium.chrome.browser.vr_shell;
import android.app.Activity; import android.app.Activity;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
...@@ -13,9 +14,11 @@ import android.content.Intent; ...@@ -13,9 +14,11 @@ import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Color;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.StrictMode; import android.os.StrictMode;
import android.os.SystemClock; import android.os.SystemClock;
...@@ -127,6 +130,7 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener, ...@@ -127,6 +130,7 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener,
private TabModelSelector mTabModelSelector; private TabModelSelector mTabModelSelector;
private boolean mInVr; private boolean mInVr;
private final Handler mEnterVrHandler;
// Whether or not the VR Device ON flow succeeded. If this is true it means the user has a VR // Whether or not the VR Device ON flow succeeded. If this is true it means the user has a VR
// headset on, but we haven't switched into VR mode yet. // headset on, but we haven't switched into VR mode yet.
...@@ -153,6 +157,8 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener, ...@@ -153,6 +157,8 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener,
// presentation experience. // presentation experience.
private boolean mVrBrowserUsed; private boolean mVrBrowserUsed;
private View mOverlayView;
private static final class VrBroadcastReceiver extends BroadcastReceiver { private static final class VrBroadcastReceiver extends BroadcastReceiver {
private final WeakReference<ChromeActivity> mTargetActivity; private final WeakReference<ChromeActivity> mTargetActivity;
...@@ -164,11 +170,28 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener, ...@@ -164,11 +170,28 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener,
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
ChromeActivity activity = mTargetActivity.get(); ChromeActivity activity = mTargetActivity.get();
if (activity == null) return; if (activity == null) return;
getInstance(activity).mDonSucceeded = true; getInstance(activity);
assert sInstance != null;
if (sInstance == null) return;
sInstance.mDonSucceeded = true;
if (sInstance.mPaused) { if (sInstance.mPaused) {
if (sInstance.mInVrAtChromeLaunch == null) sInstance.mInVrAtChromeLaunch = false; if (sInstance.mInVrAtChromeLaunch == null) sInstance.mInVrAtChromeLaunch = false;
((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)) // We add a black overlay view so that we can show black while the VR UI is loading.
.moveTaskToFront(activity.getTaskId(), 0); // Note that this alone isn't sufficient to prevent 2D UI from showing while
// resuming the Activity, see the comment about the custom animation below.
sInstance.addOverlayView();
// We start the Activity with a custom animation that keeps it hidden for a few
// hundred milliseconds - enough time for us to draw the first black view.
// TODO(mthiesse): This is really hacky. If we can find a way to cancel the
// transition animation (I couldn't), then we can just make it indefinite until the
// VR UI is ready, and then cancel it, rather than trying to guess how long it will
// take to draw the first view, and possibly adding latency to VR startup.
Bundle options =
ActivityOptions.makeCustomAnimation(activity, R.anim.stay_hidden, 0)
.toBundle();
((ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE))
.moveTaskToFront(activity.getTaskId(), 0, options);
} else { } else {
if (sInstance.mInVrAtChromeLaunch == null) sInstance.mInVrAtChromeLaunch = true; if (sInstance.mInVrAtChromeLaunch == null) sInstance.mInVrAtChromeLaunch = true;
// If a WebVR app calls requestPresent in response to the displayactivate event // If a WebVR app calls requestPresent in response to the displayactivate event
...@@ -430,8 +453,8 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener, ...@@ -430,8 +453,8 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener,
updateVrSupportLevel(); updateVrSupportLevel();
mNativeVrShellDelegate = nativeInit(); mNativeVrShellDelegate = nativeInit();
mFeedbackFrequency = VrFeedbackStatus.getFeedbackFrequency(); mFeedbackFrequency = VrFeedbackStatus.getFeedbackFrequency();
Choreographer choreographer = Choreographer.getInstance(); mEnterVrHandler = new Handler();
choreographer.postFrameCallback(new FrameCallback() { Choreographer.getInstance().postFrameCallback(new FrameCallback() {
@Override @Override
public void doFrame(long frameTimeNanos) { public void doFrame(long frameTimeNanos) {
if (mNativeVrShellDelegate == 0) return; if (mNativeVrShellDelegate == 0) return;
...@@ -456,6 +479,9 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener, ...@@ -456,6 +479,9 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener,
// Other activities should only pause while we're paused due to Android lifecycle. // Other activities should only pause while we're paused due to Android lifecycle.
assert mPaused; assert mPaused;
break; break;
case ActivityState.STOPPED:
if (activity == mActivity) cancelPendingVrEntry();
break;
case ActivityState.RESUMED: case ActivityState.RESUMED:
assert !mInVr || mShowingDaydreamDoff; assert !mInVr || mShowingDaydreamDoff;
if (mInVr && activity != mActivity) { if (mInVr && activity != mActivity) {
...@@ -557,12 +583,17 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener, ...@@ -557,12 +583,17 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener,
private void enterVr(final boolean tentativeWebVrMode) { private void enterVr(final boolean tentativeWebVrMode) {
// We can't enter VR before the application resumes, or we encounter bizarre crashes // We can't enter VR before the application resumes, or we encounter bizarre crashes
// related to gpu surfaces. // related to gpu surfaces.
// TODO(mthiesse): Is the above comment still accurate? It may have been tied to our HTML
// UI which is gone.
assert !mPaused; assert !mPaused;
if (mNativeVrShellDelegate == 0) return;
if (mInVr) return; if (mInVr) return;
if (mNativeVrShellDelegate == 0) {
cancelPendingVrEntry();
return;
}
if (!isWindowModeCorrectForVr()) { if (!isWindowModeCorrectForVr()) {
setWindowModeForVr(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); setWindowModeForVr(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
new Handler().post(new Runnable() { mEnterVrHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
enterVr(tentativeWebVrMode); enterVr(tentativeWebVrMode);
...@@ -570,9 +601,27 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener, ...@@ -570,9 +601,27 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener,
}); });
return; return;
} }
// We need to add VR UI asynchronously, or we get flashes of 2D content. Presumably this is
// because adding the VR UI is slow and Android times out and decides to just show
// something.
mEnterVrHandler.post(new Runnable() {
@Override
public void run() {
enterVrWithCorrectWindowMode(tentativeWebVrMode);
}
});
}
private void enterVrWithCorrectWindowMode(final boolean tentativeWebVrMode) {
if (mInVr) return;
if (mNativeVrShellDelegate == 0) {
cancelPendingVrEntry();
return;
}
if (!createVrShell()) { if (!createVrShell()) {
maybeSetPresentResult(false); maybeSetPresentResult(false);
mVrDaydreamApi.launchVrHomescreen(); mVrDaydreamApi.launchVrHomescreen();
cancelPendingVrEntry();
return; return;
} }
mVrClassesWrapper.setVrModeEnabled(mActivity, true); mVrClassesWrapper.setVrModeEnabled(mActivity, true);
...@@ -581,20 +630,21 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener, ...@@ -581,20 +630,21 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener,
mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
addVrViews(); addVrViews();
mVrShell.initializeNative(mActivity.getActivityTab(), mRequestedWebVr || tentativeWebVrMode,
mActivity instanceof CustomTabActivity);
boolean webVrMode = mRequestedWebVr || tentativeWebVrMode; boolean webVrMode = mRequestedWebVr || tentativeWebVrMode;
mVrShell.initializeNative(
mActivity.getActivityTab(), webVrMode, mActivity instanceof CustomTabActivity);
mVrShell.setWebVrModeEnabled(webVrMode); mVrShell.setWebVrModeEnabled(webVrMode);
// We're entering VR, but not in WebVr mode. // We're entering VR, but not in WebVr mode.
mVrBrowserUsed = !webVrMode; mVrBrowserUsed = !webVrMode;
// onResume needs to be called on GvrLayout after initialization to make sure DON flow work // onResume needs to be called on GvrLayout after initialization to make sure DON flow works
// properly. // properly.
mVrShell.resume(); if (!mPaused) mVrShell.resume();
maybeSetPresentResult(true); maybeSetPresentResult(true);
mVrShell.getContainer().setOnSystemUiVisibilityChangeListener(this); mVrShell.getContainer().setOnSystemUiVisibilityChangeListener(this);
removeOverlayView();
} }
private boolean launchInVr() { private boolean launchInVr() {
...@@ -772,6 +822,8 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener, ...@@ -772,6 +822,8 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener,
return; return;
} }
// This handles the case where we're already in VR, and an NFC scan is received that pauses
// and resumes Chrome without going through the DON flow or firing the DON success intent.
if (isDaydreamCurrentViewer() if (isDaydreamCurrentViewer()
&& mLastVrExit + REENTER_VR_TIMEOUT_MS > SystemClock.uptimeMillis()) { && mLastVrExit + REENTER_VR_TIMEOUT_MS > SystemClock.uptimeMillis()) {
mDonSucceeded = true; mDonSucceeded = true;
...@@ -790,6 +842,7 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener, ...@@ -790,6 +842,7 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener,
mDonSucceeded = false; mDonSucceeded = false;
// If we fail to enter VR when we should have entered VR, return to the home screen. // If we fail to enter VR when we should have entered VR, return to the home screen.
if (!enterVrAfterDon()) { if (!enterVrAfterDon()) {
cancelPendingVrEntry();
maybeSetPresentResult(false); maybeSetPresentResult(false);
mVrDaydreamApi.launchVrHomescreen(); mVrDaydreamApi.launchVrHomescreen();
} }
...@@ -800,6 +853,8 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener, ...@@ -800,6 +853,8 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener,
unregisterDaydreamIntent(mVrDaydreamApi); unregisterDaydreamIntent(mVrDaydreamApi);
if (mVrSupportLevel == VR_NOT_AVAILABLE) return; if (mVrSupportLevel == VR_NOT_AVAILABLE) return;
cancelPendingVrEntry();
// When the active web page has a vrdisplayactivate event handler, // When the active web page has a vrdisplayactivate event handler,
// mListeningForWebVrActivate should be set to true, which means a vrdisplayactive event // mListeningForWebVrActivate should be set to true, which means a vrdisplayactive event
// should be fired once DON flow finished. However, DON flow will pause our activity, // should be fired once DON flow finished. However, DON flow will pause our activity,
...@@ -825,6 +880,7 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener, ...@@ -825,6 +880,7 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener,
private boolean onBackPressedInternal() { private boolean onBackPressedInternal() {
if (mVrSupportLevel == VR_NOT_AVAILABLE) return false; if (mVrSupportLevel == VR_NOT_AVAILABLE) return false;
cancelPendingVrEntry();
if (!mInVr) return false; if (!mInVr) return false;
shutdownVr(true /* disableVrMode */, false /* canReenter */, true /* stayingInChrome */); shutdownVr(true /* disableVrMode */, false /* canReenter */, true /* stayingInChrome */);
return true; return true;
...@@ -895,11 +951,19 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener, ...@@ -895,11 +951,19 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener,
} }
} }
private void cancelPendingVrEntry() {
// Ensure we can't asynchronously enter VR after trying to exit it.
mEnterVrHandler.removeCallbacksAndMessages(null);
mDonSucceeded = false;
removeOverlayView();
}
/** /**
* Exits VR Shell, performing all necessary cleanup. * Exits VR Shell, performing all necessary cleanup.
*/ */
/* package */ void shutdownVr( /* package */ void shutdownVr(
boolean disableVrMode, boolean canReenter, boolean stayingInChrome) { boolean disableVrMode, boolean canReenter, boolean stayingInChrome) {
cancelPendingVrEntry();
if (!mInVr) return; if (!mInVr) return;
if (mShowingDaydreamDoff) { if (mShowingDaydreamDoff) {
onExitVrResult(true); onExitVrResult(true);
...@@ -1094,6 +1158,23 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener, ...@@ -1094,6 +1158,23 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener,
mRestoreSystemUiVisibilityFlag = -1; mRestoreSystemUiVisibilityFlag = -1;
} }
private void addOverlayView() {
if (mOverlayView != null) return;
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
FrameLayout decor = (FrameLayout) mActivity.getWindow().getDecorView();
mOverlayView = new View(mActivity);
mOverlayView.setBackgroundColor(Color.BLACK);
decor.addView(mOverlayView, -1, params);
}
private void removeOverlayView() {
if (mOverlayView == null) return;
FrameLayout decor = (FrameLayout) sInstance.mActivity.getWindow().getDecorView();
decor.removeView(mOverlayView);
mOverlayView = null;
}
/** /**
* Clean up VrShell, and associated native objects. * Clean up VrShell, and associated native objects.
*/ */
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment