From 3d21c80934f96c3b8c975512e7c121e6e011f5f6 Mon Sep 17 00:00:00 2001 From: Kurt Dresner Date: Tue, 29 Sep 2020 15:32:11 -0700 Subject: [PATCH] Add projection state APIs to UiModeManager and UiModeManagerService. Bug: 134997071 Bug: 169702986 Test: Code builds, runs, unit tests added for new functionality. Change-Id: I29cfb198101cdc5d1f4de84f8701928643b756c3 --- .../idle/DeviceIdlenessTracker.java | 68 +-- api/system-current.txt | 10 + core/api/system-current.txt | 10 + core/api/test-current.txt | 5 + .../app/IOnProjectionStateChangeListener.aidl | 22 + core/java/android/app/IUiModeManager.aidl | 32 ++ core/java/android/app/UiModeManager.java | 328 ++++++++++++- core/proto/android/server/jobscheduler.proto | 3 +- core/res/AndroidManifest.xml | 15 + data/etc/privapp-permissions-platform.xml | 4 +- packages/Shell/AndroidManifest.xml | 3 + .../android/server/UiModeManagerService.java | 338 ++++++++++++- .../batterysaver/BatterySaverPolicy.java | 109 ++--- .../server/job/JobSchedulerServiceTest.java | 3 + .../tests/servicestests/AndroidManifest.xml | 1 + .../batterysaver/BatterySaverPolicyTest.java | 14 +- services/tests/uiservicestests/Android.bp | 3 + .../server/UiModeManagerServiceTest.java | 446 ++++++++++++++++-- 18 files changed, 1268 insertions(+), 146 deletions(-) create mode 100644 core/java/android/app/IOnProjectionStateChangeListener.aidl diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java index e2c8f649fdb7c..3c4961ab68256 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/idle/DeviceIdlenessTracker.java @@ -16,6 +16,8 @@ package com.android.server.job.controllers.idle; +import static android.app.UiModeManager.PROJECTION_TYPE_NONE; + import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import android.app.AlarmManager; @@ -34,7 +36,9 @@ import com.android.server.job.JobSchedulerService; import com.android.server.job.StateControllerProto; import java.io.PrintWriter; +import java.util.Set; +/** Class to track device idle state. */ public final class DeviceIdlenessTracker extends BroadcastReceiver implements IdlenessTracker { private static final String TAG = "JobScheduler.DeviceIdlenessTracker"; private static final boolean DEBUG = JobSchedulerService.DEBUG @@ -50,8 +54,10 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id private boolean mIdle; private boolean mScreenOn; private boolean mDockIdle; - private boolean mInCarMode; + private boolean mProjectionActive; private IdlenessListener mIdleListener; + private final UiModeManager.OnProjectionStateChangeListener mOnProjectionStateChangeListener = + this::onProjectionStateChanged; private AlarmManager.OnAlarmListener mIdleAlarmListener = () -> { handleIdleTrigger(); @@ -60,10 +66,7 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id public DeviceIdlenessTracker() { // At boot we presume that the user has just "interacted" with the // device in some meaningful way. - mIdle = false; mScreenOn = true; - mDockIdle = false; - mInCarMode = false; } @Override @@ -98,11 +101,34 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id filter.addAction(Intent.ACTION_DOCK_IDLE); filter.addAction(Intent.ACTION_DOCK_ACTIVE); - // Car mode - filter.addAction(UiModeManager.ACTION_ENTER_CAR_MODE_PRIORITIZED); - filter.addAction(UiModeManager.ACTION_EXIT_CAR_MODE_PRIORITIZED); - context.registerReceiver(this, filter); + + // TODO(b/172579710): Move the callbacks off the main executor and on to + // JobSchedulerBackgroundThread.getExecutor() once synchronization is fixed in this class. + context.getSystemService(UiModeManager.class).addOnProjectionStateChangeListener( + UiModeManager.PROJECTION_TYPE_ALL, context.getMainExecutor(), + mOnProjectionStateChangeListener); + } + + private void onProjectionStateChanged(@UiModeManager.ProjectionType int activeProjectionTypes, + Set projectingPackages) { + boolean projectionActive = activeProjectionTypes != PROJECTION_TYPE_NONE; + if (mProjectionActive == projectionActive) { + return; + } + if (DEBUG) { + Slog.v(TAG, "Projection state changed: " + projectionActive); + } + mProjectionActive = projectionActive; + if (mProjectionActive) { + cancelIdlenessCheck(); + if (mIdle) { + mIdle = false; + mIdleListener.reportNewIdleState(mIdle); + } + } else { + maybeScheduleIdlenessCheck("Projection ended"); + } } @Override @@ -110,8 +136,7 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id pw.print(" mIdle: "); pw.println(mIdle); pw.print(" mScreenOn: "); pw.println(mScreenOn); pw.print(" mDockIdle: "); pw.println(mDockIdle); - pw.print(" mInCarMode: "); - pw.println(mInCarMode); + pw.print(" mProjectionActive: "); pw.println(mProjectionActive); } @Override @@ -129,8 +154,9 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id StateControllerProto.IdleController.IdlenessTracker.DeviceIdlenessTracker.IS_DOCK_IDLE, mDockIdle); proto.write( - StateControllerProto.IdleController.IdlenessTracker.DeviceIdlenessTracker.IN_CAR_MODE, - mInCarMode); + StateControllerProto.IdleController.IdlenessTracker.DeviceIdlenessTracker + .PROJECTION_ACTIVE, + mProjectionActive); proto.end(diToken); proto.end(token); @@ -186,18 +212,6 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id } maybeScheduleIdlenessCheck(action); break; - case UiModeManager.ACTION_ENTER_CAR_MODE_PRIORITIZED: - mInCarMode = true; - cancelIdlenessCheck(); - if (mIdle) { - mIdle = false; - mIdleListener.reportNewIdleState(mIdle); - } - break; - case UiModeManager.ACTION_EXIT_CAR_MODE_PRIORITIZED: - mInCarMode = false; - maybeScheduleIdlenessCheck(action); - break; case ActivityManagerService.ACTION_TRIGGER_IDLE: handleIdleTrigger(); break; @@ -205,7 +219,7 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id } private void maybeScheduleIdlenessCheck(String reason) { - if ((!mScreenOn || mDockIdle) && !mInCarMode) { + if ((!mScreenOn || mDockIdle) && !mProjectionActive) { final long nowElapsed = sElapsedRealtimeClock.millis(); final long when = nowElapsed + mInactivityIdleThreshold; if (DEBUG) { @@ -222,7 +236,7 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id private void handleIdleTrigger() { // idle time starts now. Do not set mIdle if screen is on. - if (!mIdle && (!mScreenOn || mDockIdle) && !mInCarMode) { + if (!mIdle && (!mScreenOn || mDockIdle) && !mProjectionActive) { if (DEBUG) { Slog.v(TAG, "Idle trigger fired @ " + sElapsedRealtimeClock.millis()); } @@ -231,7 +245,7 @@ public final class DeviceIdlenessTracker extends BroadcastReceiver implements Id } else { if (DEBUG) { Slog.v(TAG, "TRIGGER_IDLE received but not changing state; idle=" - + mIdle + " screen=" + mScreenOn + " car=" + mInCarMode); + + mIdle + " screen=" + mScreenOn + " projection=" + mProjectionActive); } } } diff --git a/api/system-current.txt b/api/system-current.txt index b28e7afddbe31..d4dafe7536167 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -182,6 +182,7 @@ package android { field public static final String READ_PRINT_SERVICES = "android.permission.READ_PRINT_SERVICES"; field public static final String READ_PRINT_SERVICE_RECOMMENDATIONS = "android.permission.READ_PRINT_SERVICE_RECOMMENDATIONS"; field public static final String READ_PRIVILEGED_PHONE_STATE = "android.permission.READ_PRIVILEGED_PHONE_STATE"; + field public static final String READ_PROJECTION_STATE = "android.permission.READ_PROJECTION_STATE"; field public static final String READ_RUNTIME_PROFILES = "android.permission.READ_RUNTIME_PROFILES"; field public static final String READ_SEARCH_INDEXABLES = "android.permission.READ_SEARCH_INDEXABLES"; field public static final String READ_SYSTEM_UPDATE_INFO = "android.permission.READ_SYSTEM_UPDATE_INFO"; @@ -234,6 +235,7 @@ package android { field public static final String SUSPEND_APPS = "android.permission.SUSPEND_APPS"; field public static final String SYSTEM_CAMERA = "android.permission.SYSTEM_CAMERA"; field public static final String TETHER_PRIVILEGED = "android.permission.TETHER_PRIVILEGED"; + field public static final String TOGGLE_AUTOMOTIVE_PROJECTION = "android.permission.TOGGLE_AUTOMOTIVE_PROJECTION"; field public static final String TV_INPUT_HARDWARE = "android.permission.TV_INPUT_HARDWARE"; field public static final String TV_VIRTUAL_REMOTE_CONTROLLER = "android.permission.TV_VIRTUAL_REMOTE_CONTROLLER"; field public static final String UNLIMITED_SHORTCUTS_API_CALLS = "android.permission.UNLIMITED_SHORTCUTS_API_CALLS"; @@ -796,7 +798,11 @@ package android.app { } public class UiModeManager { + method @RequiresPermission(android.Manifest.permission.READ_PROJECTION_STATE) public void addOnProjectionStateChangeListener(int, @NonNull java.util.concurrent.Executor, @NonNull android.app.UiModeManager.OnProjectionStateChangeListener); method @RequiresPermission(android.Manifest.permission.ENTER_CAR_MODE_PRIORITIZED) public void enableCarMode(@IntRange(from=0) int, int); + method @RequiresPermission(android.Manifest.permission.READ_PROJECTION_STATE) public int getActiveProjectionTypes(); + method @NonNull @RequiresPermission(android.Manifest.permission.READ_PROJECTION_STATE) public java.util.Set getProjectingPackages(int); + method @RequiresPermission(android.Manifest.permission.READ_PROJECTION_STATE) public void removeOnProjectionStateChangeListener(@NonNull android.app.UiModeManager.OnProjectionStateChangeListener); field public static final String ACTION_ENTER_CAR_MODE_PRIORITIZED = "android.app.action.ENTER_CAR_MODE_PRIORITIZED"; field public static final String ACTION_EXIT_CAR_MODE_PRIORITIZED = "android.app.action.EXIT_CAR_MODE_PRIORITIZED"; field public static final int DEFAULT_PRIORITY = 0; // 0x0 @@ -804,6 +810,10 @@ package android.app { field public static final String EXTRA_PRIORITY = "android.app.extra.PRIORITY"; } + public static interface UiModeManager.OnProjectionStateChangeListener { + method public void onProjectionStateChanged(int, @NonNull java.util.Set); + } + public final class Vr2dDisplayProperties implements android.os.Parcelable { ctor public Vr2dDisplayProperties(int, int, int); method public int describeContents(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 8b15ec3b19214..ac4b36976921a 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -182,6 +182,7 @@ package android { field public static final String READ_PRINT_SERVICES = "android.permission.READ_PRINT_SERVICES"; field public static final String READ_PRINT_SERVICE_RECOMMENDATIONS = "android.permission.READ_PRINT_SERVICE_RECOMMENDATIONS"; field public static final String READ_PRIVILEGED_PHONE_STATE = "android.permission.READ_PRIVILEGED_PHONE_STATE"; + field public static final String READ_PROJECTION_STATE = "android.permission.READ_PROJECTION_STATE"; field public static final String READ_RUNTIME_PROFILES = "android.permission.READ_RUNTIME_PROFILES"; field public static final String READ_SEARCH_INDEXABLES = "android.permission.READ_SEARCH_INDEXABLES"; field public static final String READ_SYSTEM_UPDATE_INFO = "android.permission.READ_SYSTEM_UPDATE_INFO"; @@ -234,6 +235,7 @@ package android { field public static final String SUSPEND_APPS = "android.permission.SUSPEND_APPS"; field public static final String SYSTEM_CAMERA = "android.permission.SYSTEM_CAMERA"; field public static final String TETHER_PRIVILEGED = "android.permission.TETHER_PRIVILEGED"; + field public static final String TOGGLE_AUTOMOTIVE_PROJECTION = "android.permission.TOGGLE_AUTOMOTIVE_PROJECTION"; field public static final String TV_INPUT_HARDWARE = "android.permission.TV_INPUT_HARDWARE"; field public static final String TV_VIRTUAL_REMOTE_CONTROLLER = "android.permission.TV_VIRTUAL_REMOTE_CONTROLLER"; field public static final String UNLIMITED_SHORTCUTS_API_CALLS = "android.permission.UNLIMITED_SHORTCUTS_API_CALLS"; @@ -744,7 +746,11 @@ package android.app { } public class UiModeManager { + method @RequiresPermission(android.Manifest.permission.READ_PROJECTION_STATE) public void addOnProjectionStateChangeListener(int, @NonNull java.util.concurrent.Executor, @NonNull android.app.UiModeManager.OnProjectionStateChangeListener); method @RequiresPermission(android.Manifest.permission.ENTER_CAR_MODE_PRIORITIZED) public void enableCarMode(@IntRange(from=0) int, int); + method @RequiresPermission(android.Manifest.permission.READ_PROJECTION_STATE) public int getActiveProjectionTypes(); + method @NonNull @RequiresPermission(android.Manifest.permission.READ_PROJECTION_STATE) public java.util.Set getProjectingPackages(int); + method @RequiresPermission(android.Manifest.permission.READ_PROJECTION_STATE) public void removeOnProjectionStateChangeListener(@NonNull android.app.UiModeManager.OnProjectionStateChangeListener); field public static final String ACTION_ENTER_CAR_MODE_PRIORITIZED = "android.app.action.ENTER_CAR_MODE_PRIORITIZED"; field public static final String ACTION_EXIT_CAR_MODE_PRIORITIZED = "android.app.action.EXIT_CAR_MODE_PRIORITIZED"; field public static final int DEFAULT_PRIORITY = 0; // 0x0 @@ -752,6 +758,10 @@ package android.app { field public static final String EXTRA_PRIORITY = "android.app.extra.PRIORITY"; } + public static interface UiModeManager.OnProjectionStateChangeListener { + method public void onProjectionStateChanged(int, @NonNull java.util.Set); + } + public final class Vr2dDisplayProperties implements android.os.Parcelable { ctor public Vr2dDisplayProperties(int, int, int); method public int describeContents(); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 67b496b4f7ab0..1fdb3e4a6fdeb 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -308,6 +308,11 @@ package android.app { public class UiModeManager { method public boolean isNightModeLocked(); method public boolean isUiModeLocked(); + method @RequiresPermission(value=android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, conditional=true) public boolean releaseProjection(int); + method @RequiresPermission(value=android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, conditional=true) public boolean requestProjection(int); + field public static final int PROJECTION_TYPE_ALL = 65535; // 0xffff + field public static final int PROJECTION_TYPE_AUTOMOTIVE = 1; // 0x1 + field public static final int PROJECTION_TYPE_NONE = 0; // 0x0 } public class WallpaperManager { diff --git a/core/java/android/app/IOnProjectionStateChangeListener.aidl b/core/java/android/app/IOnProjectionStateChangeListener.aidl new file mode 100644 index 0000000000000..f15498545a680 --- /dev/null +++ b/core/java/android/app/IOnProjectionStateChangeListener.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2020 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.app; + +/** {@hide} */ +oneway interface IOnProjectionStateChangeListener { + void onProjectionStateChanged(int activeProjectionTypes, in List projectingPackages); +} \ No newline at end of file diff --git a/core/java/android/app/IUiModeManager.aidl b/core/java/android/app/IUiModeManager.aidl index 41e2ec9e35721..0ba5beccbf32d 100644 --- a/core/java/android/app/IUiModeManager.aidl +++ b/core/java/android/app/IUiModeManager.aidl @@ -16,6 +16,8 @@ package android.app; +import android.app.IOnProjectionStateChangeListener; + /** * Interface used to control special UI modes. * @hide @@ -93,4 +95,34 @@ interface IUiModeManager { * Sets custom end clock time */ void setCustomNightModeEnd(long time); + + /** + * Sets projection state for the caller for the given projection type. + */ + boolean requestProjection(in IBinder binder, int projectionType, String callingPackage); + + /** + * Releases projection state for the caller for the given projection type. + */ + boolean releaseProjection(int projectionType, String callingPackage); + + /** + * Registers a listener for changes to projection state. + */ + void addOnProjectionStateChangeListener(in IOnProjectionStateChangeListener listener, int projectionType); + + /** + * Unregisters a listener for changes to projection state. + */ + void removeOnProjectionStateChangeListener(in IOnProjectionStateChangeListener listener); + + /** + * Returns packages that have currently set the given projection type. + */ + List getProjectingPackages(int projectionType); + + /** + * Returns currently set projection types. + */ + int getActiveProjectionTypes(); } diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java index e2fc5dbf10e23..8a6871fdf3f05 100644 --- a/core/java/android/app/UiModeManager.java +++ b/core/java/android/app/UiModeManager.java @@ -16,6 +16,7 @@ package android.app; +import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; @@ -27,20 +28,32 @@ import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.res.Configuration; +import android.os.Binder; import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.function.pooled.PooledLambda; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; import java.time.LocalTime; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executor; /** * This class provides access to the system uimode services. These services * allow applications to control UI modes of the device. * It provides functionality to disable the car mode and it gives access to the * night mode settings. - * + * *

These facilities are built on top of the underlying * {@link android.content.Intent#ACTION_DOCK_EVENT} broadcasts that are sent when the user * physical places the device into and out of a dock. When that happens, @@ -49,7 +62,7 @@ import java.time.LocalTime; * starts the corresponding mode activity if appropriate. See the * broadcasts {@link #ACTION_ENTER_CAR_MODE} and * {@link #ACTION_ENTER_DESK_MODE} for more information. - * + * *

In addition, the user may manually switch the system to car mode without * physically being in a dock. While in car mode -- whether by manual action * from the user or being physically placed in a dock -- a notification is @@ -59,6 +72,25 @@ import java.time.LocalTime; */ @SystemService(Context.UI_MODE_SERVICE) public class UiModeManager { + /** + * A listener with a single method that is invoked whenever the packages projecting using the + * {@link ProjectionType}s for which it is registered change. + * + * @hide + */ + @SystemApi + public interface OnProjectionStateChangeListener { + /** + * Callback invoked when projection state changes for a {@link ProjectionType} for which + * this listener was added. + * @param projectionType the listened-for {@link ProjectionType}s that have changed + * @param packageNames the {@link Set} of package names that have currently set those + * {@link ProjectionType}s. + */ + void onProjectionStateChanged(@ProjectionType int projectionType, + @NonNull Set packageNames); + } + private static final String TAG = "UiModeManager"; /** @@ -100,7 +132,7 @@ public class UiModeManager { @SystemApi public static final String ACTION_ENTER_CAR_MODE_PRIORITIZED = "android.app.action.ENTER_CAR_MODE_PRIORITIZED"; - + /** * Broadcast sent when the device's UI has switch away from car mode back * to normal mode. Typically used by a car mode app, to dismiss itself @@ -137,7 +169,7 @@ public class UiModeManager { @SystemApi public static final String ACTION_EXIT_CAR_MODE_PRIORITIZED = "android.app.action.EXIT_CAR_MODE_PRIORITIZED"; - + /** * Broadcast sent when the device's UI has switched to desk mode, * by being placed in a desk dock. After @@ -151,7 +183,7 @@ public class UiModeManager { * of the broadcast to {@link Activity#RESULT_CANCELED}. */ public static String ACTION_ENTER_DESK_MODE = "android.app.action.ENTER_DESK_MODE"; - + /** * Broadcast sent when the device's UI has switched away from desk mode back * to normal mode. Typically used by a desk mode app, to dismiss itself @@ -198,13 +230,13 @@ public class UiModeManager { * automatically switch night mode on and off based on the time. */ public static final int MODE_NIGHT_CUSTOM = 3; - + /** * Constant for {@link #setNightMode(int)} and {@link #getNightMode()}: * never run in night mode. */ public static final int MODE_NIGHT_NO = 1; - + /** * Constant for {@link #setNightMode(int)} and {@link #getNightMode()}: * always run in night mode. @@ -219,6 +251,24 @@ public class UiModeManager { */ private @Nullable Context mContext; + private final Object mLock = new Object(); + /** + * Map that stores internally created {@link InnerListener} objects keyed by their corresponding + * externally provided {@link OnProjectionStateChangeListener} objects. + */ + @GuardedBy("mLock") + private final Map + mProjectionStateListenerMap = new ArrayMap<>(); + + /** + * Resource manager that prevents memory leakage of Contexts via binder objects if clients + * fail to remove listeners. + */ + @GuardedBy("mLock") + private final OnProjectionStateChangeListenerResourceManager + mOnProjectionStateChangeListenerResourceManager = + new OnProjectionStateChangeListenerResourceManager(); + @UnsupportedAppUsage /*package*/ UiModeManager() throws ServiceNotFoundException { this(null /* context */); @@ -251,7 +301,7 @@ public class UiModeManager { public static final int ENABLE_CAR_MODE_ALLOW_SLEEP = 0x0002; /** @hide */ - @IntDef(prefix = { "ENABLE_CAR_MODE_" }, value = { + @IntDef(prefix = {"ENABLE_CAR_MODE_"}, value = { ENABLE_CAR_MODE_GO_CAR_HOME, ENABLE_CAR_MODE_ALLOW_SLEEP }) @@ -362,7 +412,7 @@ public class UiModeManager { */ @SystemApi public static final int DEFAULT_PRIORITY = 0; - + /** * Turn off special mode if currently in car mode. * @param flags One of the disable car mode flags. @@ -595,4 +645,264 @@ public class UiModeManager { } } + /** + * Indicates no projection type. Can be used to compare with the {@link ProjectionType} in + * {@link OnProjectionStateChangeListener#onProjectionStateChanged(int, Set)}. + * + * @hide + */ + @TestApi + public static final int PROJECTION_TYPE_NONE = 0x0000; + /** + * Automotive projection prevents degradation of GPS to save battery, routes incoming calls to + * the automotive role holder, etc. For use with {@link #requestProjection(int)} and + * {@link #clearProjectionState(int)}. + * + * @hide + */ + @TestApi + public static final int PROJECTION_TYPE_AUTOMOTIVE = 0x0001; + /** + * Indicates all projection types. For use with + * {@link #addOnProjectionStateChangeListener(int, Executor, OnProjectionStateChangeListener)} + * and {@link #getProjectingPackages(int)}. + * + * @hide + */ + @TestApi + public static final int PROJECTION_TYPE_ALL = 0xffff; + + /** @hide */ + @IntDef(prefix = {"PROJECTION_TYPE_"}, value = { + PROJECTION_TYPE_NONE, + PROJECTION_TYPE_AUTOMOTIVE, + PROJECTION_TYPE_ALL, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ProjectionType { + } + + /** + * Sets the given {@link ProjectionType}. + * + * Caller must have {@link android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION} if + * argument is {@link #PROJECTION_TYPE_AUTOMOTIVE}. + * @param projectionType the type of projection to request. This must be a single + * {@link ProjectionType} and cannot be a bitmask. + * @return true if the projection was successfully set + * @throws IllegalArgumentException if passed {@link #PROJECTION_TYPE_NONE}, + * {@link #PROJECTION_TYPE_ALL}, or any combination of more than one {@link ProjectionType}. + * + * @hide + */ + @TestApi + @RequiresPermission(value = android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, + conditional = true) + public boolean requestProjection(@ProjectionType int projectionType) { + if (mService != null) { + try { + return mService.requestProjection(new Binder(), projectionType, + mContext.getOpPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return false; + } + + /** + * Releases the given {@link ProjectionType}. + * + * Caller must have {@link android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION} if + * argument is {@link #PROJECTION_TYPE_AUTOMOTIVE}. + * @param projectionType the type of projection to release. This must be a single + * {@link ProjectionType} and cannot be a bitmask. + * @return true if the package had set projection and it was successfully released + * @throws IllegalArgumentException if passed {@link #PROJECTION_TYPE_NONE}, + * {@link #PROJECTION_TYPE_ALL}, or any combination of more than one {@link ProjectionType}. + * + * @hide + */ + @TestApi + @RequiresPermission(value = android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, + conditional = true) + public boolean releaseProjection(@ProjectionType int projectionType) { + if (mService != null) { + try { + return mService.releaseProjection(projectionType, mContext.getOpPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return false; + } + + /** + * Gets the packages that are currently projecting. + * + * @param projectionType the {@link ProjectionType}s to consider when computing which packages + * are projecting. Use {@link #PROJECTION_TYPE_ALL} to get all projecting + * packages. + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.READ_PROJECTION_STATE) + @NonNull + public Set getProjectingPackages(@ProjectionType int projectionType) { + if (mService != null) { + try { + return new ArraySet<>(mService.getProjectingPackages(projectionType)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return Set.of(); + } + + /** + * Gets the {@link ProjectionType}s that are currently active. + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.READ_PROJECTION_STATE) + public @ProjectionType int getActiveProjectionTypes() { + if (mService != null) { + try { + return mService.getActiveProjectionTypes(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return PROJECTION_TYPE_NONE; + } + + /** + * Configures the listener to receive callbacks when the packages projecting using the given + * {@link ProjectionType}s change. + * + * @param projectionType one or more {@link ProjectionType}s to listen for changes regarding + * @param executor an {@link Executor} on which to invoke the callbacks + * @param listener the {@link OnProjectionStateChangeListener} to add + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.READ_PROJECTION_STATE) + public void addOnProjectionStateChangeListener(@ProjectionType int projectionType, + @NonNull @CallbackExecutor Executor executor, + @NonNull OnProjectionStateChangeListener listener) { + synchronized (mLock) { + if (mProjectionStateListenerMap.containsKey(listener)) { + Slog.i(TAG, "Attempted to add listener that was already added."); + return; + } + if (mService != null) { + InnerListener innerListener = new InnerListener(executor, listener, + mOnProjectionStateChangeListenerResourceManager); + try { + mService.addOnProjectionStateChangeListener(innerListener, projectionType); + mProjectionStateListenerMap.put(listener, innerListener); + } catch (RemoteException e) { + mOnProjectionStateChangeListenerResourceManager.remove(innerListener); + throw e.rethrowFromSystemServer(); + } + } + } + } + + /** + * Removes the listener so it stops receiving updates for all {@link ProjectionType}s. + * + * @param listener the {@link OnProjectionStateChangeListener} to remove + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.READ_PROJECTION_STATE) + public void removeOnProjectionStateChangeListener( + @NonNull OnProjectionStateChangeListener listener) { + synchronized (mLock) { + InnerListener innerListener = mProjectionStateListenerMap.get(listener); + if (innerListener == null) { + Slog.i(TAG, "Attempted to remove listener that was not added."); + return; + } + if (mService != null) { + try { + mService.removeOnProjectionStateChangeListener(innerListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + mProjectionStateListenerMap.remove(listener); + mOnProjectionStateChangeListenerResourceManager.remove(innerListener); + } + } + + private static class InnerListener extends IOnProjectionStateChangeListener.Stub { + private final WeakReference + mResourceManager; + + private InnerListener(@NonNull Executor executor, + @NonNull OnProjectionStateChangeListener outerListener, + @NonNull OnProjectionStateChangeListenerResourceManager resourceManager) { + resourceManager.put(this, executor, outerListener); + mResourceManager = new WeakReference<>(resourceManager); + } + + @Override + public void onProjectionStateChanged(int activeProjectionTypes, + List projectingPackages) { + OnProjectionStateChangeListenerResourceManager resourceManager = mResourceManager.get(); + if (resourceManager == null) { + Slog.w(TAG, "Can't execute onProjectionStateChanged, resource manager is gone."); + return; + } + + OnProjectionStateChangeListener outerListener = resourceManager.getOuterListener(this); + Executor executor = resourceManager.getExecutor(this); + if (outerListener == null || executor == null) { + Slog.w(TAG, "Can't execute onProjectionStatechanged, references are null."); + return; + } + + executor.execute(PooledLambda.obtainRunnable( + OnProjectionStateChangeListener::onProjectionStateChanged, + outerListener, + activeProjectionTypes, + new ArraySet<>(projectingPackages)).recycleOnUse()); + } + } + + /** + * Wrapper class that ensures we don't leak {@link Activity} or other large {@link Context} in + * which this {@link UiModeManager} resides if/when it ends without unregistering associated + * {@link OnProjectionStateChangeListener}s. + */ + private static class OnProjectionStateChangeListenerResourceManager { + private final Map mOuterListenerMap = + new ArrayMap<>(1); + private final Map mExecutorMap = new ArrayMap<>(1); + + void put(@NonNull InnerListener innerListener, @NonNull Executor executor, + OnProjectionStateChangeListener outerListener) { + mOuterListenerMap.put(innerListener, outerListener); + mExecutorMap.put(innerListener, executor); + } + + void remove(InnerListener innerListener) { + mOuterListenerMap.remove(innerListener); + mExecutorMap.remove(innerListener); + } + + OnProjectionStateChangeListener getOuterListener(@NonNull InnerListener innerListener) { + return mOuterListenerMap.get(innerListener); + } + + Executor getExecutor(@NonNull InnerListener innerListener) { + return mExecutorMap.get(innerListener); + } + } } diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto index 76b7fc028a980..0e2bd26058369 100644 --- a/core/proto/android/server/jobscheduler.proto +++ b/core/proto/android/server/jobscheduler.proto @@ -527,7 +527,8 @@ message StateControllerProto { optional bool is_idle = 1; optional bool is_screen_on = 2; optional bool is_dock_idle = 3; - optional bool in_car_mode = 4; + reserved 4; // in_car_mode + optional bool projection_active = 5; } oneof active_tracker { diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index a7cf0cfc75cf9..15507632f4610 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2674,6 +2674,21 @@ + + + + + diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 4c3b36f78eec8..42ebfc5cd00f1 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -164,7 +164,6 @@ applications that come with the platform - @@ -175,6 +174,7 @@ applications that come with the platform + @@ -376,6 +376,8 @@ applications that come with the platform + + diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index ec47c717ac3cf..20b4451ed2926 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -250,6 +250,9 @@ + + + diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index 7051452f25cc7..f49f1b15c93a0 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -20,6 +20,8 @@ import static android.app.UiModeManager.DEFAULT_PRIORITY; import static android.app.UiModeManager.MODE_NIGHT_AUTO; import static android.app.UiModeManager.MODE_NIGHT_CUSTOM; import static android.app.UiModeManager.MODE_NIGHT_YES; +import static android.app.UiModeManager.PROJECTION_TYPE_AUTOMOTIVE; +import static android.app.UiModeManager.PROJECTION_TYPE_NONE; import static android.os.UserHandle.USER_SYSTEM; import static android.util.TimeUtils.isTimeBetween; @@ -30,6 +32,7 @@ import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.AlarmManager; +import android.app.IOnProjectionStateChangeListener; import android.app.IUiModeManager; import android.app.Notification; import android.app.NotificationManager; @@ -48,10 +51,12 @@ import android.net.Uri; import android.os.BatteryManager; import android.os.Binder; import android.os.Handler; +import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManager.ServiceType; import android.os.PowerManagerInternal; import android.os.Process; +import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; @@ -65,8 +70,10 @@ import android.service.vr.IVrManager; import android.service.vr.IVrStateCallbacks; import android.util.ArraySet; import android.util.Slog; +import android.util.SparseArray; import com.android.internal.R; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.DisableCarModeActivity; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; @@ -83,7 +90,9 @@ import java.time.DateTimeException; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -97,7 +106,9 @@ final class UiModeManagerService extends SystemService { private static final boolean ENABLE_LAUNCH_DESK_DOCK_APP = true; private static final String SYSTEM_PROPERTY_DEVICE_THEME = "persist.sys.theme"; - final Object mLock = new Object(); + private final Injector mInjector; + private final Object mLock = new Object(); + private int mDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED; private int mLastBroadcastState = Intent.EXTRA_DOCK_STATE_UNDOCKED; @@ -162,17 +173,25 @@ final class UiModeManagerService extends SystemService { private final LocalService mLocalService = new LocalService(); private PowerManagerInternal mLocalPowerManager; + @GuardedBy("mLock") + @Nullable + private SparseArray> mProjectionHolders; + @GuardedBy("mLock") + @Nullable + private SparseArray> mProjectionListeners; + public UiModeManagerService(Context context) { - super(context); - mConfiguration.setToDefaults(); + this(context, /* setupWizardComplete= */ false, /* tm= */ null, new Injector()); } @VisibleForTesting protected UiModeManagerService(Context context, boolean setupWizardComplete, - TwilightManager tm) { - this(context); + TwilightManager tm, Injector injector) { + super(context); + mConfiguration.setToDefaults(); mSetupWizardComplete = setupWizardComplete; mTwilightManager = tm; + mInjector = injector; } private static Intent buildHomeIntent(String category) { @@ -639,7 +658,7 @@ final class UiModeManagerService extends SystemService { // If the caller is the system, we will allow the DISABLE_CAR_MODE_ALL_PRIORITIES car // mode flag to be specified; this is so that the user can disable car mode at all // priorities using the persistent notification. - boolean isSystemCaller = Binder.getCallingUid() == Process.SYSTEM_UID; + boolean isSystemCaller = mInjector.getCallingUid() == Process.SYSTEM_UID; final int carModeFlags = isSystemCaller ? flags : flags & ~UiModeManager.DISABLE_CAR_MODE_ALL_PRIORITIES; @@ -853,8 +872,308 @@ final class UiModeManagerService extends SystemService { Binder.restoreCallingIdentity(ident); } } + + @Override + public boolean requestProjection(IBinder binder, + @UiModeManager.ProjectionType int projectionType, + @NonNull String callingPackage) { + assertLegit(callingPackage); + assertSingleProjectionType(projectionType); + enforceProjectionTypePermissions(projectionType); + synchronized (mLock) { + if (mProjectionHolders == null) { + mProjectionHolders = new SparseArray<>(1); + } + if (!mProjectionHolders.contains(projectionType)) { + mProjectionHolders.put(projectionType, new ArrayList<>(1)); + } + List currentHolders = mProjectionHolders.get(projectionType); + + // For all projection types, it's a noop if already held. + for (int i = 0; i < currentHolders.size(); ++i) { + if (callingPackage.equals(currentHolders.get(i).mPackageName)) { + return true; + } + } + + // Enforce projection type-specific restrictions here. + + // Automotive projection can only be set if it is currently unset. The case where it + // is already set by the calling package is taken care of above. + if (projectionType == PROJECTION_TYPE_AUTOMOTIVE && !currentHolders.isEmpty()) { + return false; + } + + ProjectionHolder projectionHolder = new ProjectionHolder(callingPackage, + projectionType, binder, + UiModeManagerService.this::releaseProjectionUnchecked); + if (!projectionHolder.linkToDeath()) { + return false; + } + currentHolders.add(projectionHolder); + Slog.d(TAG, "Package " + callingPackage + " set projection type " + + projectionType + "."); + onProjectionStateChangedLocked(projectionType); + } + return true; + } + + @Override + public boolean releaseProjection(@UiModeManager.ProjectionType int projectionType, + @NonNull String callingPackage) { + assertLegit(callingPackage); + assertSingleProjectionType(projectionType); + enforceProjectionTypePermissions(projectionType); + return releaseProjectionUnchecked(projectionType, callingPackage); + } + + @Override + public @UiModeManager.ProjectionType int getActiveProjectionTypes() { + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.READ_PROJECTION_STATE, "getActiveProjectionTypes"); + @UiModeManager.ProjectionType int projectionTypeFlag = PROJECTION_TYPE_NONE; + synchronized (mLock) { + if (mProjectionHolders != null) { + for (int i = 0; i < mProjectionHolders.size(); ++i) { + if (!mProjectionHolders.valueAt(i).isEmpty()) { + projectionTypeFlag = projectionTypeFlag | mProjectionHolders.keyAt(i); + } + } + } + } + return projectionTypeFlag; + } + + @Override + public List getProjectingPackages( + @UiModeManager.ProjectionType int projectionType) { + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.READ_PROJECTION_STATE, "getProjectionState"); + synchronized (mLock) { + List packageNames = new ArrayList<>(); + populateWithRelevantActivePackageNames(projectionType, packageNames); + return packageNames; + } + } + + public void addOnProjectionStateChangeListener(IOnProjectionStateChangeListener listener, + @UiModeManager.ProjectionType int projectionType) { + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.READ_PROJECTION_STATE, + "registerProjectionStateListener"); + if (projectionType == PROJECTION_TYPE_NONE) { + return; + } + synchronized (mLock) { + if (mProjectionListeners == null) { + mProjectionListeners = new SparseArray<>(1); + } + if (!mProjectionListeners.contains(projectionType)) { + mProjectionListeners.put(projectionType, new RemoteCallbackList<>()); + } + if (mProjectionListeners.get(projectionType).register(listener)) { + // If any of those types are active, send a callback immediately. + List packageNames = new ArrayList<>(); + @UiModeManager.ProjectionType int activeProjectionTypes = + populateWithRelevantActivePackageNames(projectionType, packageNames); + if (!packageNames.isEmpty()) { + try { + listener.onProjectionStateChanged(activeProjectionTypes, packageNames); + } catch (RemoteException e) { + Slog.w(TAG, + "Failed a call to onProjectionStateChanged() during listener " + + "registration."); + } + } + } + } + } + + + public void removeOnProjectionStateChangeListener( + IOnProjectionStateChangeListener listener) { + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.READ_PROJECTION_STATE, + "unregisterProjectionStateListener"); + synchronized (mLock) { + if (mProjectionListeners != null) { + for (int i = 0; i < mProjectionListeners.size(); ++i) { + mProjectionListeners.valueAt(i).unregister(listener); + } + } + } + } }; + private void enforceProjectionTypePermissions(@UiModeManager.ProjectionType int p) { + if ((p & PROJECTION_TYPE_AUTOMOTIVE) != 0) { + getContext().enforceCallingPermission( + android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, + "toggleProjection"); + } + } + + private static void assertSingleProjectionType(@UiModeManager.ProjectionType int p) { + // To be a single projection type it must be greater than zero and an exact power of two. + boolean projectionTypeIsPowerOfTwoOrZero = (p & p - 1) == 0; + if (p <= 0 || !projectionTypeIsPowerOfTwoOrZero) { + throw new IllegalArgumentException("Must specify exactly one projection type."); + } + } + + private static List toPackageNameList(Collection c) { + List packageNames = new ArrayList<>(); + for (ProjectionHolder p : c) { + packageNames.add(p.mPackageName); + } + return packageNames; + } + + /** + * Populates a list with the package names that have set any of the given projection types. + * @param projectionType the projection types to include + * @param packageNames the list to populate with package names + * @return the active projection types + */ + @GuardedBy("mLock") + @UiModeManager.ProjectionType + private int populateWithRelevantActivePackageNames( + @UiModeManager.ProjectionType int projectionType, List packageNames) { + packageNames.clear(); + @UiModeManager.ProjectionType int projectionTypeFlag = PROJECTION_TYPE_NONE; + if (mProjectionHolders != null) { + for (int i = 0; i < mProjectionHolders.size(); ++i) { + int key = mProjectionHolders.keyAt(i); + List holders = mProjectionHolders.valueAt(i); + if ((projectionType & key) != 0) { + if (packageNames.addAll(toPackageNameList(holders))) { + projectionTypeFlag = projectionTypeFlag | key; + } + } + } + } + return projectionTypeFlag; + } + + private boolean releaseProjectionUnchecked(@UiModeManager.ProjectionType int projectionType, + @NonNull String pkg) { + synchronized (mLock) { + boolean removed = false; + if (mProjectionHolders != null) { + List holders = mProjectionHolders.get(projectionType); + if (holders != null) { + // Iterate backward so we can safely remove while iterating. + for (int i = holders.size() - 1; i >= 0; --i) { + ProjectionHolder holder = holders.get(i); + if (pkg.equals(holder.mPackageName)) { + holder.unlinkToDeath(); + Slog.d(TAG, "Projection type " + projectionType + " released by " + + pkg + "."); + holders.remove(i); + removed = true; + } + } + } + } + if (removed) { + onProjectionStateChangedLocked(projectionType); + } else { + Slog.w(TAG, pkg + " tried to release projection type " + projectionType + + " but was not set by that package."); + } + return removed; + } + } + + private static class ProjectionHolder implements IBinder.DeathRecipient { + private final String mPackageName; + private final @UiModeManager.ProjectionType int mProjectionType; + private final IBinder mBinder; + private final ProjectionReleaser mProjectionReleaser; + + private ProjectionHolder(String packageName, + @UiModeManager.ProjectionType int projectionType, IBinder binder, + ProjectionReleaser projectionReleaser) { + mPackageName = packageName; + mProjectionType = projectionType; + mBinder = binder; + mProjectionReleaser = projectionReleaser; + } + + private boolean linkToDeath() { + try { + mBinder.linkToDeath(this, 0); + } catch (RemoteException e) { + Slog.e(TAG, "linkToDeath failed for projection requester: " + mPackageName + ".", + e); + return false; + } + return true; + } + + private void unlinkToDeath() { + mBinder.unlinkToDeath(this, 0); + } + + @Override + public void binderDied() { + Slog.w(TAG, "Projection holder " + mPackageName + + " died. Releasing projection type " + mProjectionType + "."); + mProjectionReleaser.release(mProjectionType, mPackageName); + } + + private interface ProjectionReleaser { + boolean release(@UiModeManager.ProjectionType int projectionType, + @NonNull String packageName); + } + } + + private void assertLegit(@NonNull String packageName) { + if (!doesPackageHaveCallingUid(packageName)) { + throw new SecurityException("Caller claimed bogus packageName: " + packageName + "."); + } + } + + private boolean doesPackageHaveCallingUid(@NonNull String packageName) { + try { + return getContext().getPackageManager().getPackageUid(packageName, 0) + == mInjector.getCallingUid(); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + @GuardedBy("mLock") + private void onProjectionStateChangedLocked( + @UiModeManager.ProjectionType int changedProjectionType) { + if (mProjectionListeners == null) { + return; + } + for (int i = 0; i < mProjectionListeners.size(); ++i) { + int listenerProjectionType = mProjectionListeners.keyAt(i); + // Every listener that is affected must be called back with all the state they are + // listening for. + if ((changedProjectionType & listenerProjectionType) != 0) { + RemoteCallbackList listeners = + mProjectionListeners.valueAt(i); + List packageNames = new ArrayList<>(); + @UiModeManager.ProjectionType int activeProjectionTypes = + populateWithRelevantActivePackageNames(listenerProjectionType, + packageNames); + int listenerCount = listeners.beginBroadcast(); + for (int j = 0; j < listenerCount; ++j) { + try { + listeners.getBroadcastItem(j).onProjectionStateChanged( + activeProjectionTypes, packageNames); + } catch (RemoteException e) { + Slog.w(TAG, "Failed a call to onProjectionStateChanged()."); + } + } + listeners.finishBroadcast(); + } + } + } + private void onCustomTimeUpdated(int user) { persistNightMode(user); if (mNightMode != MODE_NIGHT_CUSTOM) return; @@ -1656,4 +1975,11 @@ final class UiModeManagerService extends SystemService { } } } + + @VisibleForTesting + public static class Injector { + public int getCallingUid() { + return Binder.getCallingUid(); + } + } } diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java index 701197e690fc4..1883f4e4e86c1 100644 --- a/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java +++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java @@ -17,12 +17,8 @@ package com.android.server.power.batterysaver; import android.annotation.IntDef; import android.app.UiModeManager; -import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Configuration; import android.database.ContentObserver; import android.net.Uri; import android.os.BatterySaverPolicyConfig; @@ -190,12 +186,12 @@ public class BatterySaverPolicy extends ContentObserver { /** * Whether accessibility is currently enabled or not. */ - @GuardedBy("mLock") - private boolean mAccessibilityEnabled; + @VisibleForTesting + final PolicyBoolean mAccessibilityEnabled = new PolicyBoolean("accessibility"); - /** Whether the phone is projecting in car mode or not. */ - @GuardedBy("mLock") - private boolean mCarModeEnabled; + /** Whether the phone has set automotive projection or not. */ + @VisibleForTesting + final PolicyBoolean mAutomotiveProjectionActive = new PolicyBoolean("automotiveProjection"); /** The current default adaptive policy. */ @GuardedBy("mLock") @@ -235,19 +231,8 @@ public class BatterySaverPolicy extends ContentObserver { private final ContentResolver mContentResolver; private final BatterySavingStats mBatterySavingStats; - private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - switch (intent.getAction()) { - case UiModeManager.ACTION_ENTER_CAR_MODE_PRIORITIZED: - setCarModeEnabled(true); - break; - case UiModeManager.ACTION_EXIT_CAR_MODE_PRIORITIZED: - setCarModeEnabled(false); - break; - } - } - }; + private final UiModeManager.OnProjectionStateChangeListener mOnProjectionStateChangeListener = + (t, pkgs) -> mAutomotiveProjectionActive.update(!pkgs.isEmpty()); @GuardedBy("mLock") private final List mListeners = new ArrayList<>(); @@ -282,24 +267,14 @@ public class BatterySaverPolicy extends ContentObserver { final AccessibilityManager acm = mContext.getSystemService(AccessibilityManager.class); - acm.addAccessibilityStateChangeListener((enabled) -> setAccessibilityEnabled(enabled)); - final boolean accessibilityEnabled = acm.isEnabled(); - synchronized (mLock) { - mAccessibilityEnabled = accessibilityEnabled; - } + acm.addAccessibilityStateChangeListener(enabled -> mAccessibilityEnabled.update(enabled)); + mAccessibilityEnabled.initialize(acm.isEnabled()); - final IntentFilter filter = new IntentFilter( - UiModeManager.ACTION_ENTER_CAR_MODE_PRIORITIZED); - filter.addAction(UiModeManager.ACTION_EXIT_CAR_MODE_PRIORITIZED); - // The ENTER/EXIT_CAR_MODE_PRIORITIZED intents are sent to UserHandle.ALL, so no need to - // register as all users here. - mContext.registerReceiver(mBroadcastReceiver, filter); - final boolean carModeEnabled = - mContext.getSystemService(UiModeManager.class).getCurrentModeType() - == Configuration.UI_MODE_TYPE_CAR; - synchronized (mLock) { - mCarModeEnabled = carModeEnabled; - } + UiModeManager uiModeManager = mContext.getSystemService(UiModeManager.class); + uiModeManager.addOnProjectionStateChangeListener(UiModeManager.PROJECTION_TYPE_AUTOMOTIVE, + mContext.getMainExecutor(), mOnProjectionStateChangeListener); + mAutomotiveProjectionActive.initialize( + uiModeManager.getActiveProjectionTypes() != UiModeManager.PROJECTION_TYPE_NONE); onChange(true, null); } @@ -450,7 +425,7 @@ public class BatterySaverPolicy extends ContentObserver { final int locationMode; invalidatePowerSaveModeCaches(); - if (mCarModeEnabled + if (mAutomotiveProjectionActive.get() && rawPolicy.locationMode != PowerManager.LOCATION_MODE_NO_CHANGE && rawPolicy.locationMode != PowerManager.LOCATION_MODE_FOREGROUND_ONLY) { // If car projection is enabled, ensure that navigation works. @@ -470,12 +445,12 @@ public class BatterySaverPolicy extends ContentObserver { rawPolicy.disableOptionalSensors, rawPolicy.disableSoundTrigger, // Don't disable vibration when accessibility is on. - rawPolicy.disableVibration && !mAccessibilityEnabled, + rawPolicy.disableVibration && !mAccessibilityEnabled.get(), rawPolicy.enableAdjustBrightness, rawPolicy.enableDataSaver, rawPolicy.enableFirewall, // Don't force night mode when car projection is enabled. - rawPolicy.enableNightMode && !mCarModeEnabled, + rawPolicy.enableNightMode && !mAutomotiveProjectionActive.get(), rawPolicy.enableQuickDoze, rawPolicy.filesForInteractive, rawPolicy.filesForNoninteractive, @@ -1073,8 +1048,8 @@ public class BatterySaverPolicy extends ContentObserver { + Settings.Global.BATTERY_SAVER_ADAPTIVE_DEVICE_SPECIFIC_CONSTANTS); pw.println(" value: " + mAdaptiveDeviceSpecificSettings); - pw.println(" mAccessibilityEnabled=" + mAccessibilityEnabled); - pw.println(" mCarModeEnabled=" + mCarModeEnabled); + pw.println(" mAccessibilityEnabled=" + mAccessibilityEnabled.get()); + pw.println(" mAutomotiveProjectionActive=" + mAutomotiveProjectionActive.get()); pw.println(" mPolicyLevel=" + mPolicyLevel); dumpPolicyLocked(pw, " ", "full", mFullPolicy); @@ -1147,24 +1122,42 @@ public class BatterySaverPolicy extends ContentObserver { } } + /** + * A boolean value which should trigger a policy update when it changes. + */ @VisibleForTesting - void setAccessibilityEnabled(boolean enabled) { - synchronized (mLock) { - if (mAccessibilityEnabled != enabled) { - mAccessibilityEnabled = enabled; - updatePolicyDependenciesLocked(); - maybeNotifyListenersOfPolicyChange(); + class PolicyBoolean { + private final String mDebugName; + @GuardedBy("mLock") + private boolean mValue; + + private PolicyBoolean(String debugName) { + mDebugName = debugName; + } + + /** Sets the initial value without triggering a policy update. */ + private void initialize(boolean initialValue) { + synchronized (mLock) { + mValue = initialValue; } } - } - @VisibleForTesting - void setCarModeEnabled(boolean enabled) { - synchronized (mLock) { - if (mCarModeEnabled != enabled) { - mCarModeEnabled = enabled; - updatePolicyDependenciesLocked(); - maybeNotifyListenersOfPolicyChange(); + private boolean get() { + synchronized (mLock) { + return mValue; + } + } + + /** Sets a value, which if different from the current value, triggers a policy update. */ + @VisibleForTesting + void update(boolean newValue) { + synchronized (mLock) { + if (mValue != newValue) { + Slog.d(TAG, mDebugName + " changed to " + newValue + ", updating policy."); + mValue = newValue; + updatePolicyDependenciesLocked(); + maybeNotifyListenersOfPolicyChange(); + } } } } diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java index 043ca9e028732..dcbf8c02edf93 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java @@ -38,6 +38,7 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.IActivityManager; +import android.app.UiModeManager; import android.app.job.JobInfo; import android.app.job.JobScheduler; import android.app.usage.UsageStatsManagerInternal; @@ -147,6 +148,8 @@ public class JobSchedulerServiceTest { JobSchedulerService.sSystemClock = Clock.fixed(Clock.systemUTC().instant(), ZoneOffset.UTC); JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC); + // Called by DeviceIdlenessTracker + when(mContext.getSystemService(UiModeManager.class)).thenReturn(mock(UiModeManager.class)); mService = new TestJobSchedulerService(mContext); } diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml index 6ead95ca03b61..b9aa554aa0acc 100644 --- a/services/tests/servicestests/AndroidManifest.xml +++ b/services/tests/servicestests/AndroidManifest.xml @@ -86,6 +86,7 @@ + mService.requestProjection(mBinder, + PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); + assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); + } + + @Test + public void requestProjection_failsIfNameNotFound() throws Exception { + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)) + .thenThrow(new PackageManager.NameNotFoundException()); + + assertThrows(SecurityException.class, () -> mService.requestProjection(mBinder, + PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); + assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); + } + + @Test + public void requestProjection_failsIfNoProjectionTypes() throws Exception { + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + + assertThrows(IllegalArgumentException.class, + () -> mService.requestProjection(mBinder, PROJECTION_TYPE_NONE, PACKAGE_NAME)); + verify(mContext, never()).enforceCallingPermission( + eq(Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION), any()); + verifyZeroInteractions(mBinder); + assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); + } + + @Test + public void requestProjection_failsIfMultipleProjectionTypes() throws Exception { + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + + // Don't use PROJECTION_TYPE_ALL because that's actually == -1 and will fail the > 0 check. + int multipleProjectionTypes = PROJECTION_TYPE_AUTOMOTIVE | 0x0002 | 0x0004; + + assertThrows(IllegalArgumentException.class, + () -> mService.requestProjection(mBinder, multipleProjectionTypes, PACKAGE_NAME)); + verify(mContext, never()).enforceCallingPermission( + eq(Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION), any()); + verifyZeroInteractions(mBinder); + assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); + } + + @Test + public void requestProjection_enforcesToggleAutomotiveProjectionPermission() throws Exception { + doThrow(new SecurityException()).when(mPackageManager).getPackageUid(PACKAGE_NAME, 0); + + assertThrows(SecurityException.class, () -> mService.requestProjection(mBinder, + PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); + assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); + } + + @Test + public void requestProjection_automotive_failsIfAlreadySetByOtherPackage() throws Exception { + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); + assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); + + String otherPackage = "Raconteurs"; + when(mPackageManager.getPackageUid(otherPackage, 0)).thenReturn(TestInjector.CALLING_UID); + assertFalse(mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, otherPackage)); + assertThat(mService.getProjectingPackages(PROJECTION_TYPE_AUTOMOTIVE), + contains(PACKAGE_NAME)); + } + + @Test + public void requestProjection_failsIfCannotLinkToDeath() throws Exception { + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + doThrow(new RemoteException()).when(mBinder).linkToDeath(any(), anyInt()); + + assertFalse(mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); + assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); + } + + @Test + public void requestProjection() throws Exception { + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + // Should work for all powers of two. + for (int p = 1; p < PROJECTION_TYPE_ALL; p = p * 2) { + assertTrue(mService.requestProjection(mBinder, p, PACKAGE_NAME)); + assertTrue((mService.getActiveProjectionTypes() & p) != 0); + assertThat(mService.getProjectingPackages(p), contains(PACKAGE_NAME)); + // Subsequent calls should still succeed. + assertTrue(mService.requestProjection(mBinder, p, PACKAGE_NAME)); + } + assertEquals(PROJECTION_TYPE_ALL, mService.getActiveProjectionTypes()); + } + + @Test + public void releaseProjection_failsForBogusPackageName() throws Exception { + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); + assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); + + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)) + .thenReturn(TestInjector.CALLING_UID + 1); + + assertThrows(SecurityException.class, () -> mService.releaseProjection( + PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); + assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); + } + + @Test + public void releaseProjection_failsIfNameNotFound() throws Exception { + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); + assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)) + .thenThrow(new PackageManager.NameNotFoundException()); + + assertThrows(SecurityException.class, () -> mService.releaseProjection( + PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); + assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); + } + + @Test + public void releaseProjection_enforcesToggleAutomotiveProjectionPermission() throws Exception { + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); + assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); + doThrow(new SecurityException()).when(mContext).enforceCallingPermission( + eq(Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION), any()); + + // Should not be enforced for other types of projection. + int nonAutomotiveProjectionType = PROJECTION_TYPE_AUTOMOTIVE * 2; + mService.releaseProjection(nonAutomotiveProjectionType, PACKAGE_NAME); + assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); + + assertThrows(SecurityException.class, () -> mService.requestProjection(mBinder, + PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); + assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); + } + + @Test + public void releaseProjection() throws Exception { + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + // Should work for all powers of two. + for (int p = 1; p < PROJECTION_TYPE_ALL; p = p * 2) { + mService.requestProjection(mBinder, p, PACKAGE_NAME); + } + assertEquals(PROJECTION_TYPE_ALL, mService.getActiveProjectionTypes()); + + assertTrue(mService.releaseProjection(PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME)); + int everythingButAutomotive = PROJECTION_TYPE_ALL & ~PROJECTION_TYPE_AUTOMOTIVE; + assertEquals(everythingButAutomotive, mService.getActiveProjectionTypes()); + + for (int p = 1; p < PROJECTION_TYPE_ALL; p = p * 2) { + assertEquals(p != PROJECTION_TYPE_AUTOMOTIVE, + (boolean) mService.releaseProjection(p, PACKAGE_NAME)); + } + + assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); + } + + @Test + public void binderDeath_releasesProjection() throws Exception { + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + for (int p = 1; p < PROJECTION_TYPE_ALL; p = p * 2) { + mService.requestProjection(mBinder, p, PACKAGE_NAME); + } + assertEquals(PROJECTION_TYPE_ALL, mService.getActiveProjectionTypes()); + ArgumentCaptor deathRecipientCaptor = ArgumentCaptor.forClass( + IBinder.DeathRecipient.class); + verify(mBinder, atLeastOnce()).linkToDeath(deathRecipientCaptor.capture(), anyInt()); + + // Wipe them out. All of them. + deathRecipientCaptor.getAllValues().forEach(IBinder.DeathRecipient::binderDied); + assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); + } + + @Test + public void getActiveProjectionTypes() throws Exception { + assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); + assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); + mService.releaseProjection(PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); + assertEquals(PROJECTION_TYPE_NONE, mService.getActiveProjectionTypes()); + } + + @Test + public void getProjectingPackages() throws Exception { + assertTrue(mService.getProjectingPackages(PROJECTION_TYPE_ALL).isEmpty()); + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); + assertEquals(1, mService.getProjectingPackages(PROJECTION_TYPE_AUTOMOTIVE).size()); + assertEquals(1, mService.getProjectingPackages(PROJECTION_TYPE_ALL).size()); + assertThat(mService.getProjectingPackages(PROJECTION_TYPE_AUTOMOTIVE), + contains(PACKAGE_NAME)); + assertThat(mService.getProjectingPackages(PROJECTION_TYPE_ALL), contains(PACKAGE_NAME)); + mService.releaseProjection(PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); + assertThat(mService.getProjectingPackages(PROJECTION_TYPE_ALL), empty()); + } + + @Test + public void addOnProjectionStateChangeListener_enforcesReadProjStatePermission() { + doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission( + eq(android.Manifest.permission.READ_PROJECTION_STATE), any()); + IOnProjectionStateChangeListener listener = mock(IOnProjectionStateChangeListener.class); + + assertThrows(SecurityException.class, () -> mService.addOnProjectionStateChangeListener( + listener, PROJECTION_TYPE_ALL)); + } + + @Test + public void addOnProjectionStateChangeListener_callsListenerIfProjectionActive() + throws Exception { + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); + assertEquals(PROJECTION_TYPE_AUTOMOTIVE, mService.getActiveProjectionTypes()); + + IOnProjectionStateChangeListener listener = mock(IOnProjectionStateChangeListener.class); + when(listener.asBinder()).thenReturn(mBinder); // Any binder will do + mService.addOnProjectionStateChangeListener(listener, PROJECTION_TYPE_ALL); + verify(listener).onProjectionStateChanged(eq(PROJECTION_TYPE_AUTOMOTIVE), + eq(List.of(PACKAGE_NAME))); + } + + @Test + public void removeOnProjectionStateChangeListener_enforcesReadProjStatePermission() { + doThrow(new SecurityException()).when(mContext).enforceCallingOrSelfPermission( + eq(android.Manifest.permission.READ_PROJECTION_STATE), any()); + IOnProjectionStateChangeListener listener = mock(IOnProjectionStateChangeListener.class); + + assertThrows(SecurityException.class, () -> mService.removeOnProjectionStateChangeListener( + listener)); + } + + @Test + public void removeOnProjectionStateChangeListener() throws Exception { + IOnProjectionStateChangeListener listener = mock(IOnProjectionStateChangeListener.class); + when(listener.asBinder()).thenReturn(mBinder); // Any binder will do. + mService.addOnProjectionStateChangeListener(listener, PROJECTION_TYPE_ALL); + + mService.removeOnProjectionStateChangeListener(listener); + // Now set automotive projection, should not call back. + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); + verify(listener, never()).onProjectionStateChanged(anyInt(), any()); + } + + @Test + public void projectionStateChangeListener_calledWhenStateChanges() throws Exception { + IOnProjectionStateChangeListener listener = mock(IOnProjectionStateChangeListener.class); + when(listener.asBinder()).thenReturn(mBinder); // Any binder will do. + mService.addOnProjectionStateChangeListener(listener, PROJECTION_TYPE_ALL); + verify(listener, atLeastOnce()).asBinder(); // Called twice during register. + + // No calls initially, no projection state set. + verifyNoMoreInteractions(listener); + + // Now set automotive projection, should call back. + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); + verify(listener).onProjectionStateChanged(eq(PROJECTION_TYPE_AUTOMOTIVE), + eq(List.of(PACKAGE_NAME))); + + // Subsequent calls that are noops do nothing. + mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); + int unsetProjectionType = 0x0002; + mService.releaseProjection(unsetProjectionType, PACKAGE_NAME); + verifyNoMoreInteractions(listener); + + // Release should call back though. + mService.releaseProjection(PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); + verify(listener).onProjectionStateChanged(eq(PROJECTION_TYPE_NONE), + eq(List.of())); + + // But only the first time. + mService.releaseProjection(PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); + verifyNoMoreInteractions(listener); + } + + @Test + public void projectionStateChangeListener_calledForAnyRelevantStateChange() throws Exception { + int fakeProjectionType = 0x0002; + int otherFakeProjectionType = 0x0004; + String otherPackageName = "Internet Arms"; + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + when(mPackageManager.getPackageUid(otherPackageName, 0)) + .thenReturn(TestInjector.CALLING_UID); + IOnProjectionStateChangeListener listener = mock(IOnProjectionStateChangeListener.class); + when(listener.asBinder()).thenReturn(mBinder); // Any binder will do. + IOnProjectionStateChangeListener listener2 = mock(IOnProjectionStateChangeListener.class); + when(listener2.asBinder()).thenReturn(mBinder); // Any binder will do. + mService.addOnProjectionStateChangeListener(listener, fakeProjectionType); + mService.addOnProjectionStateChangeListener(listener2, + fakeProjectionType | otherFakeProjectionType); + verify(listener, atLeastOnce()).asBinder(); // Called twice during register. + verify(listener2, atLeastOnce()).asBinder(); // Called twice during register. + + mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); + verifyNoMoreInteractions(listener, listener2); + + // fakeProjectionType should trigger both. + mService.requestProjection(mBinder, fakeProjectionType, PACKAGE_NAME); + verify(listener).onProjectionStateChanged(eq(fakeProjectionType), + eq(List.of(PACKAGE_NAME))); + verify(listener2).onProjectionStateChanged(eq(fakeProjectionType), + eq(List.of(PACKAGE_NAME))); + + // otherFakeProjectionType should only trigger the second listener. + mService.requestProjection(mBinder, otherFakeProjectionType, otherPackageName); + verifyNoMoreInteractions(listener); + verify(listener2).onProjectionStateChanged( + eq(fakeProjectionType | otherFakeProjectionType), + eq(List.of(PACKAGE_NAME, otherPackageName))); + + // Turning off fakeProjectionType should trigger both again. + mService.releaseProjection(fakeProjectionType, PACKAGE_NAME); + verify(listener).onProjectionStateChanged(eq(PROJECTION_TYPE_NONE), eq(List.of())); + verify(listener2).onProjectionStateChanged(eq(otherFakeProjectionType), + eq(List.of(otherPackageName))); + + // Turning off otherFakeProjectionType should only trigger the second listener. + mService.releaseProjection(otherFakeProjectionType, otherPackageName); + verifyNoMoreInteractions(listener); + verify(listener2).onProjectionStateChanged(eq(PROJECTION_TYPE_NONE), eq(List.of())); + } + + @Test + public void projectionStateChangeListener_unregisteredOnDeath() throws Exception { + IOnProjectionStateChangeListener listener = mock(IOnProjectionStateChangeListener.class); + IBinder listenerBinder = mock(IBinder.class); + when(listener.asBinder()).thenReturn(listenerBinder); + mService.addOnProjectionStateChangeListener(listener, PROJECTION_TYPE_ALL); + ArgumentCaptor listenerDeathRecipient = ArgumentCaptor.forClass( + IBinder.DeathRecipient.class); + verify(listenerBinder).linkToDeath(listenerDeathRecipient.capture(), anyInt()); + + // Now kill the binder for the listener. This should remove it from the list of listeners. + listenerDeathRecipient.getValue().binderDied(); + when(mPackageManager.getPackageUid(PACKAGE_NAME, 0)).thenReturn(TestInjector.CALLING_UID); + mService.requestProjection(mBinder, PROJECTION_TYPE_AUTOMOTIVE, PACKAGE_NAME); + verify(listener, never()).onProjectionStateChanged(anyInt(), any()); + } + + private static class TestInjector extends UiModeManagerService.Injector { + private static final int CALLING_UID = 8675309; + + public int getCallingUid() { + return CALLING_UID; + } + } }