chromium/ui/android/java/src/org/chromium/ui/base/OverlayTransformApiHelper.java

// Copyright 2022 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.ui.base;

import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.view.AttachedSurfaceControl;
import android.view.FrameMetrics;
import android.view.SurfaceControl;
import android.view.Window;

import androidx.annotation.RequiresApi;

import org.chromium.ui.gfx.OverlayTransform;

import java.lang.ref.WeakReference;

/** Helper class to avoid fail of ART's class verification for S_V2 APIs in old device. */
@RequiresApi(Build.VERSION_CODES.S_V2)
final class OverlayTransformApiHelper
        implements AttachedSurfaceControl.OnBufferTransformHintChangedListener,
                Window.OnFrameMetricsAvailableListener {
    private final WindowAndroid mWindowAndroid;
    private final WeakReference<Window> mWindow;
    private boolean mBufferTransformListenerAdded;
    private boolean mFrameMetricsListenerAdded;

    static OverlayTransformApiHelper create(WindowAndroid windowAndroid) {
        if (windowAndroid.getWindow() == null) return null;
        return new OverlayTransformApiHelper(windowAndroid);
    }

    private OverlayTransformApiHelper(WindowAndroid windowAndroid) {
        mWindowAndroid = windowAndroid;
        mWindow = new WeakReference<>(mWindowAndroid.getWindow());
        addOnBufferTransformHintChangedListener();
    }

    void destroy() {
        removeOnFrameMetricsAvailableListener();
        removeOnBufferTransformHintChangedListener();
    }

    private void addOnBufferTransformHintChangedListener() {
        Window window = mWindow.get();
        if (window == null) return;
        AttachedSurfaceControl surfacecontrol = window.getRootSurfaceControl();
        if (surfacecontrol == null) {
            // If AttachedSurfaceControl is not available yet, wait until it's ready and set the
            // listener.
            addOnFrameMetricsAvailableListener();
        } else {
            doAddOnBufferTransformHintChangedListener();
        }
    }

    @Override
    public void onBufferTransformHintChanged(int hint) {
        mWindowAndroid.onOverlayTransformUpdated();
    }

    private void doAddOnBufferTransformHintChangedListener() {
        if (mBufferTransformListenerAdded) return;
        Window window = mWindow.get();
        if (window == null) return;
        AttachedSurfaceControl surfacecontrol = window.getRootSurfaceControl();
        if (surfacecontrol != null) {
            surfacecontrol.addOnBufferTransformHintChangedListener(this);
            mBufferTransformListenerAdded = true;
        }
    }

    private void removeOnBufferTransformHintChangedListener() {
        if (!mBufferTransformListenerAdded) return;

        Window window = mWindow.get();
        if (window == null) return;
        AttachedSurfaceControl surfacecontrol = window.getRootSurfaceControl();
        if (surfacecontrol != null) {
            surfacecontrol.removeOnBufferTransformHintChangedListener(this);
            mBufferTransformListenerAdded = false;
        }
    }

    @Override
    public void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, int dropCount) {
        // AttachedSurfaceControl is available after setContentView is called and 1st draw happen.
        AttachedSurfaceControl surfaceControl = window.getRootSurfaceControl();
        if (surfaceControl != null) {
            removeOnFrameMetricsAvailableListener();
            doAddOnBufferTransformHintChangedListener();
        }
    }

    private void addOnFrameMetricsAvailableListener() {
        if (mFrameMetricsListenerAdded) return;
        Window window = mWindow.get();
        if (window == null) return;
        window.addOnFrameMetricsAvailableListener(this, new Handler(Looper.myLooper()));
        mFrameMetricsListenerAdded = true;
    }

    private void removeOnFrameMetricsAvailableListener() {
        if (!mFrameMetricsListenerAdded) return;
        Window window = mWindow.get();
        if (window == null) return;
        window.removeOnFrameMetricsAvailableListener(this);
        mFrameMetricsListenerAdded = false;
    }

    @OverlayTransform
    int getOverlayTransform() {
        Window window = mWindow.get();
        if (window == null) return OverlayTransform.INVALID;
        AttachedSurfaceControl surfacecontrol = window.getRootSurfaceControl();
        if (surfacecontrol == null) {
            return OverlayTransform.INVALID;
        }
        int bufferTransform;
        try {
            bufferTransform = surfacecontrol.getBufferTransformHint();
        } catch (NullPointerException | IllegalStateException e) {
            // Can throw exception from implementation of getBufferTransformHint.
            // See crbug.com/1358898, crbug.com/1468179.
            return OverlayTransform.INVALID;
        }
        return toOverlayTransform(bufferTransform);
    }

    private static @OverlayTransform int toOverlayTransform(int bufferTransform) {
        switch (bufferTransform) {
            case SurfaceControl.BUFFER_TRANSFORM_IDENTITY:
                return OverlayTransform.NONE;
            case SurfaceControl.BUFFER_TRANSFORM_MIRROR_HORIZONTAL:
                return OverlayTransform.FLIP_HORIZONTAL;
            case SurfaceControl.BUFFER_TRANSFORM_MIRROR_VERTICAL:
                return OverlayTransform.FLIP_VERTICAL;
            case SurfaceControl.BUFFER_TRANSFORM_ROTATE_90:
                return OverlayTransform.ROTATE_CLOCKWISE_90;
            case SurfaceControl.BUFFER_TRANSFORM_ROTATE_180:
                return OverlayTransform.ROTATE_CLOCKWISE_180;
            case SurfaceControl.BUFFER_TRANSFORM_ROTATE_270:
                return OverlayTransform.ROTATE_CLOCKWISE_270;
                // Combination cases between BUFFER_TRANSFORM_MIRROR_HORIZONTAL,
                // BUFFER_TRANSFORM_MIRROR_VERTICAL, BUFFER_TRANSFORM_ROTATE_90 are not handled
                // because expected behavior is under-specified by android APIs
            default:
                // INVALID makes WindowAndroid fallback to display rotation
                return OverlayTransform.INVALID;
        }
    }
}