blob: 0b8f5503733a741015729a2267acddb2e09f4f58 [file] [log] [blame]
// Copyright 2019 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.
package org.chromium.chrome.browser.vr;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.pm.ActivityInfo;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import org.chromium.base.Log;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.content_public.browser.ScreenOrientationDelegate;
import org.chromium.content_public.browser.ScreenOrientationProvider;
import org.chromium.ui.widget.Toast;
/**
* Provides a fullscreen overlay for immersive AR mode.
*/
public class ArImmersiveOverlay
implements SurfaceHolder.Callback2, View.OnTouchListener, ScreenOrientationDelegate {
private static final String TAG = "ArImmersiveOverlay";
private static final boolean DEBUG_LOGS = false;
private ArCoreJavaUtils mArCoreJavaUtils;
private ChromeActivity mActivity;
private boolean mSurfaceReportedReady;
private Integer mRestoreOrientation;
private boolean mCleanupInProgress;
private SurfaceUiWrapper mSurfaceUi;
public void show(@NonNull ChromeActivity activity, @NonNull ArCoreJavaUtils caller) {
if (DEBUG_LOGS) Log.i(TAG, "constructor");
mArCoreJavaUtils = caller;
mActivity = activity;
// Choose a concrete implementation to create a drawable Surface and make it fullscreen.
// It forwards SurfaceHolder callbacks and touch events to this ArImmersiveOverlay object.
mSurfaceUi = new SurfaceUiDialog(this);
}
private interface SurfaceUiWrapper {
public void onSurfaceVisible();
public void destroy();
}
private class SurfaceUiDialog implements SurfaceUiWrapper, DialogInterface.OnCancelListener {
private Toast mNotificationToast;
private Dialog mDialog;
// Android supports multiple variants of fullscreen applications. Use fully-immersive
// "sticky" mode without navigation or status bars, and show a toast with a "pull from top
// and press back button to exit" prompt.
private static final int VISIBILITY_FLAGS_IMMERSIVE = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
public SurfaceUiDialog(ArImmersiveOverlay parent) {
// Create a fullscreen dialog and use its backing Surface for drawing.
mDialog = new Dialog(mActivity, android.R.style.Theme_NoTitleBar_Fullscreen);
mDialog.getWindow().setBackgroundDrawable(null);
mDialog.getWindow().takeSurface(parent);
View view = mDialog.getWindow().getDecorView();
view.setSystemUiVisibility(VISIBILITY_FLAGS_IMMERSIVE);
view.setOnTouchListener(parent);
mDialog.setOnCancelListener(this);
mDialog.getWindow().setLayout(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mDialog.show();
}
@Override // SurfaceUiWrapper
public void onSurfaceVisible() {
if (mNotificationToast == null) {
int resId = R.string.immersive_fullscreen_api_notification;
mNotificationToast = Toast.makeText(mActivity, resId, Toast.LENGTH_LONG);
mNotificationToast.setGravity(Gravity.TOP | Gravity.CENTER, 0, 0);
}
mNotificationToast.show();
}
@Override // SurfaceUiWrapper
public void destroy() {
if (mNotificationToast != null) {
mNotificationToast.cancel();
}
mDialog.dismiss();
}
@Override // DialogInterface.OnCancelListener
public void onCancel(DialogInterface dialog) {
if (DEBUG_LOGS) Log.i(TAG, "onCancel");
cleanupAndExit();
}
}
@Override // View.OnTouchListener
public boolean onTouch(View v, MotionEvent ev) {
// Only forward primary actions, ignore more complex events such as secondary pointer
// touches. Ignore batching since we're only sending one ray pose per frame.
if (ev.getAction() == MotionEvent.ACTION_DOWN || ev.getAction() == MotionEvent.ACTION_MOVE
|| ev.getAction() == MotionEvent.ACTION_UP) {
boolean touching = ev.getAction() != MotionEvent.ACTION_UP;
if (DEBUG_LOGS) Log.i(TAG, "onTouch touching=" + touching);
mArCoreJavaUtils.onDrawingSurfaceTouch(touching, ev.getX(0), ev.getY(0));
}
return true;
}
@Override // ScreenOrientationDelegate
public boolean canUnlockOrientation(Activity activity, int defaultOrientation) {
if (mActivity == activity && mRestoreOrientation != null) {
mRestoreOrientation = defaultOrientation;
return false;
}
return true;
}
@Override // ScreenOrientationDelegate
public boolean canLockOrientation() {
return false;
}
@Override // SurfaceHolder.Callback2
public void surfaceCreated(SurfaceHolder holder) {
if (DEBUG_LOGS) Log.i(TAG, "surfaceCreated");
// Do nothing here, we'll handle setup on the following surfaceChanged.
}
@Override // SurfaceHolder.Callback2
public void surfaceRedrawNeeded(SurfaceHolder holder) {
if (DEBUG_LOGS) Log.i(TAG, "surfaceRedrawNeeded");
}
@Override // SurfaceHolder.Callback2
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// WebXR immersive sessions don't support resize, so use the first reported size.
// We shouldn't get resize events since we're using FLAG_LAYOUT_STABLE and are
// locking screen orientation.
if (mSurfaceReportedReady) {
int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
if (DEBUG_LOGS)
Log.i(TAG,
"surfaceChanged ignoring change to width=" + width + " height=" + height
+ " rotation=" + rotation);
return;
}
// Save current orientation mode, and then lock current orientation.
ScreenOrientationProvider.getInstance().setOrientationDelegate(this);
if (mRestoreOrientation == null) {
mRestoreOrientation = mActivity.getRequestedOrientation();
}
mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
if (DEBUG_LOGS)
Log.i(TAG, "surfaceChanged size=" + width + "x" + height + " rotation=" + rotation);
mArCoreJavaUtils.onDrawingSurfaceReady(holder.getSurface(), rotation, width, height);
mSurfaceReportedReady = true;
// Show a toast with instructions how to exit fullscreen mode now if necessary.
mSurfaceUi.onSurfaceVisible();
}
@Override // SurfaceHolder.Callback2
public void surfaceDestroyed(SurfaceHolder holder) {
if (DEBUG_LOGS) Log.i(TAG, "surfaceDestroyed");
cleanupAndExit();
}
public void cleanupAndExit() {
if (DEBUG_LOGS) Log.i(TAG, "cleanupAndExit");
// Avoid duplicate cleanup if we're exiting via ArCoreJavaUtils's endSession.
// That triggers cleanupAndExit -> remove SurfaceView -> surfaceDestroyed -> cleanupAndExit.
if (mCleanupInProgress) return;
mCleanupInProgress = true;
mSurfaceUi.destroy();
// The JS app may have put an element into fullscreen mode during the immersive session,
// even if this wasn't visible to the user. Ensure that we fully exit out of any active
// fullscreen state on session end to avoid being left in a confusing state.
if (mActivity.getActivityTab() != null) {
mActivity.getActivityTab().exitFullscreenMode();
}
// Restore orientation.
ScreenOrientationProvider.getInstance().setOrientationDelegate(null);
if (mRestoreOrientation != null) mActivity.setRequestedOrientation(mRestoreOrientation);
mRestoreOrientation = null;
// The surface is destroyed when exiting via "back" button, but also in other lifecycle
// situations such as switching apps or toggling the phone's power button. Treat each of
// these as exiting the immersive session. We need to run the destroy callbacks to ensure
// consistent state after non-exiting lifecycle events.
mArCoreJavaUtils.onDrawingSurfaceDestroyed();
}
}