From 6f3fb7e872c037dc0b7ea8d0f5de99c259496dfc Mon Sep 17 00:00:00 2001 From: Nick Chameyev Date: Thu, 5 Sep 2024 10:51:53 +0000 Subject: Add hidden APIs to listen for screen timeout policy to PowerManager This is needed for DeviceStateProvider on foldable devices to force using tent mode while there is no timeout that makes the device to go to sleep. This also could be used in the future for analytics reporting to understand how often we have screen wakelock available when switching between displays (b/304491690). This is hidden API to be used by system_server's DeviceStateProvider or SystemUI. The method will be called under a trunk stable flag. Context for the reason of this change: currently on book-style foldable devices we switch to the inner display at 90 degrees instead of 0 degrees in certain cases, such as: - screen is rotate landscape or reverse-landscape - device is physically lying on a flat surface or in reverse-lansdcape orientation We want to try to add wake lock signal too as we have received feedback from many users that they want to prop up the device in cases like watching vertical videos, camera app, google meet/zoom calls. It's not an ideal signal, but it might cover most of these scenarios. Test: atest PowerManagerServiceTest Test: atest NotifierTest Bug: 363174979 Flag: EXEMPT bugfix Change-Id: I42ada86831defe1735f45a05866fec0a828c6501 --- core/java/Android.bp | 1 + .../hardware/display/DisplayManagerInternal.java | 5 + core/java/android/os/IPowerManager.aidl | 5 + .../android/os/IScreenTimeoutPolicyListener.aidl | 29 +++ core/java/android/os/PowerManager.java | 113 ++++++++++++ .../server/display/DisplayManagerService.java | 9 + .../java/com/android/server/power/Notifier.java | 149 +++++++++++++++ .../java/com/android/server/power/PowerGroup.java | 9 + .../android/server/power/PowerManagerService.java | 87 ++++++++- .../src/com/android/server/power/NotifierTest.java | 201 +++++++++++++++++++++ .../server/power/PowerManagerServiceTest.java | 117 +++++++++++- 11 files changed, 722 insertions(+), 3 deletions(-) create mode 100644 core/java/android/os/IScreenTimeoutPolicyListener.aidl diff --git a/core/java/Android.bp b/core/java/Android.bp index ce767f46dd50..1e97d4f12836 100644 --- a/core/java/Android.bp +++ b/core/java/Android.bp @@ -223,6 +223,7 @@ filegroup { "android/os/IThermalService.aidl", "android/os/IPowerManager.aidl", "android/os/IWakeLockCallback.aidl", + "android/os/IScreenTimeoutPolicyListener.aidl", ], } diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index 68b6cfc012fc..d273ddb15cc4 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -448,6 +448,11 @@ public abstract class DisplayManagerInternal { */ public abstract IntArray getDisplayIds(); + /** + * Get group id for given display id + */ + public abstract int getGroupIdForDisplay(int displayId); + /** * Called upon presentation started/ended on the display. * @param displayId the id of the display where presentation started. diff --git a/core/java/android/os/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl index 4cac4dee0bea..17697e67c757 100644 --- a/core/java/android/os/IPowerManager.aidl +++ b/core/java/android/os/IPowerManager.aidl @@ -22,6 +22,7 @@ import android.os.ParcelDuration; import android.os.PowerSaveState; import android.os.WorkSource; import android.os.IWakeLockCallback; +import android.os.IScreenTimeoutPolicyListener; /** @hide */ @@ -45,6 +46,10 @@ interface IPowerManager @UnsupportedAppUsage boolean isWakeLockLevelSupported(int level); boolean isWakeLockLevelSupportedWithDisplayId(int level, int displayId); + oneway void addScreenTimeoutPolicyListener(int displayId, + IScreenTimeoutPolicyListener listener); + oneway void removeScreenTimeoutPolicyListener(int displayId, + IScreenTimeoutPolicyListener listener); void userActivity(int displayId, long time, int event, int flags); void wakeUp(long time, int reason, String details, String opPackageName); diff --git a/core/java/android/os/IScreenTimeoutPolicyListener.aidl b/core/java/android/os/IScreenTimeoutPolicyListener.aidl new file mode 100644 index 000000000000..62811a0cb436 --- /dev/null +++ b/core/java/android/os/IScreenTimeoutPolicyListener.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 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.os; + +/** + * Listener for screen timeout policy changes + * @see PowerManager#addScreenTimeoutPolicyListener(int, IScreenTimeoutPolicyListener) + * @hide + */ +oneway interface IScreenTimeoutPolicyListener { + /** + * @see PowerManager#addScreenTimeoutPolicyListener + */ + oneway void onScreenTimeoutPolicyChanged(int screenTimeoutPolicy); +} diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index cd48f0847f8d..1801df048b3e 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -34,6 +34,7 @@ import android.annotation.TestApi; import android.app.PropertyInvalidatedCache; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; +import android.os.IScreenTimeoutPolicyListener; import android.service.dreams.Sandman; import android.util.ArrayMap; import android.util.ArraySet; @@ -1048,6 +1049,29 @@ public final class PowerManager { int NIGHT_MODE = 16; } + /** + * Screen timeout policy type: the screen turns off after a timeout + * @hide + */ + public static final int SCREEN_TIMEOUT_ACTIVE = 0; + + /** + * Screen timeout policy type: the screen is kept 'on' (no timeout) + * @hide + */ + public static final int SCREEN_TIMEOUT_KEEP_DISPLAY_ON = 1; + + /** + * @hide + */ + @IntDef(prefix = { "SCREEN_TIMEOUT_" }, value = { + SCREEN_TIMEOUT_ACTIVE, + SCREEN_TIMEOUT_KEEP_DISPLAY_ON + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ScreenTimeoutPolicy{} + + /** * Either the location providers shouldn't be affected by battery saver, * or battery saver is off. @@ -1208,6 +1232,9 @@ public final class PowerManager { private final ArrayMap mThermalHeadroomListenerMap = new ArrayMap<>(); + private final ArrayMap + mScreenTimeoutPolicyListeners = new ArrayMap<>(); + /** * {@hide} */ @@ -1749,6 +1776,77 @@ public final class PowerManager { } } + /** + * Adds a listener to be notified about changes in screen timeout policy. + * + *

The screen timeout policy determines the behavior of the device's screen + * after a period of inactivity. It can be used to understand if the display is going + * to be turned off after a timeout to conserve power, or if it will be kept on indefinitely. + * For example, it might be useful for adjusting display switch conditions on foldable + * devices based on the current timeout policy. + * + *

See {@link ScreenTimeoutPolicy} for possible values. + * + *

The listener will be fired with the initial state upon subscribing. + * + *

IScreenTimeoutPolicyListener is called on either system server's main thread or + * on a binder thread if subscribed outside the system service process. + * + * @param displayId display id for which to be notified about screen timeout policy changes + * @param executor executor on which to execute ScreenTimeoutPolicyListener methods + * @param listener listener that will be fired on screem timeout policy updates + * @hide + */ + @RequiresPermission(android.Manifest.permission.DEVICE_POWER) + public void addScreenTimeoutPolicyListener(int displayId, + @NonNull @CallbackExecutor Executor executor, + @NonNull ScreenTimeoutPolicyListener listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + Objects.requireNonNull(executor, "executor cannot be null"); + Preconditions.checkArgument(!mScreenTimeoutPolicyListeners.containsKey(listener), + "Listener already registered: %s", listener); + + final IScreenTimeoutPolicyListener stub = new IScreenTimeoutPolicyListener.Stub() { + public void onScreenTimeoutPolicyChanged(int screenTimeoutPolicy) { + final long token = Binder.clearCallingIdentity(); + try { + executor.execute(() -> + listener.onScreenTimeoutPolicyChanged(screenTimeoutPolicy)); + } finally { + Binder.restoreCallingIdentity(token); + } + } + }; + + try { + mService.addScreenTimeoutPolicyListener(displayId, stub); + mScreenTimeoutPolicyListeners.put(listener, stub); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Removes a listener that is used to listen for screen timeout policy changes. + * @see PowerManager#addScreenTimeoutPolicyListener(int, ScreenTimeoutPolicyListener) + * @param displayId display id for which to be notified about screen timeout changes + * @hide + */ + @RequiresPermission(android.Manifest.permission.DEVICE_POWER) + public void removeScreenTimeoutPolicyListener(int displayId, + @NonNull ScreenTimeoutPolicyListener listener) { + Objects.requireNonNull(listener, "listener cannot be null"); + IScreenTimeoutPolicyListener internalListener = mScreenTimeoutPolicyListeners.get(listener); + Preconditions.checkArgument(internalListener != null, "Listener was not added"); + + try { + mService.removeScreenTimeoutPolicyListener(displayId, internalListener); + mScreenTimeoutPolicyListeners.remove(listener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Returns true if the specified wake lock level is supported. * @@ -3824,6 +3922,21 @@ public final class PowerManager { void onStateChanged(boolean enabled); } + /** + * Listener for screen timeout policy changes + * @see PowerManager#addScreenTimeoutPolicyListener(int, ScreenTimeoutPolicyListener) + * @hide + */ + public interface ScreenTimeoutPolicyListener { + /** + * Invoked on changes in screen timeout policy. + * + * @param screenTimeoutPolicy Screen timeout policy, one of {@link ScreenTimeoutPolicy} + * @see PowerManager#addScreenTimeoutPolicyListener + */ + void onScreenTimeoutPolicyChanged(@ScreenTimeoutPolicy int screenTimeoutPolicy); + } + /** * A wake lock is a mechanism to indicate that your application needs * to have the device stay on. diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 5740e16dc886..29c8a537bc3d 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -5845,6 +5845,15 @@ public final class DisplayManagerService extends SystemService { return displayIds; } + @Override + public int getGroupIdForDisplay(int displayId) { + synchronized (mSyncRoot) { + final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(displayId); + if (display == null) return Display.INVALID_DISPLAY_GROUP; + return display.getDisplayInfoLocked().displayGroupId; + } + } + @Override public DisplayManagerInternal.DisplayOffloadSession registerDisplayOffloader( int displayId, @NonNull DisplayManagerInternal.DisplayOffloader displayOffloader) { diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java index 271c818f7daa..fb8aac648552 100644 --- a/services/core/java/com/android/server/power/Notifier.java +++ b/services/core/java/com/android/server/power/Notifier.java @@ -37,11 +37,14 @@ import android.os.BatteryStatsInternal; import android.os.Bundle; import android.os.Handler; import android.os.IWakeLockCallback; +import android.os.IScreenTimeoutPolicyListener; import android.os.Looper; import android.os.Message; import android.os.PowerManager; +import android.os.PowerManager.ScreenTimeoutPolicy; import android.os.PowerManagerInternal; import android.os.Process; +import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; @@ -52,6 +55,7 @@ import android.os.WorkSource; import android.os.WorkSource.WorkChain; import android.provider.Settings; import android.telephony.TelephonyManager; +import android.util.ArrayMap; import android.util.EventLog; import android.util.Slog; import android.util.SparseArray; @@ -152,6 +156,11 @@ public class Notifier { private final Intent mScreenOffIntent; private final Bundle mScreenOnOffOptions; + // Display id -> ScreenTimeoutPolicyListenersContainer that contains list of screen + // wake lock listeners + private final SparseArray mScreenTimeoutPolicyListeners + = new SparseArray<>(); + // True if the device should suspend when the screen is off due to proximity. private final boolean mSuspendWhenScreenOffDueToProximityConfig; @@ -1269,6 +1278,146 @@ public class Notifier { } } + /** + * Adds a listener for the screen timeout policy + * @param displayId display ID + * @param screenTimeoutPolicy initial state of the timeout policy + * @param listener callback to receive screen timeout policy updates + */ + void addScreenTimeoutPolicyListener(int displayId, @ScreenTimeoutPolicy int screenTimeoutPolicy, + IScreenTimeoutPolicyListener listener) { + synchronized (mLock) { + ScreenTimeoutPolicyListenersContainer listenersContainer = + mScreenTimeoutPolicyListeners.get(displayId); + if (listenersContainer == null) { + listenersContainer = new ScreenTimeoutPolicyListenersContainer( + screenTimeoutPolicy); + mScreenTimeoutPolicyListeners.set(displayId, listenersContainer); + } + + listenersContainer.addListener(listener); + } + } + + /** + * Removes a listener for the screen timeout policy + * @param displayId display id from which the listener should be removed + * @param listener the instance of the listener + */ + void removeScreenTimeoutPolicyListener(int displayId, + IScreenTimeoutPolicyListener listener) { + synchronized (mLock) { + ScreenTimeoutPolicyListenersContainer listenersContainer = + mScreenTimeoutPolicyListeners.get(displayId); + if (listenersContainer == null) { + return; + } + + listenersContainer.removeListener(listener); + + if (listenersContainer.isEmpty()) { + mScreenTimeoutPolicyListeners.remove(displayId); + } + } + } + + /** + * Clears all screen timeout policy listeners for the specified display id + * @param displayId display id from which the listeners should be cleared + */ + void clearScreenTimeoutPolicyListeners(int displayId) { + synchronized (mLock) { + mScreenTimeoutPolicyListeners.remove(displayId); + } + } + + /** + * Notifies about screen timeout policy changes of the corresponding display group if + * it has changed + * @param displayGroupId the id of the display group to report + * @param screenTimeoutPolicy screen timeout policy + */ + void notifyScreenTimeoutPolicyChanges(int displayGroupId, + @ScreenTimeoutPolicy int screenTimeoutPolicy) { + synchronized (mLock) { + for (int idx = 0; idx < mScreenTimeoutPolicyListeners.size(); idx++) { + final int displayId = mScreenTimeoutPolicyListeners.keyAt(idx); + if (mDisplayManagerInternal.getGroupIdForDisplay(displayId) == displayGroupId) { + final ScreenTimeoutPolicyListenersContainer container = + mScreenTimeoutPolicyListeners.valueAt(idx); + container.updateScreenTimeoutPolicyAndNotifyIfNeeded(screenTimeoutPolicy); + } + } + } + } + + private final class ScreenTimeoutPolicyListenersContainer { + private final RemoteCallbackList mListeners; + private final ArrayMap mLastReportedState = + new ArrayMap<>(); + + @ScreenTimeoutPolicy + private volatile int mScreenTimeoutPolicy; + + ScreenTimeoutPolicyListenersContainer(int screenTimeoutPolicy) { + mScreenTimeoutPolicy = screenTimeoutPolicy; + mListeners = new RemoteCallbackList() { + @Override + public void onCallbackDied(IScreenTimeoutPolicyListener callbackInterface) { + mLastReportedState.remove(callbackInterface); + } + }; + } + + void updateScreenTimeoutPolicyAndNotifyIfNeeded( + @ScreenTimeoutPolicy int screenTimeoutPolicy) { + mScreenTimeoutPolicy = screenTimeoutPolicy; + + mHandler.post(() -> { + for (int i = mListeners.beginBroadcast() - 1; i >= 0; i--) { + final IScreenTimeoutPolicyListener listener = mListeners.getBroadcastItem(i); + notifyListenerIfNeeded(listener); + } + mListeners.finishBroadcast(); + }); + } + + void addListener(IScreenTimeoutPolicyListener listener) { + mListeners.register(listener); + mHandler.post(() -> notifyListenerIfNeeded(listener)); + } + + void removeListener(IScreenTimeoutPolicyListener listener) { + mListeners.unregister(listener); + mLastReportedState.remove(listener); + } + + boolean isEmpty() { + return mListeners.getRegisteredCallbackCount() == 0; + } + + private void notifyListenerIfNeeded(IScreenTimeoutPolicyListener listener) { + final int currentScreenTimeoutPolicy = mScreenTimeoutPolicy; + final Integer reportedScreenTimeoutPolicy = mLastReportedState.get(listener); + final boolean needsReporting = reportedScreenTimeoutPolicy == null + || !reportedScreenTimeoutPolicy.equals(currentScreenTimeoutPolicy); + + if (!needsReporting) return; + + try { + listener.onScreenTimeoutPolicyChanged(currentScreenTimeoutPolicy); + mLastReportedState.put(listener, currentScreenTimeoutPolicy); + } catch (RemoteException e) { + // The RemoteCallbackList will take care of removing + // the dead object for us. + Slog.e(TAG, "Remote exception when notifying screen timeout policy change", e); + } catch (Throwable e) { + Slog.e(TAG, "Exception when notifying screen timeout policy change", e); + removeListener(listener); + } + } + } + private final class NotifierHandler extends Handler { public NotifierHandler(Looper looper) { diff --git a/services/core/java/com/android/server/power/PowerGroup.java b/services/core/java/com/android/server/power/PowerGroup.java index 01a2045df426..86eb34cead15 100644 --- a/services/core/java/com/android/server/power/PowerGroup.java +++ b/services/core/java/com/android/server/power/PowerGroup.java @@ -16,6 +16,8 @@ package com.android.server.power; +import static android.os.PowerManager.SCREEN_TIMEOUT_KEEP_DISPLAY_ON; +import static android.os.PowerManager.SCREEN_TIMEOUT_ACTIVE; import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP; import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE; import static android.os.PowerManagerInternal.WAKEFULNESS_DOZING; @@ -34,6 +36,7 @@ import static com.android.server.power.PowerManagerService.WAKE_LOCK_STAY_AWAKE; import android.hardware.display.DisplayManagerInternal; import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest; import android.os.PowerManager; +import android.os.PowerManager.ScreenTimeoutPolicy; import android.os.PowerManagerInternal; import android.os.PowerSaveState; import android.os.Trace; @@ -415,6 +418,12 @@ public class PowerGroup { return (mWakeLockSummary & (screenOnWakeLockMask)) != 0; } + @ScreenTimeoutPolicy + public int getScreenTimeoutPolicy() { + return hasWakeLockKeepingScreenOnLocked() ? SCREEN_TIMEOUT_KEEP_DISPLAY_ON + : SCREEN_TIMEOUT_ACTIVE; + } + public void setWakeLockSummaryLocked(int summary) { mWakeLockSummary = summary; } diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index ce8dc69e4b26..23383a9c55c0 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -63,6 +63,7 @@ import android.hardware.SystemSensorManager; import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateManager; import android.hardware.display.AmbientDisplayConfiguration; +import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerInternal; import android.hardware.power.Boost; import android.hardware.power.Mode; @@ -76,6 +77,7 @@ import android.os.Handler; import android.os.HandlerExecutor; import android.os.IBinder; import android.os.IPowerManager; +import android.os.IScreenTimeoutPolicyListener; import android.os.IWakeLockCallback; import android.os.Looper; import android.os.Message; @@ -341,6 +343,7 @@ public final class PowerManagerService extends SystemService private LightsManager mLightsManager; private BatteryManagerInternal mBatteryManagerInternal; private DisplayManagerInternal mDisplayManagerInternal; + private DisplayManager mDisplayManager; private IBatteryStats mBatteryStats; private WindowManagerPolicy mPolicy; private Notifier mNotifier; @@ -758,6 +761,24 @@ public final class PowerManagerService extends SystemService } } + private final class DisplayListener implements DisplayManager.DisplayListener { + + @Override + public void onDisplayAdded(int displayId) { + + } + + @Override + public void onDisplayRemoved(int displayId) { + mNotifier.clearScreenTimeoutPolicyListeners(displayId); + } + + @Override + public void onDisplayChanged(int displayId) { + + } + } + private final class DisplayGroupPowerChangeListener implements DisplayManagerInternal.DisplayGroupListener { @@ -1354,6 +1375,7 @@ public final class PowerManagerService extends SystemService mDisplayManagerInternal = getLocalService(DisplayManagerInternal.class); mPolicy = getLocalService(WindowManagerPolicy.class); mBatteryManagerInternal = getLocalService(BatteryManagerInternal.class); + mDisplayManager = mContext.getSystemService(DisplayManager.class); mAttentionDetector.systemReady(mContext); SensorManager sensorManager = new SystemSensorManager(mContext, mHandler.getLooper()); @@ -1373,6 +1395,7 @@ public final class PowerManagerService extends SystemService DisplayGroupPowerChangeListener displayGroupPowerChangeListener = new DisplayGroupPowerChangeListener(); mDisplayManagerInternal.registerDisplayGroupListener(displayGroupPowerChangeListener); + mDisplayManager.registerDisplayListener(new DisplayListener(), mHandler); if(mDreamManager != null){ // This DreamManager method does not acquire a lock, so it should be safe to call. @@ -2571,7 +2594,10 @@ public final class PowerManagerService extends SystemService // Phase 5: Send notifications, if needed. finishWakefulnessChangeIfNeededLocked(); - // Phase 6: Update suspend blocker. + // Phase 6: Notify screen timeout policy changes if needed + notifyScreenTimeoutPolicyChangesLocked(); + + // Phase 7: Update suspend blocker. // Because we might release the last suspend blocker here, we need to make sure // we finished everything else first! updateSuspendBlockerLocked(); @@ -3824,6 +3850,16 @@ public final class PowerManagerService extends SystemService & WAKE_LOCK_PROXIMITY_SCREEN_OFF) != 0; } + @GuardedBy("mLock") + private void notifyScreenTimeoutPolicyChangesLocked() { + for (int idx = 0; idx < mPowerGroups.size(); idx++) { + final int powerGroupId = mPowerGroups.keyAt(idx); + final PowerGroup powerGroup = mPowerGroups.valueAt(idx); + final int screenTimeoutPolicy = powerGroup.getScreenTimeoutPolicy(); + mNotifier.notifyScreenTimeoutPolicyChanges(powerGroupId, screenTimeoutPolicy); + } + } + /** * Updates the suspend blocker that keeps the CPU alive. * @@ -5972,6 +6008,55 @@ public final class PowerManagerService extends SystemService } } + @Override // Binder call + public void addScreenTimeoutPolicyListener(int displayId, + IScreenTimeoutPolicyListener listener) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + + if (displayId == Display.INVALID_DISPLAY) { + throw new IllegalArgumentException("Valid display id is expected"); + } + + final long ident = Binder.clearCallingIdentity(); + try { + int initialTimeoutPolicy; + final int displayGroupId = mDisplayManagerInternal.getGroupIdForDisplay(displayId); + synchronized (mLock) { + final PowerGroup powerGroup = mPowerGroups.get(displayGroupId); + if (powerGroup != null) { + initialTimeoutPolicy = powerGroup.getScreenTimeoutPolicy(); + } else { + throw new IllegalArgumentException("No display found for the specified " + + "display id " + displayId); + } + } + + mNotifier.addScreenTimeoutPolicyListener(displayId, initialTimeoutPolicy, + listener); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override // Binder call + public void removeScreenTimeoutPolicyListener(int displayId, + IScreenTimeoutPolicyListener listener) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, + null); + + if (displayId == Display.INVALID_DISPLAY) { + throw new IllegalArgumentException("Valid display id is expected"); + } + + final long ident = Binder.clearCallingIdentity(); + try { + mNotifier.removeScreenTimeoutPolicyListener(displayId, listener); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + @Override // Binder call public void userActivity(int displayId, long eventTime, @PowerManager.UserActivityEvent int event, int flags) { diff --git a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java index 469bd66b7e7b..83a390d7f70b 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java @@ -16,6 +16,8 @@ package com.android.server.power; +import static android.os.PowerManager.SCREEN_TIMEOUT_KEEP_DISPLAY_ON; +import static android.os.PowerManager.SCREEN_TIMEOUT_ACTIVE; import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP; import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE; @@ -30,6 +32,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -49,7 +52,9 @@ import android.hardware.display.DisplayManagerInternal; import android.os.BatteryStats; import android.os.BatteryStatsInternal; import android.os.Handler; +import android.os.IBinder; import android.os.IWakeLockCallback; +import android.os.IScreenTimeoutPolicyListener; import android.os.Looper; import android.os.PowerManager; import android.os.RemoteException; @@ -84,6 +89,7 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.concurrent.Executor; @@ -916,6 +922,201 @@ public class NotifierTest { PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK), -1); } + @Test + public void testScreenTimeoutListener_reportsScreenTimeoutPolicyChange() throws Exception { + createNotifier(); + final IScreenTimeoutPolicyListener listener = Mockito.mock( + IScreenTimeoutPolicyListener.class); + final IBinder listenerBinder = Mockito.mock(IBinder.class); + when(listener.asBinder()).thenReturn(listenerBinder); + mNotifier.addScreenTimeoutPolicyListener(Display.DEFAULT_DISPLAY, + SCREEN_TIMEOUT_ACTIVE, listener); + mTestLooper.dispatchAll(); + clearInvocations(listener); + + mNotifier.notifyScreenTimeoutPolicyChanges(Display.DEFAULT_DISPLAY_GROUP, + /* hasScreenWakeLock= */ SCREEN_TIMEOUT_KEEP_DISPLAY_ON); + + // Verify that the event is sent asynchronously on a handler + verify(listener, never()).onScreenTimeoutPolicyChanged(anyInt()); + mTestLooper.dispatchAll(); + verify(listener).onScreenTimeoutPolicyChanged(SCREEN_TIMEOUT_KEEP_DISPLAY_ON); + } + + @Test + public void testScreenTimeoutListener_addAndRemoveListener_doesNotInvokeListener() + throws Exception { + createNotifier(); + final IScreenTimeoutPolicyListener listener = Mockito.mock( + IScreenTimeoutPolicyListener.class); + final IBinder listenerBinder = Mockito.mock(IBinder.class); + when(listener.asBinder()).thenReturn(listenerBinder); + mNotifier.addScreenTimeoutPolicyListener(Display.DEFAULT_DISPLAY, + SCREEN_TIMEOUT_ACTIVE, listener); + mTestLooper.dispatchAll(); + clearInvocations(listener); + mNotifier.removeScreenTimeoutPolicyListener(Display.DEFAULT_DISPLAY, listener); + + mNotifier.notifyScreenTimeoutPolicyChanges(Display.DEFAULT_DISPLAY_GROUP, + SCREEN_TIMEOUT_KEEP_DISPLAY_ON); + mTestLooper.dispatchAll(); + + // Callback should not be fired as listener is removed + verify(listener, never()).onScreenTimeoutPolicyChanged(anyInt()); + } + + @Test + public void testScreenTimeoutListener_addAndClearListeners_doesNotInvokeListener() + throws Exception { + createNotifier(); + final IScreenTimeoutPolicyListener listener = Mockito.mock( + IScreenTimeoutPolicyListener.class); + final IBinder listenerBinder = Mockito.mock(IBinder.class); + when(listener.asBinder()).thenReturn(listenerBinder); + mNotifier.addScreenTimeoutPolicyListener(Display.DEFAULT_DISPLAY, + SCREEN_TIMEOUT_ACTIVE, listener); + mTestLooper.dispatchAll(); + clearInvocations(listener); + mNotifier.clearScreenTimeoutPolicyListeners(Display.DEFAULT_DISPLAY); + + mNotifier.notifyScreenTimeoutPolicyChanges(Display.DEFAULT_DISPLAY_GROUP, + SCREEN_TIMEOUT_KEEP_DISPLAY_ON); + mTestLooper.dispatchAll(); + + // Callback should not be fired as listener is removed + verify(listener, never()).onScreenTimeoutPolicyChanged(anyInt()); + } + + @Test + public void testScreenTimeoutListener_subscribedToAnotherDisplay_listenerNotFired() + throws Exception { + createNotifier(); + + final IScreenTimeoutPolicyListener listener = Mockito.mock( + IScreenTimeoutPolicyListener.class); + final IBinder listenerBinder = Mockito.mock(IBinder.class); + when(listener.asBinder()).thenReturn(listenerBinder); + mNotifier.addScreenTimeoutPolicyListener(Display.DEFAULT_DISPLAY, + SCREEN_TIMEOUT_ACTIVE, listener); + mTestLooper.dispatchAll(); + clearInvocations(listener); + + mNotifier.notifyScreenTimeoutPolicyChanges(/* displayGroupId= */ 123, + SCREEN_TIMEOUT_KEEP_DISPLAY_ON); + mTestLooper.dispatchAll(); + + // Callback should not be fired as we subscribed only to the DEFAULT_DISPLAY + verify(listener, never()).onScreenTimeoutPolicyChanged(anyInt()); + } + + @Test + public void testScreenTimeoutListener_listenerDied_listenerNotFired() + throws Exception { + createNotifier(); + + final IScreenTimeoutPolicyListener listener = Mockito.mock( + IScreenTimeoutPolicyListener.class); + final IBinder listenerBinder = Mockito.mock(IBinder.class); + when(listener.asBinder()).thenReturn(listenerBinder); + + mNotifier.addScreenTimeoutPolicyListener(Display.DEFAULT_DISPLAY, + SCREEN_TIMEOUT_ACTIVE, listener); + mTestLooper.dispatchAll(); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(IBinder.DeathRecipient.class); + verify(listenerBinder).linkToDeath(captor.capture(), anyInt()); + mTestLooper.dispatchAll(); + captor.getValue().binderDied(); + clearInvocations(listener); + + mNotifier.notifyScreenTimeoutPolicyChanges(Display.DEFAULT_DISPLAY, + SCREEN_TIMEOUT_KEEP_DISPLAY_ON); + mTestLooper.dispatchAll(); + + // Callback should not be fired as binder died + verify(listener, never()).onScreenTimeoutPolicyChanged(anyInt()); + } + + @Test + public void testScreenTimeoutListener_listenerThrowsException_listenerNotFiredSecondTime() + throws Exception { + createNotifier(); + + final IScreenTimeoutPolicyListener listener = Mockito.mock( + IScreenTimeoutPolicyListener.class); + final IBinder listenerBinder = Mockito.mock(IBinder.class); + when(listener.asBinder()).thenReturn(listenerBinder); + doThrow(RuntimeException.class).when(listener).onScreenTimeoutPolicyChanged(anyInt()); + mNotifier.addScreenTimeoutPolicyListener(Display.DEFAULT_DISPLAY_GROUP, + SCREEN_TIMEOUT_ACTIVE, listener); + mTestLooper.dispatchAll(); + clearInvocations(listener); + + mNotifier.notifyScreenTimeoutPolicyChanges(Display.DEFAULT_DISPLAY, + SCREEN_TIMEOUT_KEEP_DISPLAY_ON); + mTestLooper.dispatchAll(); + + // Callback should not be fired as it has thrown an exception once + verify(listener, never()).onScreenTimeoutPolicyChanged(anyInt()); + } + + @Test + public void testScreenTimeoutListener_nonDefaultDisplay_stillReportsPolicyCorrectly() + throws Exception { + createNotifier(); + final int otherDisplayId = 123; + final int otherDisplayGroupId = 123_00; + when(mDisplayManagerInternal.getGroupIdForDisplay(otherDisplayId)).thenReturn( + otherDisplayGroupId); + final IScreenTimeoutPolicyListener listener = Mockito.mock( + IScreenTimeoutPolicyListener.class); + final IBinder listenerBinder = Mockito.mock(IBinder.class); + when(listener.asBinder()).thenReturn(listenerBinder); + mNotifier.addScreenTimeoutPolicyListener(otherDisplayId, + SCREEN_TIMEOUT_ACTIVE, listener); + mTestLooper.dispatchAll(); + clearInvocations(listener); + + mNotifier.notifyScreenTimeoutPolicyChanges(otherDisplayGroupId, + SCREEN_TIMEOUT_KEEP_DISPLAY_ON); + mTestLooper.dispatchAll(); + + verify(listener).onScreenTimeoutPolicyChanged(SCREEN_TIMEOUT_KEEP_DISPLAY_ON); + } + + @Test + public void testScreenTimeoutListener_timeoutPolicyTimeout_reportsTimeoutOnSubscription() + throws Exception { + createNotifier(); + final IScreenTimeoutPolicyListener listener = Mockito.mock( + IScreenTimeoutPolicyListener.class); + final IBinder listenerBinder = Mockito.mock(IBinder.class); + when(listener.asBinder()).thenReturn(listenerBinder); + + mNotifier.addScreenTimeoutPolicyListener(Display.DEFAULT_DISPLAY, + SCREEN_TIMEOUT_ACTIVE, listener); + mTestLooper.dispatchAll(); + + verify(listener).onScreenTimeoutPolicyChanged(SCREEN_TIMEOUT_ACTIVE); + } + + @Test + public void testScreenTimeoutListener_policyHeld_reportsHeldOnSubscription() + throws Exception { + createNotifier(); + final IScreenTimeoutPolicyListener listener = Mockito.mock( + IScreenTimeoutPolicyListener.class); + final IBinder listenerBinder = Mockito.mock(IBinder.class); + when(listener.asBinder()).thenReturn(listenerBinder); + + mNotifier.addScreenTimeoutPolicyListener(Display.DEFAULT_DISPLAY, + SCREEN_TIMEOUT_KEEP_DISPLAY_ON, listener); + mTestLooper.dispatchAll(); + + verify(listener).onScreenTimeoutPolicyChanged(SCREEN_TIMEOUT_KEEP_DISPLAY_ON); + } + private final PowerManagerService.Injector mInjector = new PowerManagerService.Injector() { @Override Notifier createNotifier(Looper looper, Context context, IBatteryStats batteryStats, diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java index 376091e4a241..3cb27451bd57 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java @@ -20,6 +20,8 @@ import static android.app.ActivityManager.PROCESS_STATE_BOUND_TOP; import static android.app.ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE; import static android.app.ActivityManager.PROCESS_STATE_RECEIVER; import static android.app.ActivityManager.PROCESS_STATE_TOP_SLEEPING; +import static android.os.PowerManager.SCREEN_TIMEOUT_KEEP_DISPLAY_ON; +import static android.os.PowerManager.SCREEN_TIMEOUT_ACTIVE; import static android.os.PowerManager.USER_ACTIVITY_EVENT_BUTTON; import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP; import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE; @@ -69,6 +71,7 @@ import android.hardware.devicestate.DeviceState; import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback; import android.hardware.display.AmbientDisplayConfiguration; +import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerInternal; import android.hardware.power.Boost; import android.hardware.power.Mode; @@ -79,6 +82,7 @@ import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.IWakeLockCallback; +import android.os.IScreenTimeoutPolicyListener; import android.os.Looper; import android.os.PowerManager; import android.os.PowerManagerInternal; @@ -132,6 +136,7 @@ import org.junit.rules.TestRule; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @@ -166,6 +171,7 @@ public class PowerManagerServiceTest { @Mock private BatterySaverStateMachine mBatterySaverStateMachineMock; @Mock private LightsManager mLightsManagerMock; @Mock private DisplayManagerInternal mDisplayManagerInternalMock; + @Mock private DisplayManager mDisplayManagerMock; @Mock private BatteryManagerInternal mBatteryManagerInternalMock; @Mock private ActivityManagerInternal mActivityManagerInternalMock; @Mock private AttentionManagerInternal mAttentionManagerInternalMock; @@ -185,6 +191,8 @@ public class PowerManagerServiceTest { @Mock private DeviceStateManager mDeviceStateManagerMock; @Mock private DeviceConfigParameterProvider mDeviceParameterProvider; + @Captor private ArgumentCaptor mDisplayListenerArgumentCaptor; + @Rule public TestRule compatChangeRule = new PlatformCompatChangeRule(); @Rule public SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -254,6 +262,7 @@ public class PowerManagerServiceTest { mContextSpy = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); mResourcesSpy = spy(mContextSpy.getResources()); when(mContextSpy.getResources()).thenReturn(mResourcesSpy); + when(mContextSpy.getSystemService(DisplayManager.class)).thenReturn(mDisplayManagerMock); setBatterySaverSupported(); MockContentResolver cr = new MockContentResolver(mContextSpy); @@ -2947,15 +2956,19 @@ public class PowerManagerServiceTest { assertThat(mService.getPowerGroupSize()).isEqualTo(4); } - private WakeLock acquireWakeLock(String tag, int flags) { + private WakeLock acquireWakeLock(String tag, int flags, int displayId) { IBinder token = new Binder(); String packageName = "pkg.name"; mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName, - null /* workSource */, null /* historyTag */, Display.INVALID_DISPLAY, + null /* workSource */, null /* historyTag */, displayId, null /* callback */); return mService.findWakeLockLocked(token); } + private WakeLock acquireWakeLock(String tag, int flags) { + return acquireWakeLock(tag, flags, Display.INVALID_DISPLAY); + } + /** * Test IPowerManager.acquireWakeLock() with a IWakeLockCallback. */ @@ -3442,6 +3455,106 @@ public class PowerManagerServiceTest { } } + @Test + public void testAddWakeLockKeepingScreenOn_addsToNotifierAndReportsTimeoutPolicyChange() { + IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); + when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds); + + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.displayGroupId = Display.DEFAULT_DISPLAY_GROUP; + when(mDisplayManagerInternalMock.getDisplayInfo(Display.DEFAULT_DISPLAY)) + .thenReturn(displayInfo); + + createService(); + startSystem(); + + final IScreenTimeoutPolicyListener listener = Mockito.mock( + IScreenTimeoutPolicyListener.class); + mService.getBinderServiceInstance().addScreenTimeoutPolicyListener( + Display.DEFAULT_DISPLAY_GROUP, listener); + verify(mNotifierMock).addScreenTimeoutPolicyListener(Display.DEFAULT_DISPLAY_GROUP, + SCREEN_TIMEOUT_ACTIVE, listener); + clearInvocations(mNotifierMock); + + acquireWakeLock("screenBright", PowerManager.SCREEN_BRIGHT_WAKE_LOCK, + Display.DEFAULT_DISPLAY); + verify(mNotifierMock).notifyScreenTimeoutPolicyChanges(Display.DEFAULT_DISPLAY_GROUP, + SCREEN_TIMEOUT_KEEP_DISPLAY_ON); + } + + @Test + public void test_addAndRemoveScreenTimeoutListener_propagatesToNotifier() + throws Exception { + IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); + when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds); + + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.displayGroupId = Display.DEFAULT_DISPLAY_GROUP; + when(mDisplayManagerInternalMock.getDisplayInfo(Display.DEFAULT_DISPLAY)) + .thenReturn(displayInfo); + + createService(); + startSystem(); + + final IScreenTimeoutPolicyListener listener = Mockito.mock( + IScreenTimeoutPolicyListener.class); + mService.getBinderServiceInstance().addScreenTimeoutPolicyListener( + Display.DEFAULT_DISPLAY, listener); + + clearInvocations(mNotifierMock); + mService.getBinderServiceInstance().removeScreenTimeoutPolicyListener( + Display.DEFAULT_DISPLAY, listener); + verify(mNotifierMock).removeScreenTimeoutPolicyListener(Display.DEFAULT_DISPLAY, + listener); + } + + @Test + public void test_displayIsRemoved_clearsScreenTimeoutListeners() + throws Exception { + IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); + when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds); + + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.displayGroupId = Display.DEFAULT_DISPLAY_GROUP; + when(mDisplayManagerInternalMock.getDisplayInfo(Display.DEFAULT_DISPLAY)) + .thenReturn(displayInfo); + + createService(); + startSystem(); + verify(mDisplayManagerMock).registerDisplayListener( + mDisplayListenerArgumentCaptor.capture(), any()); + clearInvocations(mNotifierMock); + + mDisplayListenerArgumentCaptor.getValue().onDisplayRemoved(Display.DEFAULT_DISPLAY); + + verify(mNotifierMock).clearScreenTimeoutPolicyListeners(Display.DEFAULT_DISPLAY); + } + + @Test + public void testScreenWakeLockListener_screenHasWakelocks_addsWithHeldTimeoutPolicyToNotifier() + throws Exception { + IntArray displayGroupIds = IntArray.wrap(new int[]{Display.DEFAULT_DISPLAY_GROUP}); + when(mDisplayManagerInternalMock.getDisplayGroupIds()).thenReturn(displayGroupIds); + + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.displayGroupId = Display.DEFAULT_DISPLAY_GROUP; + when(mDisplayManagerInternalMock.getDisplayInfo(Display.DEFAULT_DISPLAY)) + .thenReturn(displayInfo); + + createService(); + startSystem(); + + acquireWakeLock("screenBright", PowerManager.SCREEN_BRIGHT_WAKE_LOCK, + Display.DEFAULT_DISPLAY); + + final IScreenTimeoutPolicyListener listener = Mockito.mock( + IScreenTimeoutPolicyListener.class); + mService.getBinderServiceInstance().addScreenTimeoutPolicyListener( + Display.DEFAULT_DISPLAY_GROUP, listener); + verify(mNotifierMock).notifyScreenTimeoutPolicyChanges(Display.DEFAULT_DISPLAY_GROUP, + SCREEN_TIMEOUT_KEEP_DISPLAY_ON); + } + @Test public void testHalAutoSuspendMode_enabledByConfiguration() { AtomicReference callback = -- cgit v1.2.3-59-g8ed1b