Chromium Code Reviews| Index: chrome/android/java/src/org/chromium/chrome/browser/vr_shell/VrShellDelegate.java |
| diff --git a/chrome/android/java/src/org/chromium/chrome/browser/vr_shell/VrShellDelegate.java b/chrome/android/java/src/org/chromium/chrome/browser/vr_shell/VrShellDelegate.java |
| index af104f87ea807d8484f72366883393b6cf7b2f52..52f6c35950b9cafcc9ba77dbbb9f7934b696577c 100644 |
| --- a/chrome/android/java/src/org/chromium/chrome/browser/vr_shell/VrShellDelegate.java |
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/vr_shell/VrShellDelegate.java |
| @@ -12,6 +12,7 @@ import android.content.Intent; |
| import android.content.pm.ActivityInfo; |
| import android.content.res.Configuration; |
| import android.net.Uri; |
| +import android.os.AsyncTask; |
| import android.os.Handler; |
| import android.os.StrictMode; |
| import android.os.SystemClock; |
| @@ -27,6 +28,7 @@ import android.widget.FrameLayout; |
| import org.chromium.base.ActivityState; |
| import org.chromium.base.ApplicationStatus; |
| +import org.chromium.base.ContextUtils; |
| import org.chromium.base.Log; |
| import org.chromium.base.VisibleForTesting; |
| import org.chromium.base.annotations.CalledByNative; |
| @@ -36,16 +38,23 @@ import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.ChromeActivity; |
| import org.chromium.chrome.browser.ChromeFeatureList; |
| import org.chromium.chrome.browser.ChromeTabbedActivity; |
| +import org.chromium.chrome.browser.customtabs.CustomTabActivity; |
| import org.chromium.chrome.browser.infobar.InfoBarIdentifier; |
| import org.chromium.chrome.browser.infobar.SimpleConfirmInfoBarBuilder; |
| import org.chromium.chrome.browser.multiwindow.MultiWindowUtils; |
| import org.chromium.chrome.browser.tab.Tab; |
| import org.chromium.chrome.browser.tabmodel.TabModelSelector; |
| +import java.io.File; |
| +import java.io.FileOutputStream; |
| +import java.io.IOException; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.reflect.Constructor; |
| import java.lang.reflect.InvocationTargetException; |
| +import java.util.concurrent.ExecutionException; |
| +import java.util.concurrent.TimeUnit; |
| +import java.util.concurrent.TimeoutException; |
| /** |
| * Manages interactions with the VR Shell. |
| @@ -69,6 +78,10 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| public static final int VR_CARDBOARD = 1; |
| public static final int VR_DAYDREAM = 2; // Supports both Cardboard and Daydream viewer. |
| + private static final String BASE_VR_FOLDER = "vr"; |
| + public static final String TID_FILE = "tid"; |
| + public static final String RESULT_SUCCESS_FILE = "success"; |
| + |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({VR_NOT_AVAILABLE, VR_CARDBOARD, VR_DAYDREAM}) |
| public @interface VrSupportLevel {} |
| @@ -79,13 +92,17 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| private static final String CARDBOARD_CATEGORY = "com.google.intent.category.CARDBOARD"; |
| private static final String VR_ACTIVITY_ALIAS = |
| - "org.chromium.chrome.browser.VRChromeTabbedActivity"; |
| + "org.chromium.chrome.browser.vr_shell.VrProxyActivity"; |
| private static final long REENTER_VR_TIMEOUT_MS = 1000; |
| + private static final String VR_CORE_MARKET_URI = |
| + "market://details?id=" + VrCoreVersionChecker.VR_CORE_PACKAGE_ID; |
| + |
| private static VrShellDelegate sInstance; |
| + private static AsyncTask<Void, Void, Void> sRegisterIntentTask; |
| - private final ChromeActivity mActivity; |
| + private ChromeActivity mActivity; |
| @VrSupportLevel |
| private int mVrSupportLevel; |
| @@ -98,6 +115,7 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| private TabModelSelector mTabModelSelector; |
| private boolean mInVr; |
| + private boolean mTriedDONFlow; |
| private boolean mEnteringVr; |
| private boolean mPaused; |
| private int mRestoreSystemUiVisibilityFlag = -1; |
| @@ -108,6 +126,10 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| private boolean mListeningForWebVrActivate; |
| private boolean mListeningForWebVrActivateBeforePause; |
| + public static File getOrCreateVRDirectory() { |
| + return ContextUtils.getApplicationContext().getDir(BASE_VR_FOLDER, Context.MODE_PRIVATE); |
| + } |
| + |
| /** |
| * Called when the native library is first available. |
| */ |
| @@ -154,17 +176,6 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| } |
| /** |
| - * Handles a VR intent, entering VR in the process. |
| - */ |
| - public static void enterVRFromIntent(Intent intent) { |
| - assert isDaydreamVrIntent(intent); |
| - boolean created_delegate = sInstance == null; |
| - VrShellDelegate instance = getInstance(); |
| - if (instance == null) return; |
| - if (!instance.enterVRFromIntent() && created_delegate) instance.destroy(); |
| - } |
| - |
| - /** |
| * Whether or not the intent is a Daydream VR Intent. |
| */ |
| public static boolean isDaydreamVrIntent(Intent intent) { |
| @@ -226,11 +237,13 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| @CalledByNative |
| private static VrShellDelegate getInstance() { |
| - Activity activity = ApplicationStatus.getLastTrackedFocusedActivity(); |
| - if (sInstance != null && activity instanceof ChromeTabbedActivity) return sInstance; |
| + return getInstance(ApplicationStatus.getLastTrackedFocusedActivity()); |
| + } |
| + |
| + private static VrShellDelegate getInstance(Activity activity) { |
| if (!LibraryLoader.isInitialized()) return null; |
| - // Note that we only support ChromeTabbedActivity for now. |
| - if (activity == null || !(activity instanceof ChromeTabbedActivity)) return null; |
| + if (activity == null || !isSupportedActivity(activity)) return null; |
| + if (sInstance != null) return sInstance; |
| VrClassesWrapper wrapper = getVrClassesWrapper(); |
| if (wrapper == null) return null; |
| sInstance = new VrShellDelegate((ChromeActivity) activity, wrapper); |
| @@ -238,6 +251,10 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| return sInstance; |
| } |
| + private static boolean isSupportedActivity(Activity activity) { |
| + return activity instanceof ChromeTabbedActivity || activity instanceof CustomTabActivity; |
| + } |
| + |
| /** |
| * @return A helper class for creating VR-specific classes that may not be available at compile |
| * time. |
| @@ -266,16 +283,44 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| private static PendingIntent getEnterVRPendingIntent( |
| VrDaydreamApi dayreamApi, Activity activity) { |
| - return PendingIntent.getActivity(activity, 0, |
| - dayreamApi.createVrIntent(new ComponentName(activity, VR_ACTIVITY_ALIAS)), |
| - PendingIntent.FLAG_ONE_SHOT); |
| + Intent vrIntent = dayreamApi.createVrIntent(new ComponentName(activity, VR_ACTIVITY_ALIAS)); |
| + return PendingIntent.getActivity(activity, 0, vrIntent, PendingIntent.FLAG_ONE_SHOT); |
| } |
| /** |
| * Registers the Intent to fire after phone inserted into a headset. |
| */ |
| - private static void registerDaydreamIntent(VrDaydreamApi dayreamApi, Activity activity) { |
| - dayreamApi.registerDaydreamIntent(getEnterVRPendingIntent(dayreamApi, activity)); |
| + private static void registerDaydreamIntent( |
| + final VrDaydreamApi dayreamApi, final Activity activity) { |
| + if (sRegisterIntentTask != null) sRegisterIntentTask.cancel(true); |
| + sRegisterIntentTask = new AsyncTask<Void, Void, Void>() { |
| + @Override |
| + protected Void doInBackground(Void... params) { |
| + // TODO(mthiesse, crbug.com/706845): This is a workaround for a bug in Daydream |
| + // (b/33074373) where the intent we send to launchInVr is ignored after the first |
| + // time we call launchInVr, and the old intent is reused. Remove TID_FILE and pass a |
| + // tid in the VR Intent once our GVR deps are rolled to a version with this bug |
| + // fixed. |
| + File tidFile = new File(getOrCreateVRDirectory(), TID_FILE); |
| + try { |
| + FileOutputStream stream = new FileOutputStream(tidFile, false /* append */); |
| + stream.write(Integer.toString(activity.getTaskId()).getBytes()); |
| + stream.close(); |
| + } catch (IOException e) { |
| + Log.e(TAG, "Failed to write file: " + tidFile.getAbsolutePath()); |
| + } |
| + return null; |
| + } |
| + |
| + @Override |
| + protected void onPostExecute(Void params) { |
| + // TODO(mthiesse): See b/33074373. The intent passed here is only used the very |
| + // first time this function is called, and seems to only be used again after device |
| + // restart. |
| + dayreamApi.registerDaydreamIntent(getEnterVRPendingIntent(dayreamApi, activity)); |
|
Ted C
2017/03/31 18:17:18
if it is used multiple times, should it still be O
mthiesse
2017/03/31 20:06:14
I'll take a look at this Monday, but I'm actually
|
| + sRegisterIntentTask = null; |
| + } |
| + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |
| } |
| /** |
| @@ -312,19 +357,28 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| mNativeVrShellDelegate, frameTimeNanos, 1.0d / display.getRefreshRate()); |
| } |
| }); |
| - ApplicationStatus.registerStateListenerForActivity(this, activity); |
| + ApplicationStatus.registerStateListenerForAllActivities(this); |
| } |
| @Override |
| public void onActivityStateChange(Activity activity, int newState) { |
| switch (newState) { |
| case ActivityState.DESTROYED: |
| - destroy(); |
| + if (activity == mActivity) destroy(); |
| break; |
| case ActivityState.PAUSED: |
| - pauseVR(); |
| + if (activity == mActivity) { |
| + if (sRegisterIntentTask != null) { |
| + sRegisterIntentTask.cancel(true); |
| + sRegisterIntentTask = null; |
| + } |
| + pauseVR(); |
| + } |
| break; |
| case ActivityState.RESUMED: |
| + assert !mInVr; |
| + if (!isSupportedActivity(activity)) return; |
| + mActivity = (ChromeActivity) activity; |
| resumeVR(); |
| break; |
| default: |
| @@ -353,41 +407,36 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| } |
| /** |
| - * Handle a VR intent, entering VR in the process unless we're unable to. |
| + * Handle a successful VR DON flow, entering VR in the process unless we're unable to. |
| + * @return False if VR entry failed. |
| */ |
| - private boolean enterVRFromIntent() { |
| - // Vr Intent is only used on Daydream devices. |
| - if (mVrSupportLevel != VR_DAYDREAM) return false; |
| + private boolean enterVRAfterDON() { |
| if (mListeningForWebVrActivateBeforePause && !mRequestedWebVR) { |
| nativeDisplayActivate(mNativeVrShellDelegate); |
| - return false; |
| + return true; |
| + } |
| + if (mInVr) { |
| + setEnterVRResult(true); |
| + return true; |
| } |
| + |
| // Normally, if the active page doesn't have a vrdisplayactivate listener, and WebVR was not |
| - // presenting and VrShell was not enabled, we shouldn't enter VR and Daydream Homescreen |
| - // should show after DON flow. But due to a failure in unregisterDaydreamIntent, we still |
| - // try to enterVR. Here we detect this case and force switch to Daydream Homescreen. |
| + // presenting and VrShell was not enabled, the Daydream Homescreen should show after the DON |
| + // flow. However, due to a failure in unregisterDaydreamIntent, we still try to enterVR, so |
| + // detect this case and fail to enter VR. |
| if (!mListeningForWebVrActivateBeforePause && !mRequestedWebVR |
| && !isVrShellEnabled(mVrSupportLevel)) { |
| - mVrDaydreamApi.launchVrHomescreen(); |
| return false; |
| } |
| - if (mInVr) { |
| - setEnterVRResult(true); |
| - return false; |
| - } |
| if (!canEnterVR(mActivity.getActivityTab())) { |
| setEnterVRResult(false); |
| return false; |
| } |
| - if (mPaused) { |
| - // We can't enter VR before the application resumes, or we encounter bizarre crashes |
| - // related to gpu surfaces. Set this flag to enter VR on the next resume. |
| - prepareToEnterVR(); |
| - mEnteringVr = true; |
| - } else { |
| - enterVR(); |
| - } |
| + // We can't enter VR before the application resumes, or we encounter bizarre crashes |
| + // related to gpu surfaces. Set this flag to enter VR on the next resume. |
| + assert !mPaused; |
| + enterVR(); |
| return true; |
| } |
| @@ -402,6 +451,7 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| private void enterVR() { |
| if (mNativeVrShellDelegate == 0) return; |
| if (mInVr) return; |
| + mEnteringVr = true; |
| if (mRestoreSystemUiVisibilityFlag == -1 |
| || mActivity.getResources().getConfiguration().orientation |
| != Configuration.ORIENTATION_LANDSCAPE) { |
| @@ -513,9 +563,17 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| // due to the lack of support for unexported activities. |
| enterVR(); |
| } else { |
| + if (sRegisterIntentTask != null) { |
| + try { |
| + sRegisterIntentTask.get(500, TimeUnit.MILLISECONDS); |
| + } catch (InterruptedException | ExecutionException | TimeoutException e) { |
| + return ENTER_VR_CANCELLED; |
| + } |
| + } |
| if (!mVrDaydreamApi.launchInVr(getEnterVRPendingIntent(mVrDaydreamApi, mActivity))) { |
| return ENTER_VR_CANCELLED; |
| } |
| + mTriedDONFlow = true; |
| } |
| return ENTER_VR_REQUESTED; |
| } |
| @@ -540,21 +598,8 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| private void resumeVR() { |
| mPaused = false; |
| - if (mVrSupportLevel == VR_NOT_AVAILABLE) return; |
| - if (mVrSupportLevel == VR_DAYDREAM |
| - && (isVrShellEnabled(mVrSupportLevel) || mListeningForWebVrActivateBeforePause)) { |
| - registerDaydreamIntent(mVrDaydreamApi, mActivity); |
| - } |
| - |
| - if (mEnteringVr) { |
| - enterVR(); |
| - } else if (mRequestedWebVR) { |
| - // If this is still set, it means the user backed out of the DON flow, and we won't be |
| - // receiving an intent from daydream. |
| - nativeSetPresentResult(mNativeVrShellDelegate, false); |
| - mRequestedWebVR = false; |
| - } |
| - |
| + // TODO(mthiesse): If we ever support staying in VR while paused, make sure to call resume |
| + // on VrShell. |
| StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); |
| try { |
| nativeOnResume(mNativeVrShellDelegate); |
| @@ -562,19 +607,42 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| StrictMode.setThreadPolicy(oldPolicy); |
| } |
| - if (mInVr) { |
| - setupVrModeWindowFlags(); |
| - oldPolicy = StrictMode.allowThreadDiskWrites(); |
| + if (mVrSupportLevel != VR_DAYDREAM) return; |
| + if (mListeningForWebVrActivateBeforePause |
| + || (isVrShellEnabled(mVrSupportLevel) |
| + && (mActivity instanceof ChromeTabbedActivity))) { |
| + registerDaydreamIntent(mVrDaydreamApi, mActivity); |
| + } |
| + if (!mInVr && !mTriedDONFlow && !mListeningForWebVrActivateBeforePause) return; |
| + |
| + if (mVrDaydreamApi.isDaydreamCurrentViewer() |
| + && mLastVRExit + REENTER_VR_TIMEOUT_MS > SystemClock.uptimeMillis()) { |
| + enterVRInternal(); |
| + } |
| + |
| + if (mTriedDONFlow || mListeningForWebVrActivateBeforePause) { |
| + mTriedDONFlow = false; |
| + boolean shouldEnterVr = false; |
| + // We use a proxy activity to handle VR intents, which resumes this activity based on |
| + // tid, so we won't get a new Intent and have to read a file to check whether the DON |
| + // flow was successful. |
| + oldPolicy = StrictMode.allowThreadDiskReads(); |
| + StrictMode.allowThreadDiskWrites(); |
| try { |
| - mVrShell.resume(); |
| - } catch (IllegalArgumentException e) { |
| - Log.e(TAG, "Unable to resume VrShell", e); |
| + File resultFile = new File(getOrCreateVRDirectory(), RESULT_SUCCESS_FILE); |
| + if (resultFile.exists()) shouldEnterVr = true; |
| + resultFile.delete(); |
| } finally { |
| StrictMode.setThreadPolicy(oldPolicy); |
| } |
| - } else if (mVrSupportLevel == VR_DAYDREAM && mVrDaydreamApi.isDaydreamCurrentViewer() |
| - && mLastVRExit + REENTER_VR_TIMEOUT_MS > SystemClock.uptimeMillis()) { |
| - enterVRInternal(); |
| + // If we fail to enter VR when we should have entered VR, return to the home screen. |
| + if (shouldEnterVr && !enterVRAfterDON()) mVrDaydreamApi.launchVrHomescreen(); |
| + } |
| + |
| + if (mRequestedWebVR && !(mInVr || mEnteringVr)) { |
| + // This means the user backed out of the DON flow, and we won't be entering VR. |
| + nativeSetPresentResult(mNativeVrShellDelegate, false); |
| + mRequestedWebVR = false; |
| } |
| } |
| @@ -612,8 +680,8 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| private void onExitVRResult(boolean success) { |
| assert mVrSupportLevel != VR_NOT_AVAILABLE; |
| // For now, we don't handle re-entering VR when exit fails, so keep trying to exit. |
| - if (!success && sInstance.mVrDaydreamApi.exitFromVr(EXIT_VR_RESULT, new Intent())) return; |
| - sInstance.mVrClassesWrapper.setVrModeEnabled(sInstance.mActivity, false); |
| + if (!success && mVrDaydreamApi.exitFromVr(EXIT_VR_RESULT, new Intent())) return; |
| + mVrClassesWrapper.setVrModeEnabled(mActivity, false); |
| } |
| @CalledByNative |
| @@ -703,19 +771,18 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| return; |
| } |
| - SimpleConfirmInfoBarBuilder.create(tab, |
| - new SimpleConfirmInfoBarBuilder.Listener() { |
| - @Override |
| - public void onInfoBarDismissed() {} |
| + SimpleConfirmInfoBarBuilder.Listener listener = new SimpleConfirmInfoBarBuilder.Listener() { |
| + @Override |
| + public void onInfoBarDismissed() {} |
| - @Override |
| - public boolean onInfoBarButtonClicked(boolean isPrimary) { |
| - activity.startActivity(new Intent(Intent.ACTION_VIEW, |
| - Uri.parse("market://details?id=" |
| - + VrCoreVersionChecker.VR_CORE_PACKAGE_ID))); |
| - return false; |
| - } |
| - }, |
| + @Override |
| + public boolean onInfoBarButtonClicked(boolean isPrimary) { |
| + activity.startActivity( |
| + new Intent(Intent.ACTION_VIEW, Uri.parse(VR_CORE_MARKET_URI))); |
| + return false; |
| + } |
| + }; |
| + SimpleConfirmInfoBarBuilder.create(tab, listener, |
| InfoBarIdentifier.VR_SERVICES_UPGRADE_ANDROID, R.drawable.vr_services, infobarText, |
| buttonText, null, true); |
| } |
| @@ -800,6 +867,7 @@ public class VrShellDelegate implements ApplicationStatus.ActivityStateListener |
| private void destroy() { |
| if (sInstance == null) return; |
| + shutdownVR(true, false); |
| if (mNativeVrShellDelegate != 0) nativeDestroy(mNativeVrShellDelegate); |
| ApplicationStatus.unregisterActivityStateListener(this); |
| sInstance = null; |