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

Commit 27b5f63f authored by Wilson Wu's avatar Wilson Wu Committed by Ming-Shin Lu
Browse files

Introduce ViewRootRefreshRateController

This controller used to handle request based on the
refresh rate preference for the current view root.

First apply lower display support refresh rate when
typing to improve the battery life and restore to
previous preferred rate after finish typing.

The controller only limit the max preferred refresh
rate so ideally it shouldn't introduce extra power
consumption.

Bug: 281720315
Test: atest FrameworksCoreTests:ViewRootRefreshRateControllerTest
Test: atest CtsInputMethodTestCases
Merged-In: I2fb9c549da19ff01e7cc3fd8bfc1f9c19aa0f0a8
Change-Id: Id40a97e737ee9eda0a9bcd019ce24631e9a339a8
parent 395c0b37
Loading
Loading
Loading
Loading
+126 −0
Original line number Original line Diff line number Diff line
@@ -50,6 +50,8 @@ import static android.view.ViewRootImplProto.VISIBLE_RECT;
import static android.view.ViewRootImplProto.WIDTH;
import static android.view.ViewRootImplProto.WIDTH;
import static android.view.ViewRootImplProto.WINDOW_ATTRIBUTES;
import static android.view.ViewRootImplProto.WINDOW_ATTRIBUTES;
import static android.view.ViewRootImplProto.WIN_FRAME;
import static android.view.ViewRootImplProto.WIN_FRAME;
import static android.view.ViewRootRefreshRateController.RefreshRatePref.LOWER;
import static android.view.ViewRootRefreshRateController.RefreshRatePref.RESTORE;
import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
@@ -96,6 +98,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Nullable;
import android.annotation.Size;
import android.annotation.Size;
import android.annotation.UiContext;
import android.annotation.UiContext;
import android.annotation.UiThread;
import android.app.ActivityManager;
import android.app.ActivityManager;
import android.app.ActivityThread;
import android.app.ActivityThread;
import android.app.ICompatCameraControlCallback;
import android.app.ICompatCameraControlCallback;
@@ -240,6 +243,7 @@ import java.util.OptionalInt;
import java.util.Queue;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Consumer;


/**
/**
@@ -422,6 +426,74 @@ public final class ViewRootImpl implements ViewParent,
                ICompatCameraControlCallback callback);
                ICompatCameraControlCallback callback);
    }
    }


    /**
     * Used to notify if the user is typing or not.
     * @hide
     */
    public interface TypingHintNotifier {
        /**
         * Called when the typing hint is changed. This would be invoked by the
         * {@link android.view.inputmethod.RemoteInputConnectionImpl}
         * to hint if the user is typing when the it is {@link #isActive() active}.
         *
         * This can be only happened on the UI thread. The behavior won't be guaranteed if
         * invoking this on a non-UI thread.
         *
         * @param isTyping {@code true} if the user is typing.
         */
        @UiThread
        void onTypingHintChanged(boolean isTyping);

        /**
         * Indicates whether the notifier is currently in active state or not.
         *
         * @see #deactivate()
         */
        boolean isActive();

        /**
         * Deactivate the notifier when no longer in use. Mostly invoked when finishing the typing.
         */
        void deactivate();
    }

    /**
     * The {@link TypingHintNotifier} implementation used to handle
     * the refresh rate preference when the typing state is changed.
     */
    private static class TypingHintNotifierImpl implements TypingHintNotifier {

        private final AtomicReference<TypingHintNotifier> mActiveNotifier;

        @NonNull
        private final ViewRootRefreshRateController mController;

        TypingHintNotifierImpl(@NonNull AtomicReference<TypingHintNotifier> notifier,
                @NonNull ViewRootRefreshRateController controller) {
            mController = controller;
            mActiveNotifier = notifier;
        }

        @Override
        public void onTypingHintChanged(boolean isTyping) {
            if (!isActive()) {
                // No-op when the listener was deactivated.
                return;
            }
            mController.updateRefreshRatePreference(isTyping ? LOWER : RESTORE);
        }

        @Override
        public boolean isActive() {
            return mActiveNotifier.get() == this;
        }

        @Override
        public void deactivate() {
            mActiveNotifier.compareAndSet(this, null);
        }
    }

    /**
    /**
     * Callback used to notify corresponding activity about camera compat control changes, override
     * Callback used to notify corresponding activity about camera compat control changes, override
     * configuration change and make sure that all resources are set correctly before updating the
     * configuration change and make sure that all resources are set correctly before updating the
@@ -429,6 +501,32 @@ public final class ViewRootImpl implements ViewParent,
     */
     */
    private ActivityConfigCallback mActivityConfigCallback;
    private ActivityConfigCallback mActivityConfigCallback;


    /**
     * The current active {@link TypingHintNotifier} to handle
     * typing hint change operations.
     */
    private final AtomicReference<TypingHintNotifier> mActiveTypingHintNotifier =
            new AtomicReference<>(null);

    /**
     * Create a {@link TypingHintNotifier} if the client support variable
     * refresh rate for typing. The {@link TypingHintNotifier} is created
     * and mapped to a new active input connection each time.
     *
     * @hide
     */
    @Nullable
    public TypingHintNotifier createTypingHintNotifierIfSupported() {
        if (mRefreshRateController == null) {
            return null;
        }
        final TypingHintNotifier newNotifier = new TypingHintNotifierImpl(mActiveTypingHintNotifier,
                mRefreshRateController);
        mActiveTypingHintNotifier.set(newNotifier);

        return newNotifier;
    }

    /**
    /**
     * Used when configuration change first updates the config of corresponding activity.
     * Used when configuration change first updates the config of corresponding activity.
     * In that case we receive a call back from {@link ActivityThread} and this flag is used to
     * In that case we receive a call back from {@link ActivityThread} and this flag is used to
@@ -858,6 +956,8 @@ public final class ViewRootImpl implements ViewParent,
    private final InsetsController mInsetsController;
    private final InsetsController mInsetsController;
    private final ImeFocusController mImeFocusController;
    private final ImeFocusController mImeFocusController;


    private ViewRootRefreshRateController mRefreshRateController;

    private boolean mIsSurfaceOpaque;
    private boolean mIsSurfaceOpaque;


    private final BackgroundBlurDrawable.Aggregator mBlurRegionAggregator =
    private final BackgroundBlurDrawable.Aggregator mBlurRegionAggregator =
@@ -1048,6 +1148,13 @@ public final class ViewRootImpl implements ViewParent,
                mViewConfiguration,
                mViewConfiguration,
                mContext.getSystemService(InputMethodManager.class));
                mContext.getSystemService(InputMethodManager.class));


        // Whether the variable refresh rate for typing is supported.
        boolean useVariableRefreshRateWhenTyping = context.getResources().getBoolean(
                R.bool.config_variableRefreshRateTypingSupported);
        if (useVariableRefreshRateWhenTyping) {
            mRefreshRateController = new ViewRootRefreshRateController(this);
        }

        mViewBoundsSandboxingEnabled = getViewBoundsSandboxingEnabled();
        mViewBoundsSandboxingEnabled = getViewBoundsSandboxingEnabled();
        mIsStylusPointerIconEnabled =
        mIsStylusPointerIconEnabled =
                InputSettings.isStylusPointerIconEnabled(mContext);
                InputSettings.isStylusPointerIconEnabled(mContext);
@@ -2089,6 +2196,10 @@ public final class ViewRootImpl implements ViewParent,
        if (!mIsInTraversal) {
        if (!mIsInTraversal) {
            scheduleTraversals();
            scheduleTraversals();
        }
        }

        if (!mInsetsController.getState().isSourceOrDefaultVisible(ID_IME, Type.ime())) {
            notifyLeaveTypingEvent();
        }
    }
    }


    @Override
    @Override
@@ -6850,6 +6961,17 @@ public final class ViewRootImpl implements ViewParent,
        }
        }
    }
    }


    /**
     * Restores the refresh rate after leaving typing, the leaving typing cases like
     * the IME insets is invisible or the user interacts the screen outside keyboard.
     */
    @UiThread
    private void notifyLeaveTypingEvent() {
        if (mRefreshRateController != null && mActiveTypingHintNotifier.get() != null) {
            mRefreshRateController.updateRefreshRatePreference(RESTORE);
        }
    }

    /**
    /**
     * Delivers post-ime input events to the view hierarchy.
     * Delivers post-ime input events to the view hierarchy.
     */
     */
@@ -7067,6 +7189,10 @@ public final class ViewRootImpl implements ViewParent,
                mLastClickToolType = event.getToolType(event.getActionIndex());
                mLastClickToolType = event.getToolType(event.getActionIndex());
            }
            }


            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
                notifyLeaveTypingEvent();
            }

            mAttachInfo.mUnbufferedDispatchRequested = false;
            mAttachInfo.mUnbufferedDispatchRequested = false;
            mAttachInfo.mHandlingPointerEvent = true;
            mAttachInfo.mHandlingPointerEvent = true;
            // If the event was fully handled by the handwriting initiator, then don't dispatch it
            // If the event was fully handled by the handwriting initiator, then don't dispatch it
+220 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.view;

import static android.os.Trace.TRACE_TAG_VIEW;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.os.Trace;
import android.util.Log;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Controller to request refresh rate preference operations to the {@link ViewRootImpl}.
 *
 * @hide
 */
public class ViewRootRefreshRateController {

    private static final String TAG = "VRRefreshRateController";

    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private static final float TARGET_REFRESH_RATE_UPPER_BOUND = 60f;

    @NonNull
    private final ViewRootImpl mViewRootImpl;

    private final RefreshRateParams mRateParams;

    private final boolean mHasPreferredRefreshRate;

    private int mRefreshRatePref = RefreshRatePref.NONE;

    private boolean mMaxRefreshRateOverride = false;

    @IntDef(value = {
            RefreshRatePref.NONE,
            RefreshRatePref.LOWER,
            RefreshRatePref.RESTORE,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface RefreshRatePref {
        /**
         * Indicates that no refresh rate preference.
         */
        int NONE = 0;

        /**
         * Indicates that apply the lower refresh rate.
         */
        int LOWER = 1;

        /**
         * Indicates that restore to previous refresh rate.
         */
        int RESTORE = 2;
    }

    public ViewRootRefreshRateController(@NonNull ViewRootImpl viewRoot) {
        mViewRootImpl = viewRoot;
        mRateParams = new RefreshRateParams(getLowerSupportedRefreshRate());
        mHasPreferredRefreshRate = hasPreferredRefreshRate();
        if (mHasPreferredRefreshRate && DEBUG) {
            Log.d(TAG, "App has preferred refresh rate. name:" + viewRoot);
        }
    }

    /**
     * Updates the preference to {@link ViewRootRefreshRateController#mRefreshRatePref},
     * and check if it's needed to update the preferred refresh rate on demand. Like if the
     * user is typing, try to apply the {@link RefreshRateParams#mTargetRefreshRate}.
     *
     * @param refreshRatePref to indicate the refresh rate preference
     */
    public void updateRefreshRatePreference(@RefreshRatePref int refreshRatePref) {
        mRefreshRatePref = refreshRatePref;
        doRefreshRateCheck();
    }

    private void doRefreshRateCheck() {
        if (mRefreshRatePref == RefreshRatePref.NONE) {
            return;
        }
        if (mHasPreferredRefreshRate) {
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "mMaxRefreshRateOverride:" + mMaxRefreshRateOverride
                    + ", mRefreshRatePref:" + refreshRatePrefToString(mRefreshRatePref));
        }

        switch (mRefreshRatePref) {
            case RefreshRatePref.LOWER :
                if (!mMaxRefreshRateOverride) {
                    // Save previous preferred rate before update
                    mRateParams.savePreviousRefreshRateParams(mViewRootImpl.mWindowAttributes);
                    updateMaxRefreshRate();
                } else if (mViewRootImpl.mDisplay.getRefreshRate()
                        > mRateParams.mTargetRefreshRate) {
                    // Boosted, try to update again.
                    updateMaxRefreshRate();
                }
                break;
            case RefreshRatePref.RESTORE :
                resetRefreshRate();
                break;
            default :
                throw new RuntimeException("Unexpected value: " + mRefreshRatePref);
        }
    }

    private void updateMaxRefreshRate() {
        Trace.traceBegin(TRACE_TAG_VIEW, "VRRC.updateMaxRefreshRate");
        WindowManager.LayoutParams params = mViewRootImpl.mWindowAttributes;
        params.preferredMaxDisplayRefreshRate = mRateParams.mTargetRefreshRate;
        mViewRootImpl.setLayoutParams(params, false);
        mMaxRefreshRateOverride = true;
        Trace.instant(TRACE_TAG_VIEW, "VRRC update preferredMax="
                + mRateParams.mTargetRefreshRate);
        Trace.traceEnd(TRACE_TAG_VIEW);
        if (DEBUG) {
            Log.d(TAG, "update max refresh rate to: " + params.preferredMaxDisplayRefreshRate);
        }
    }

    private void resetRefreshRate() {
        if (!mMaxRefreshRateOverride) {
            return;
        }
        Trace.traceBegin(TRACE_TAG_VIEW, "VRRC.resetRefreshRate");
        WindowManager.LayoutParams params = mViewRootImpl.mWindowAttributes;
        params.preferredMaxDisplayRefreshRate = mRateParams.mPreviousPreferredMaxRefreshRate;
        mViewRootImpl.setLayoutParams(params, false);
        mMaxRefreshRateOverride = false;
        Trace.instant(TRACE_TAG_VIEW, "VRRC restore previous="
                + mRateParams.mPreviousPreferredMaxRefreshRate);
        Trace.traceEnd(TRACE_TAG_VIEW);
        if (DEBUG) {
            Log.d(TAG, "reset max refresh rate to: " + params.preferredMaxDisplayRefreshRate);
        }
    }

    private boolean hasPreferredRefreshRate() {
        WindowManager.LayoutParams params = mViewRootImpl.mWindowAttributes;
        return params.preferredRefreshRate > 0
                || params.preferredMaxDisplayRefreshRate > 0
                || params.preferredMinDisplayRefreshRate > 0
                || params.preferredDisplayModeId > 0;
    }

    private float getLowerSupportedRefreshRate() {
        final Display display = mViewRootImpl.mDisplay;
        final Display.Mode defaultMode = display.getDefaultMode();
        float targetRefreshRate = defaultMode.getRefreshRate();
        for (Display.Mode mode : display.getSupportedModes()) {
            if (mode.getRefreshRate() < targetRefreshRate) {
                targetRefreshRate = mode.getRefreshRate();
            }
        }
        if (targetRefreshRate < TARGET_REFRESH_RATE_UPPER_BOUND) {
            targetRefreshRate = TARGET_REFRESH_RATE_UPPER_BOUND;
        }
        return targetRefreshRate;
    }

    private static String refreshRatePrefToString(@RefreshRatePref int pref) {
        switch (pref) {
            case RefreshRatePref.NONE:
                return "NONE";
            case RefreshRatePref.LOWER:
                return "LOWER";
            case RefreshRatePref.RESTORE:
                return "RESTORE";
            default:
                return "Unknown pref=" + pref;
        }
    }

    /**
     * A class for recording refresh rate parameters of the target view, including the target
     * refresh rate we want to apply when entering particular states, and the original preferred
     * refresh rate for restoring when leaving the state.
     */
    private static class RefreshRateParams {
        float mTargetRefreshRate;

        float mPreviousPreferredMaxRefreshRate = 0;

        RefreshRateParams(float targetRefreshRate) {
            mTargetRefreshRate = targetRefreshRate;
            if (DEBUG) {
                Log.d(TAG, "The target rate: " + targetRefreshRate);
            }
        }
        void savePreviousRefreshRateParams(WindowManager.LayoutParams param) {
            mPreviousPreferredMaxRefreshRate = param.preferredMaxDisplayRefreshRate;
            if (DEBUG) {
                Log.d(TAG, "Save previous params, preferred: " + param.preferredRefreshRate
                        + ", Max: " + param.preferredMaxDisplayRefreshRate);
            }
        }
    }
}
+30 −0
Original line number Original line Diff line number Diff line
@@ -28,6 +28,7 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.AnyThread;
import android.annotation.AnyThread;
import android.annotation.NonNull;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.graphics.RectF;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.CancellationSignal;
@@ -182,6 +183,8 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub {


    private CancellationSignalBeamer.Receiver mBeamer;
    private CancellationSignalBeamer.Receiver mBeamer;


    private ViewRootImpl.TypingHintNotifier mTypingHintNotifier;

    RemoteInputConnectionImpl(@NonNull Looper looper,
    RemoteInputConnectionImpl(@NonNull Looper looper,
            @NonNull InputConnection inputConnection,
            @NonNull InputConnection inputConnection,
            @NonNull InputMethodManager inputMethodManager, @Nullable View servedView) {
            @NonNull InputMethodManager inputMethodManager, @Nullable View servedView) {
@@ -190,6 +193,12 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub {
        mH = new Handler(mLooper);
        mH = new Handler(mLooper);
        mParentInputMethodManager = inputMethodManager;
        mParentInputMethodManager = inputMethodManager;
        mServedView = new WeakReference<>(servedView);
        mServedView = new WeakReference<>(servedView);
        if (servedView != null) {
            final ViewRootImpl viewRoot = servedView.getViewRootImpl();
            if (viewRoot != null) {
                mTypingHintNotifier = viewRoot.createTypingHintNotifierIfSupported();
            }
        }
    }
    }


    /**
    /**
@@ -364,6 +373,12 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub {
            return;
            return;
        }
        }
        dispatch(() -> {
        dispatch(() -> {
            notifyTypingHint(false /* isTyping */);
            // Deactivate the notifier when finishing typing.
            if (mTypingHintNotifier != null) {
                mTypingHintNotifier.deactivate();
            }

            // Note that we do not need to worry about race condition here, because 1) mFinished is
            // Note that we do not need to worry about race condition here, because 1) mFinished is
            // updated only inside this block, and 2) the code here is running on a Handler hence we
            // updated only inside this block, and 2) the code here is running on a Handler hence we
            // assume multiple closeConnection() tasks will not be handled at the same time.
            // assume multiple closeConnection() tasks will not be handled at the same time.
@@ -628,6 +643,7 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub {
                return;
                return;
            }
            }
            ic.commitText(text, newCursorPosition);
            ic.commitText(text, newCursorPosition);
            notifyTypingHint(true /* isTyping */);
        });
        });
    }
    }


@@ -783,6 +799,7 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub {
                return;
                return;
            }
            }
            ic.setComposingText(text, newCursorPosition);
            ic.setComposingText(text, newCursorPosition);
            notifyTypingHint(true /* isTyping */);
        });
        });
    }
    }


@@ -910,6 +927,7 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub {
                return;
                return;
            }
            }
            ic.deleteSurroundingText(beforeLength, afterLength);
            ic.deleteSurroundingText(beforeLength, afterLength);
            notifyTypingHint(true /* isTyping */);
        });
        });
    }
    }


@@ -1473,4 +1491,16 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub {
    private static boolean useImeTracing() {
    private static boolean useImeTracing() {
        return ImeTracing.getInstance().isEnabled();
        return ImeTracing.getInstance().isEnabled();
    }
    }

    /**
     * Dispatch the typing hint to {@link ViewRootImpl.TypingHintNotifier}.
     * The input connection indicates that the user is typing when {@link #commitText} or
     * {@link #setComposingText)} and the user finish typing when {@link #deactivate()}.
     */
    @UiThread
    private void notifyTypingHint(boolean isTyping) {
        if (mTypingHintNotifier != null) {
            mTypingHintNotifier.onTypingHintChanged(isTyping);
        }
    }
}
}
+3 −0
Original line number Original line Diff line number Diff line
@@ -6550,6 +6550,9 @@
         device. -->
         device. -->
    <bool name="config_enableAppCloningBuildingBlocks">true</bool>
    <bool name="config_enableAppCloningBuildingBlocks">true</bool>


    <!-- Whether the variable refresh rate when typing feature is enabled for the device. -->
    <bool name="config_variableRefreshRateTypingSupported">false</bool>

    <!-- Enables or disables support for repair mode. The feature creates a secure
    <!-- Enables or disables support for repair mode. The feature creates a secure
         environment to protect the user's privacy when the device is being repaired.
         environment to protect the user's privacy when the device is being repaired.
         Off by default, since OEMs may have had a similar feature on their devices. -->
         Off by default, since OEMs may have had a similar feature on their devices. -->
+2 −0
Original line number Original line Diff line number Diff line
@@ -4942,6 +4942,8 @@


  <java-symbol type="bool" name="config_repairModeSupported" />
  <java-symbol type="bool" name="config_repairModeSupported" />


  <java-symbol type="bool" name="config_variableRefreshRateTypingSupported" />

  <java-symbol type="string" name="config_devicePolicyManagementUpdater" />
  <java-symbol type="string" name="config_devicePolicyManagementUpdater" />


  <java-symbol type="string" name="config_deviceSpecificDeviceStatePolicyProvider" />
  <java-symbol type="string" name="config_deviceSpecificDeviceStatePolicyProvider" />
Loading