Loading core/java/android/view/ViewRootImpl.java +126 −0 Original line number Original line Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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; /** /** Loading Loading @@ -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 Loading @@ -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 Loading Loading @@ -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 = Loading Loading @@ -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); Loading Loading @@ -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 Loading Loading @@ -6849,6 +6960,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. */ */ Loading Loading @@ -7066,6 +7188,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 Loading core/java/android/view/ViewRootRefreshRateController.java 0 → 100644 +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); } } } } core/java/android/view/inputmethod/RemoteInputConnectionImpl.java +30 −0 Original line number Original line Diff line number Diff line Loading @@ -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; Loading Loading @@ -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) { Loading @@ -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(); } } } } /** /** Loading Loading @@ -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. Loading Loading @@ -628,6 +643,7 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub { return; return; } } ic.commitText(text, newCursorPosition); ic.commitText(text, newCursorPosition); notifyTypingHint(true /* isTyping */); }); }); } } Loading Loading @@ -783,6 +799,7 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub { return; return; } } ic.setComposingText(text, newCursorPosition); ic.setComposingText(text, newCursorPosition); notifyTypingHint(true /* isTyping */); }); }); } } Loading Loading @@ -910,6 +927,7 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub { return; return; } } ic.deleteSurroundingText(beforeLength, afterLength); ic.deleteSurroundingText(beforeLength, afterLength); notifyTypingHint(true /* isTyping */); }); }); } } Loading Loading @@ -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); } } } } core/res/res/values/config.xml +3 −0 Original line number Original line Diff line number Diff line Loading @@ -6562,6 +6562,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. --> Loading core/res/res/values/symbols.xml +2 −0 Original line number Original line Diff line number Diff line Loading @@ -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 Loading
core/java/android/view/ViewRootImpl.java +126 −0 Original line number Original line Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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; /** /** Loading Loading @@ -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 Loading @@ -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 Loading Loading @@ -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 = Loading Loading @@ -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); Loading Loading @@ -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 Loading Loading @@ -6849,6 +6960,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. */ */ Loading Loading @@ -7066,6 +7188,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 Loading
core/java/android/view/ViewRootRefreshRateController.java 0 → 100644 +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); } } } }
core/java/android/view/inputmethod/RemoteInputConnectionImpl.java +30 −0 Original line number Original line Diff line number Diff line Loading @@ -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; Loading Loading @@ -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) { Loading @@ -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(); } } } } /** /** Loading Loading @@ -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. Loading Loading @@ -628,6 +643,7 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub { return; return; } } ic.commitText(text, newCursorPosition); ic.commitText(text, newCursorPosition); notifyTypingHint(true /* isTyping */); }); }); } } Loading Loading @@ -783,6 +799,7 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub { return; return; } } ic.setComposingText(text, newCursorPosition); ic.setComposingText(text, newCursorPosition); notifyTypingHint(true /* isTyping */); }); }); } } Loading Loading @@ -910,6 +927,7 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub { return; return; } } ic.deleteSurroundingText(beforeLength, afterLength); ic.deleteSurroundingText(beforeLength, afterLength); notifyTypingHint(true /* isTyping */); }); }); } } Loading Loading @@ -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); } } } }
core/res/res/values/config.xml +3 −0 Original line number Original line Diff line number Diff line Loading @@ -6562,6 +6562,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. --> Loading
core/res/res/values/symbols.xml +2 −0 Original line number Original line Diff line number Diff line Loading @@ -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