Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bc52466f authored by Winson Chung's avatar Winson Chung Committed by Android (Google) Code Review
Browse files

Merge "Use system composition sampling listener"

parents 0869d3ec 600c1864
Loading
Loading
Loading
Loading
+109 −153
Original line number Diff line number Diff line
@@ -16,201 +16,157 @@

package com.android.systemui.statusbar.phone;

import static android.view.Display.DEFAULT_DISPLAY;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.view.SurfaceControl;
import android.view.CompositionSamplingListener;
import android.view.View;

import com.android.systemui.R;
import java.io.PrintWriter;

/**
 * Updates the nav bar tint based on the color of the content behind the nav bar.
 */
public class NavBarTintController implements View.OnAttachStateChangeListener,
        View.OnLayoutChangeListener {

public class NavBarTintController {
    public static final int MIN_COLOR_ADAPT_TRANSITION_TIME = 400;
    public static final int DEFAULT_COLOR_ADAPT_TRANSITION_TIME = 1700;

    private final HandlerThread mColorAdaptHandlerThread = new HandlerThread("ColorExtractThread");
    private Handler mColorAdaptionHandler;

    // Poll time for each iteration to color sample
    private static final int COLOR_ADAPTION_TIMEOUT = 300;

    // Passing the threshold of this luminance value will make the button black otherwise white
    private static final float LUMINANCE_THRESHOLD = 0.3f;

    // The margin from the bounds of the view to color sample around
    private static final int COLOR_SAMPLE_MARGIN = 10;

    private boolean mRunning;

    private final Handler mHandler = new Handler();
    private final NavigationBarView mNavigationBarView;
    private final LightBarTransitionsController mLightBarController;
    private final Handler mMainHandler = new Handler(Looper.getMainLooper());
    private final int mBarRadius;
    private final int mBarBottom;

    private final CompositionSamplingListener mSamplingListener;
    private final Runnable mUpdateSamplingListener = this::updateSamplingListener;
    private final Rect mSamplingBounds = new Rect();
    private boolean mSamplingEnabled = false;
    private boolean mSamplingListenerRegistered = false;

    private float mLastMediaLuma;
    private boolean mUpdateOnNextDraw;

    public NavBarTintController(NavigationBarView navigationBarView,
            LightBarTransitionsController lightBarController) {
        mSamplingListener = new CompositionSamplingListener(
                navigationBarView.getContext().getMainExecutor()) {
            @Override
            public void onSampleCollected(float medianLuma) {
                updateTint(medianLuma);
            }
        };
        mNavigationBarView = navigationBarView;
        mNavigationBarView.addOnAttachStateChangeListener(this);
        mNavigationBarView.addOnLayoutChangeListener(this);
        mLightBarController = lightBarController;
    }

        final Resources res = navigationBarView.getResources();
        mBarRadius = res.getDimensionPixelSize(R.dimen.navigation_handle_radius);
        mBarBottom = res.getDimensionPixelSize(R.dimen.navigation_handle_bottom);
    void onDraw() {
        if (mUpdateOnNextDraw) {
            mUpdateOnNextDraw = false;
            requestUpdateSamplingListener();
        }
    }

    public void start() {
    void start() {
        if (!isEnabled(mNavigationBarView.getContext())) {
            return;
        }
        if (mColorAdaptionHandler == null) {
            mColorAdaptHandlerThread.start();
            mColorAdaptionHandler = new Handler(mColorAdaptHandlerThread.getLooper());
        }
        mColorAdaptionHandler.removeCallbacksAndMessages(null);
        mColorAdaptionHandler.post(this::updateTint);
        mRunning = true;
        mSamplingEnabled = true;
        // Defer calling updateSamplingListener since we may have just reinflated prior to this
        requestUpdateSamplingListener();
    }

    public void end() {
        if (mColorAdaptionHandler != null) {
            mColorAdaptionHandler.removeCallbacksAndMessages(null);
        }
        mRunning = false;
    void stop() {
        mSamplingEnabled = false;
        requestUpdateSamplingListener();
    }

    public void stop() {
        end();
        if (mColorAdaptionHandler != null) {
            mColorAdaptHandlerThread.quitSafely();
    @Override
    public void onViewAttachedToWindow(View view) {
        requestUpdateSamplingListener();
    }

    @Override
    public void onViewDetachedFromWindow(View view) {
        // Defer calling updateSamplingListener the attach info has not yet been reset
        requestUpdateSamplingListener();
    }

    private void updateTint() {
        int[] navPos = new int[2];
        int[] butPos = new int[2];
    @Override
    public void onLayoutChange(View v, int left, int top, int right, int bottom,
            int oldLeft, int oldTop, int oldRight, int oldBottom) {
        mSamplingBounds.setEmpty();
        // TODO: Extend this to 2/3 button layout as well
        View view = mNavigationBarView.getHomeHandle().getCurrentView();
        if (view == null) {
            return;
        if (view != null) {
            int[] pos = new int[2];
            view.getLocationOnScreen(pos);
            final Rect samplingBounds = new Rect(pos[0], pos[1],
                    pos[0] + view.getWidth(), pos[1] + view.getHeight());
            if (!samplingBounds.equals(mSamplingBounds)) {
                mSamplingBounds.set(samplingBounds);
                requestUpdateSamplingListener();
            }
        }

        // Determine the area of the icon within its view bounds
        view.getLocationInSurface(butPos);
        final int navWidth = view.getWidth();
        final int navHeight = view.getHeight();
        int viewBottom = butPos[1] + navHeight - mBarBottom;
        final Rect viewIconRect = new Rect(butPos[0], viewBottom - mBarRadius * 2,
                butPos[0] + navWidth, viewBottom);

        if (mNavigationBarView.getCurrentView() == null || viewIconRect.isEmpty()) {
            scheduleColorAdaption();
            return;
    }
        mNavigationBarView.getCurrentView().getLocationOnScreen(navPos);
        viewIconRect.offset(navPos[0], navPos[1]);

        // Apply a margin area around the button region to sample the colors, crop from screenshot
        final Rect cropRect = new Rect(viewIconRect);
        cropRect.inset(-COLOR_SAMPLE_MARGIN, -COLOR_SAMPLE_MARGIN);
        if (cropRect.isEmpty()) {
            scheduleColorAdaption();
            return;
    private void requestUpdateSamplingListener() {
        mHandler.removeCallbacks(mUpdateSamplingListener);
        mHandler.post(mUpdateSamplingListener);
    }

        // Determine the size of the home area
        Rect homeArea = new Rect(COLOR_SAMPLE_MARGIN, COLOR_SAMPLE_MARGIN,
                viewIconRect.width() + COLOR_SAMPLE_MARGIN,
                viewIconRect.height() + COLOR_SAMPLE_MARGIN);

        // Get the screenshot around the home button icon to determine the color
        DisplayMetrics mDisplayMetrics = new DisplayMetrics();
        mNavigationBarView.getContext().getDisplay().getRealMetrics(mDisplayMetrics);
        final Bitmap hardBitmap = SurfaceControl
                .screenshot(new Rect(), mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels,
                        mNavigationBarView.getContext().getDisplay().getRotation());
        if (hardBitmap != null && cropRect.bottom <= hardBitmap.getHeight()
                && cropRect.left + cropRect.width() <= hardBitmap.getWidth()) {
            final Bitmap cropBitmap = Bitmap.createBitmap(hardBitmap, cropRect.left, cropRect.top,
                    cropRect.width(), cropRect.height());
            final Bitmap softBitmap = cropBitmap.copy(Config.ARGB_8888, false);

            // Get the luminance value to determine if the home button should be black or white
            final int[] pixels = new int[softBitmap.getByteCount() / 4];
            softBitmap.getPixels(pixels, 0, softBitmap.getWidth(), 0, 0, softBitmap.getWidth(),
                    softBitmap.getHeight());
            float r = 0, g = 0, blue = 0;

            int width = cropRect.width();
            int total = 0;
            for (int i = 0; i < pixels.length; i += 4) {
                int x = i % width;
                int y = i / width;
                if (!homeArea.contains(x, y)) {
                    r += Color.red(pixels[i]);
                    g += Color.green(pixels[i]);
                    blue += Color.blue(pixels[i]);
                    total++;
                }
            }

            r /= total;
            g /= total;
            blue /= total;

            r = Math.max(Math.min(r / 255f, 1), 0);
            g = Math.max(Math.min(g / 255f, 1), 0);
            blue = Math.max(Math.min(blue / 255f, 1), 0);

            if (r <= 0.03928) {
                r /= 12.92;
            } else {
                r = (float) Math.pow((r + 0.055) / 1.055, 2.4);
    private void updateSamplingListener() {
        if (mSamplingListenerRegistered) {
            mSamplingListenerRegistered = false;
            CompositionSamplingListener.unregister(mSamplingListener);
        }
            if (g <= 0.03928) {
                g /= 12.92;
            } else {
                g = (float) Math.pow((g + 0.055) / 1.055, 2.4);
        if (mSamplingEnabled && !mSamplingBounds.isEmpty()
                && mNavigationBarView.isAttachedToWindow()) {
            if (!mNavigationBarView.getViewRootImpl().getSurfaceControl().isValid()) {
                // The view may still be attached, but the surface backing the window can be
                // destroyed, so wait until the next draw to update the listener again
                mUpdateOnNextDraw = true;
                return;
            }
            mSamplingListenerRegistered = true;
            CompositionSamplingListener.register(mSamplingListener, DEFAULT_DISPLAY,
                    mNavigationBarView.getViewRootImpl().getSurfaceControl().getHandle(),
                    mSamplingBounds);
        }
            if (blue <= 0.03928) {
                blue /= 12.92;
            } else {
                blue = (float) Math.pow((blue + 0.055) / 1.055, 2.4);
    }

            if (r * 0.2126 + g * 0.7152 + blue * 0.0722 > LUMINANCE_THRESHOLD) {
    private void updateTint(float medianLuma) {
        mLastMediaLuma = medianLuma;
        if (medianLuma > LUMINANCE_THRESHOLD) {
            // Black
                mMainHandler.post(
                        () -> mLightBarController
                                .setIconsDark(true /* dark */, true /* animate */));
            mLightBarController.setIconsDark(true /* dark */, true /* animate */);
        } else {
            // White
                mMainHandler.post(
                        () -> mLightBarController
                                .setIconsDark(false /* dark */, true /* animate */));
            }
            cropBitmap.recycle();
            hardBitmap.recycle();
            mLightBarController.setIconsDark(false /* dark */, true /* animate */);
        }
        scheduleColorAdaption();
    }

    private void scheduleColorAdaption() {
        mColorAdaptionHandler.removeCallbacksAndMessages(null);
        if (!mRunning || !isEnabled(mNavigationBarView.getContext())) {
            return;
        }
        mColorAdaptionHandler.postDelayed(this::updateTint, COLOR_ADAPTION_TIMEOUT);
    void dump(PrintWriter pw) {
        pw.println("NavBarTintController:");
        pw.println("  navBar isAttached: " + mNavigationBarView.isAttachedToWindow());
        pw.println("  navBar isScValid: " + (mNavigationBarView.isAttachedToWindow()
                ? mNavigationBarView.getViewRootImpl().getSurfaceControl().isValid()
                : "false"));
        pw.println("  mSamplingListenerRegistered: " + mSamplingListenerRegistered);
        pw.println("  mSamplingBounds: " + mSamplingBounds);
        pw.println("  mLastMediaLuma: " + mLastMediaLuma);
    }

    public static boolean isEnabled(Context context) {
        return Settings.Global.getInt(context.getContentResolver(),
        return context.getDisplayId() == DEFAULT_DISPLAY
                && Settings.Global.getInt(context.getContentResolver(),
                        NavigationPrototypeController.NAV_COLOR_ADAPT_ENABLE_SETTING, 0) == 1
                && Settings.Global.getInt(context.getContentResolver(),
                        NavigationPrototypeController.SHOW_HOME_HANDLE_SETTING, 0) == 1;
+1 −1
Original line number Diff line number Diff line
@@ -991,7 +991,7 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback
                    }
                } else {
                    // Screen off disable it
                    mNavigationBarView.getColorAdaptionController().end();
                    mNavigationBarView.getColorAdaptionController().stop();
                }
            }
            if (Intent.ACTION_USER_SWITCHED.equals(action)) {
+10 −5
Original line number Diff line number Diff line
@@ -333,7 +333,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
            if (enabled) {
                mColorAdaptionController.start();
            } else {
                mColorAdaptionController.end();
                mColorAdaptionController.stop();
            }
        }

@@ -486,6 +486,12 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
        }
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        mColorAdaptionController.onDraw();
    }

    private void updateNavigationGestures() {
        if (mGestureHelper instanceof QuickStepController) {
            final int[] assignedMap = mPrototypeController.getGestureActionMap();
@@ -990,7 +996,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
        if (visible) {
            mColorAdaptionController.start();
        } else {
            mColorAdaptionController.end();
            mColorAdaptionController.stop();
        }
    }

@@ -1244,7 +1250,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
        if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
            mColorAdaptionController.start();
        } else {
            mColorAdaptionController.end();
            mColorAdaptionController.stop();
        }
    }

@@ -1330,7 +1336,6 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
        Dependency.get(PluginManager.class).addPluginListener(this,
                NavGesture.class, false /* Only one */);
        setUpSwipeUpOnboarding(isQuickStepSwipeUpEnabled());
        mColorAdaptionController.start();

        if (mPrototypeController.isEnabled()) {
            WindowManager wm = (WindowManager) getContext()
@@ -1363,7 +1368,6 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
            mGestureHelper.destroy();
        }
        mPrototypeController.unregister();
        mColorAdaptionController.stop();
        setUpSwipeUpOnboarding(false);
        for (int i = 0; i < mButtonDispatchers.size(); ++i) {
            mButtonDispatchers.valueAt(i).onDestroy();
@@ -1454,6 +1458,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
            mGestureHelper.dump(pw);
        }
        mRecentsOnboarding.dump(pw);
        mColorAdaptionController.dump(pw);
    }

    @Override