chromium/content/public/android/java/src/org/chromium/content/browser/ScreenOrientationProviderImpl.java

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser;

import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.util.Pair;
import android.view.Surface;

import androidx.annotation.Nullable;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ActivityStateListener;
import org.chromium.base.Log;
import org.chromium.content_public.browser.ScreenOrientationDelegate;
import org.chromium.content_public.browser.ScreenOrientationProvider;
import org.chromium.content_public.browser.WebContents;
import org.chromium.device.mojom.ScreenOrientationLockType;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.display.DisplayAndroid;

import java.util.Map;
import java.util.WeakHashMap;

/** This is the implementation of the C++ counterpart ScreenOrientationProvider. */
@JNINamespace("content")
public class ScreenOrientationProviderImpl
        implements ActivityStateListener, ScreenOrientationProvider {
    private static class Holder {
        private static ScreenOrientationProviderImpl sInstance =
                new ScreenOrientationProviderImpl();
    }

    private static final String TAG = "ScreenOrientation";

    // More readable constants to be passed to |addPendingRequest|.
    private static final boolean LOCK = true;
    private static final boolean UNLOCK = false;

    private ScreenOrientationDelegate mDelegate;

    /**
     * The keys of the map are the activities for which screen orientation are
     * trying to lock.
     * The values of the map are the most recent default web screen orientation request for each
     * activity.
     */
    private Map<Activity, Byte> mDefaultOrientationOverrides = new WeakHashMap<>();

    /**
     * The keys of the map are the activities for which screen orientation requests are
     * delayed.
     * The values of the map are the most recent screen orientation request for each activity.
     * The map will contain an entry with a null value if screen orientation requests are delayed
     * for an activity but no screen orientation requests have been made for the activity.
     */
    private Map<Activity, Pair<Boolean, Integer>> mDelayedRequests = new WeakHashMap<>();

    private static final class PendingRequest implements WindowEventObserver {
        private final ScreenOrientationProviderImpl mProvider;
        private final WindowEventObserverManager mWindowEventManager;
        private final boolean mLockOrUnlock;
        private final byte mWebScreenOrientation;
        private boolean mObserverRemoved;

        public PendingRequest(
                ScreenOrientationProviderImpl provider,
                WindowEventObserverManager windowEventManager,
                boolean lockOrUnlock,
                byte webScreenOrientation) {
            mProvider = provider;
            mWindowEventManager = windowEventManager;
            mLockOrUnlock = lockOrUnlock;
            mWebScreenOrientation = webScreenOrientation;
            mWindowEventManager.addObserver(this);
        }

        public void cancel() {
            if (mObserverRemoved) return;
            mWindowEventManager.removeObserver(this);
            mObserverRemoved = true;
        }

        @Override
        public void onWindowAndroidChanged(WindowAndroid newWindowAndroid) {
            if (newWindowAndroid == null) return;

            if (mLockOrUnlock) {
                mProvider.lockOrientation(newWindowAndroid, mWebScreenOrientation);
            } else {
                mProvider.unlockOrientation(newWindowAndroid);
            }
            mWindowEventManager.removeObserver(this);
            mObserverRemoved = true;
        }
    }

    private final Map<WebContents, PendingRequest> mPendingRequests = new WeakHashMap<>();

    @CalledByNative
    public static ScreenOrientationProviderImpl getInstance() {
        return Holder.sInstance;
    }

    private static int getOrientationFromWebScreenOrientations(
            byte orientation, @Nullable WindowAndroid window, Context context) {
        switch (orientation) {
            case ScreenOrientationLockType.DEFAULT:
                return ActivityInfo.SCREEN_ORIENTATION_USER;
            case ScreenOrientationLockType.PORTRAIT_PRIMARY:
                return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
            case ScreenOrientationLockType.PORTRAIT_SECONDARY:
                return ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
            case ScreenOrientationLockType.LANDSCAPE_PRIMARY:
                return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
            case ScreenOrientationLockType.LANDSCAPE_SECONDARY:
                return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
            case ScreenOrientationLockType.PORTRAIT:
                return ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT;
            case ScreenOrientationLockType.LANDSCAPE:
                return ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
            case ScreenOrientationLockType.ANY:
                return ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR;
            case ScreenOrientationLockType.NATURAL:
                // If the tab is being reparented, we don't have a display strongly associated with
                // it, so we get the default display.
                DisplayAndroid displayAndroid =
                        (window != null)
                                ? window.getDisplay()
                                : DisplayAndroid.getNonMultiDisplay(context);
                int rotation = displayAndroid.getRotation();
                if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
                    if (displayAndroid.getDisplayHeight() >= displayAndroid.getDisplayWidth()) {
                        return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
                    }
                    return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
                } else {
                    if (displayAndroid.getDisplayHeight() < displayAndroid.getDisplayWidth()) {
                        return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
                    }
                    return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
                }
            default:
                Log.w(TAG, "Trying to lock to unsupported orientation!");
                return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
        }
    }

    @Override
    public void onActivityStateChange(Activity activity, @ActivityState int newState) {
        if (newState == ActivityState.DESTROYED) {
            mDelayedRequests.remove(activity);
        }
    }

    private void addPendingRequest(
            WebContents webContents, boolean lockOrUnlock, byte webScreenOrientation) {
        WindowEventObserverManager windowEventManager =
                WindowEventObserverManager.from(webContents);
        PendingRequest existingRequest = mPendingRequests.get(webContents);
        if (existingRequest != null) existingRequest.cancel();
        mPendingRequests.put(
                webContents,
                new PendingRequest(this, windowEventManager, lockOrUnlock, webScreenOrientation));
    }

    @CalledByNative
    private void lockOrientationForWebContents(WebContents webContents, byte webScreenOrientation) {
        WindowAndroid window = webContents.getTopLevelNativeWindow();
        if (window == null) {
            addPendingRequest(webContents, LOCK, webScreenOrientation);
        } else {
            lockOrientation(window, webScreenOrientation);
        }
    }

    @Override
    public void lockOrientation(@Nullable WindowAndroid window, byte webScreenOrientation) {
        // WindowAndroid may be null if the tab is being reparented.
        if (window == null) return;
        Activity activity = window.getActivity().get();

        // Locking orientation is only supported for web contents that have an associated activity.
        // Note that we can't just use the focused activity, as that would lead to bugs where
        // unlockOrientation unlocks a different activity to the one that was locked.
        if (activity == null) return;

        int orientation =
                getOrientationFromWebScreenOrientations(webScreenOrientation, window, activity);
        if (orientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
            return;
        }

        setMaybeDelayedRequestedOrientation(activity, /* lock= */ true, orientation);
    }

    @CalledByNative
    private void unlockOrientationForWebContents(WebContents webContents) {
        WindowAndroid window = webContents.getTopLevelNativeWindow();
        if (window == null) {
            addPendingRequest(webContents, UNLOCK, (byte) 0);
        } else {
            unlockOrientation(window);
        }
    }

    @Override
    public void unlockOrientation(@Nullable WindowAndroid window) {
        // WindowAndroid may be null if the tab is being reparented.
        if (window == null) return;
        Activity activity = window.getActivity().get();

        // Locking orientation is only supported for web contents that have an associated activity.
        // Note that we can't just use the focused activity, as that would lead to bugs where
        // unlockOrientation unlocks a different activity to the one that was locked.
        if (activity == null) return;
        byte mDefaultWebOrientation = (byte) ScreenOrientationLockType.DEFAULT;
        if (mDefaultOrientationOverrides.containsKey(activity)) {
            mDefaultWebOrientation = mDefaultOrientationOverrides.get(activity);
        }

        int defaultOrientation =
                getOrientationFromWebScreenOrientations(mDefaultWebOrientation, window, activity);

        try {
            if (defaultOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
                ActivityInfo info =
                        activity.getPackageManager()
                                .getActivityInfo(
                                        activity.getComponentName(), PackageManager.GET_META_DATA);
                defaultOrientation = info.screenOrientation;
            }
        } catch (PackageManager.NameNotFoundException e) {
            // Do nothing, defaultOrientation should be SCREEN_ORIENTATION_UNSPECIFIED.
        } finally {
            setMaybeDelayedRequestedOrientation(activity, /* lock= */ false, defaultOrientation);
        }
    }

    @Override
    public void delayOrientationRequests(WindowAndroid window) {
        Activity activity = window.getActivity().get();
        if ((activity == null || areRequestsDelayedForActivity(activity))) {
            return;
        }

        mDelayedRequests.put(activity, null);
        ApplicationStatus.registerStateListenerForActivity(this, activity);
    }

    @Override
    public void runDelayedOrientationRequests(WindowAndroid window) {
        Activity activity = window.getActivity().get();
        if ((activity == null || !areRequestsDelayedForActivity(activity))) {
            return;
        }

        Pair<Boolean, Integer> delayedRequest = mDelayedRequests.remove(activity);
        if (delayedRequest != null) {
            setRequestedOrientationNow(activity, delayedRequest.first, delayedRequest.second);
        }
        if (mDelayedRequests.isEmpty()) {
            ApplicationStatus.unregisterActivityStateListener(this);
        }
    }

    @CalledByNative
    public boolean isOrientationLockEnabled() {
        return mDelegate == null || mDelegate.canLockOrientation();
    }

    @Override
    public void setOrientationDelegate(ScreenOrientationDelegate delegate) {
        mDelegate = delegate;
    }

    @Override
    public void setOverrideDefaultOrientation(WindowAndroid window, byte defaultWebOrientation) {
        if (window == null) return;
        Activity activity = window.getActivity().get();

        if (activity == null) return;

        if (defaultWebOrientation != ScreenOrientationLockType.DEFAULT) {
            mDefaultOrientationOverrides.put(activity, defaultWebOrientation);
        } else {
            mDefaultOrientationOverrides.remove(activity);
        }
    }

    /** Returns whether screen orientation requests are delayed for the passed-in activity. */
    private boolean areRequestsDelayedForActivity(Activity activity) {
        return mDelayedRequests.containsKey(activity);
    }

    /** Sets the requested orientation for the activity delaying the request if needed. */
    private void setMaybeDelayedRequestedOrientation(
            Activity activity, boolean lock, int orientation) {
        if (areRequestsDelayedForActivity(activity)) {
            mDelayedRequests.put(activity, Pair.create(lock, orientation));
        } else {
            setRequestedOrientationNow(activity, lock, orientation);
        }
    }

    /** Sets the requested orientation for the activity. */
    private void setRequestedOrientationNow(Activity activity, boolean lock, int orientation) {
        if (mDelegate != null) {
            if ((lock && !mDelegate.canLockOrientation())
                    || (!lock && !mDelegate.canUnlockOrientation(activity, orientation))) {
                return;
            }
        }

        activity.setRequestedOrientation(orientation);
    }
}