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
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 e2c8f64..3c4961a 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.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 @@
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 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 @@
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<String> 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 @@
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 @@
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 @@
}
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 @@
}
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 @@
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 @@
} 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 b28e7af..d4dafe7 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -182,6 +182,7 @@
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 @@
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 @@
}
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<java.lang.String> 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 @@
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<java.lang.String>);
+ }
+
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 8b15ec3..ac4b369 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -182,6 +182,7 @@
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 @@
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 @@
}
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<java.lang.String> 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 @@
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<java.lang.String>);
+ }
+
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 67b496b..1fdb3e4a6 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -308,6 +308,11 @@
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 0000000..f154985
--- /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<String> projectingPackages);
+}
\ No newline at end of file
diff --git a/core/java/android/app/IUiModeManager.aidl b/core/java/android/app/IUiModeManager.aidl
index 41e2ec9..0ba5bec 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 @@
* 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<String> 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 e2fc5dbf..8a6871f 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.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.
- *
+ *
* <p>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 @@
* starts the corresponding mode activity if appropriate. See the
* broadcasts {@link #ACTION_ENTER_CAR_MODE} and
* {@link #ACTION_ENTER_DESK_MODE} for more information.
- *
+ *
* <p>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 @@
*/
@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<String> packageNames);
+ }
+
private static final String TAG = "UiModeManager";
/**
@@ -100,7 +132,7 @@
@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 @@
@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 @@
* 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 @@
* 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 @@
*/
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<OnProjectionStateChangeListener, InnerListener>
+ 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 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 @@
*/
@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 @@
}
}
+ /**
+ * 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<String> 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<OnProjectionStateChangeListenerResourceManager>
+ 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<String> 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<InnerListener, OnProjectionStateChangeListener> mOuterListenerMap =
+ new ArrayMap<>(1);
+ private final Map<InnerListener, Executor> 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 76b7fc0..0e2bd26 100644
--- a/core/proto/android/server/jobscheduler.proto
+++ b/core/proto/android/server/jobscheduler.proto
@@ -527,7 +527,8 @@
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 a7cf0cf..1550763 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2674,6 +2674,21 @@
<permission android:name="android.permission.COMPANION_APPROVE_WIFI_CONNECTIONS"
android:protectionLevel="signature|privileged" />
+ <!-- Allows an app to read and listen to projection state.
+ @hide
+ @SystemApi
+ -->
+ <permission android:name="android.permission.READ_PROJECTION_STATE"
+ android:protectionLevel="signature" />
+
+ <!-- Allows an app to set and release automotive projection.
+ <p>Once permissions can be granted via role-only, this needs to be changed to
+ protectionLevel="role" and added to the SYSTEM_AUTOMOTIVE_PROJECTION role.
+ @hide
+ @SystemApi
+ -->
+ <permission android:name="android.permission.TOGGLE_AUTOMOTIVE_PROJECTION"
+ android:protectionLevel="signature|privileged" />
<!-- ================================== -->
<!-- Permissions affecting the system wallpaper -->
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 4c3b36f..42ebfc5 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -164,7 +164,6 @@
<permission name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/>
<permission name="android.permission.CONTROL_INCALL_EXPERIENCE"/>
<permission name="android.permission.DUMP"/>
- <permission name="android.permission.HANDLE_CAR_MODE_CHANGES"/>
<permission name="android.permission.INTERACT_ACROSS_USERS"/>
<permission name="android.permission.LOCAL_MAC_ADDRESS"/>
<permission name="android.permission.MANAGE_USERS"/>
@@ -175,6 +174,7 @@
<permission name="android.permission.READ_CARRIER_APP_INFO"/>
<permission name="android.permission.READ_NETWORK_USAGE_HISTORY"/>
<permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
+ <permission name="android.permission.READ_PROJECTION_STATE"/>
<permission name="android.permission.READ_SEARCH_INDEXABLES"/>
<permission name="android.permission.REBOOT"/>
<permission name="android.permission.REGISTER_CALL_PROVIDER"/>
@@ -376,6 +376,8 @@
<permission name="android.permission.STOP_APP_SWITCHES"/>
<permission name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/>
<permission name="android.permission.SUSPEND_APPS" />
+ <!-- Permissions required for UiModeManager and Telecom car mode CTS tests -->
+ <permission name="android.permission.TOGGLE_AUTOMOTIVE_PROJECTION" />
<permission name="android.permission.UPDATE_APP_OPS_STATS"/>
<permission name="android.permission.USE_RESERVED_DISK"/>
<permission name="android.permission.WIFI_UPDATE_USABILITY_STATS_SCORE"/>
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index ec47c71..20b4451 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -250,6 +250,9 @@
<!-- Permission required for CTS test - UiModeManagerTest -->
<uses-permission android:name="android.permission.ENTER_CAR_MODE_PRIORITIZED"/>
+ <!-- Permission required for CTS tests - UiModeManagerTest, CarModeInCallServiceTest -->
+ <uses-permission android:name="android.permission.TOGGLE_AUTOMOTIVE_PROJECTION"/>
+
<!-- Permission required for CTS test - SystemConfigTest -->
<uses-permission android:name="android.permission.READ_CARRIER_APP_INFO"/>
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index 7051452..f49f1b1 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.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.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.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.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.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 @@
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 @@
private final LocalService mLocalService = new LocalService();
private PowerManagerInternal mLocalPowerManager;
+ @GuardedBy("mLock")
+ @Nullable
+ private SparseArray<List<ProjectionHolder>> mProjectionHolders;
+ @GuardedBy("mLock")
+ @Nullable
+ private SparseArray<RemoteCallbackList<IOnProjectionStateChangeListener>> 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 @@
// 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 @@
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<ProjectionHolder> 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<String> getProjectingPackages(
+ @UiModeManager.ProjectionType int projectionType) {
+ getContext().enforceCallingOrSelfPermission(
+ android.Manifest.permission.READ_PROJECTION_STATE, "getProjectionState");
+ synchronized (mLock) {
+ List<String> 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<String> 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<String> toPackageNameList(Collection<ProjectionHolder> c) {
+ List<String> 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<String> 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<ProjectionHolder> 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<ProjectionHolder> 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<IOnProjectionStateChangeListener> listeners =
+ mProjectionListeners.valueAt(i);
+ List<String> 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 @@
}
}
}
+
+ @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 701197e..1883f4e 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 @@
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 @@
/**
* 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 @@
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<BatterySaverPolicyListener> mListeners = new ArrayList<>();
@@ -282,24 +267,14 @@
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 @@
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 @@
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 @@
+ 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 @@
}
}
+ /**
+ * 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 043ca9e..dcbf8c0 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 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 @@
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 6ead95c..b9aa554 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -86,6 +86,7 @@
<uses-permission android:name="android.permission.ACCESS_VIBRATOR_STATE"/>
<uses-permission android:name="android.permission.VIBRATE_ALWAYS_ON"/>
<uses-permission android:name="android.permission.CONTROL_DEVICE_STATE"/>
+ <uses-permission android:name="android.permission.READ_PROJECTION_STATE"/>
<!-- Uses API introduced in O (26) -->
<uses-sdk android:minSdkVersion="1"
diff --git a/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java
index dc30add..fb6b29e 100644
--- a/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java
@@ -117,7 +117,7 @@
@SmallTest
public void testGetBatterySaverPolicy_PolicyVibration_WithAccessibilityEnabled() {
- mBatterySaverPolicy.setAccessibilityEnabled(true);
+ mBatterySaverPolicy.mAccessibilityEnabled.update(true);
testServiceDefaultValue_Off(ServiceType.VIBRATION);
}
@@ -340,7 +340,7 @@
verifyBatterySaverConstantsUpdated();
}
- public void testCarModeChanges_Full() {
+ public void testAutomotiveProjectionChanges_Full() {
mBatterySaverPolicy.updateConstantsLocked(
"gps_mode=" + PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF
+ ",enable_night_mode=true", "");
@@ -350,7 +350,7 @@
assertTrue(mBatterySaverPolicy.getBatterySaverPolicy(
ServiceType.NIGHT_MODE).batterySaverEnabled);
- mBatterySaverPolicy.setCarModeEnabled(true);
+ mBatterySaverPolicy.mAutomotiveProjectionActive.update(true);
assertThat(mBatterySaverPolicy.getBatterySaverPolicy(ServiceType.LOCATION).locationMode)
.isAnyOf(PowerManager.LOCATION_MODE_NO_CHANGE,
@@ -358,7 +358,7 @@
assertFalse(mBatterySaverPolicy.getBatterySaverPolicy(
ServiceType.NIGHT_MODE).batterySaverEnabled);
- mBatterySaverPolicy.setCarModeEnabled(false);
+ mBatterySaverPolicy.mAutomotiveProjectionActive.update(false);
assertThat(mBatterySaverPolicy.getBatterySaverPolicy(ServiceType.LOCATION).locationMode)
.isEqualTo(PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF);
@@ -366,7 +366,7 @@
ServiceType.NIGHT_MODE).batterySaverEnabled);
}
- public void testCarModeChanges_Adaptive() {
+ public void testAutomotiveProjectionChanges_Adaptive() {
mBatterySaverPolicy.setAdaptivePolicyLocked(
Policy.fromSettings(
"gps_mode=" + PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF
@@ -377,7 +377,7 @@
assertTrue(mBatterySaverPolicy.getBatterySaverPolicy(
ServiceType.NIGHT_MODE).batterySaverEnabled);
- mBatterySaverPolicy.setCarModeEnabled(true);
+ mBatterySaverPolicy.mAutomotiveProjectionActive.update(true);
assertThat(mBatterySaverPolicy.getBatterySaverPolicy(ServiceType.LOCATION).locationMode)
.isAnyOf(PowerManager.LOCATION_MODE_NO_CHANGE,
@@ -385,7 +385,7 @@
assertFalse(mBatterySaverPolicy.getBatterySaverPolicy(
ServiceType.NIGHT_MODE).batterySaverEnabled);
- mBatterySaverPolicy.setCarModeEnabled(false);
+ mBatterySaverPolicy.mAutomotiveProjectionActive.update(false);
assertThat(mBatterySaverPolicy.getBatterySaverPolicy(ServiceType.LOCATION).locationMode)
.isEqualTo(PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF);
diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp
index 4439f99..e5646db 100644
--- a/services/tests/uiservicestests/Android.bp
+++ b/services/tests/uiservicestests/Android.bp
@@ -25,6 +25,9 @@
"hamcrest-library",
"testables",
"truth-prebuilt",
+ // TODO: remove once Android migrates to JUnit 4.12,
+ // which provides assertThrows
+ "testng",
],
libs: [
diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
index 88b1d19..4df469e 100644
--- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java
@@ -16,48 +16,21 @@
package com.android.server;
-import android.Manifest;
-import android.app.AlarmManager;
-import android.app.IUiModeManager;
-import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.os.Handler;
-import android.os.PowerManager;
-import android.os.PowerManagerInternal;
-import android.os.PowerSaveState;
-import android.os.RemoteException;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-
-import com.android.server.twilight.TwilightListener;
-import com.android.server.twilight.TwilightManager;
-import com.android.server.twilight.TwilightState;
-import com.android.server.wm.WindowManagerInternal;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-
-import java.time.LocalDateTime;
-import java.time.LocalTime;
-import java.time.ZoneId;
-import java.util.function.Consumer;
-
import static android.app.UiModeManager.MODE_NIGHT_AUTO;
import static android.app.UiModeManager.MODE_NIGHT_CUSTOM;
import static android.app.UiModeManager.MODE_NIGHT_NO;
import static android.app.UiModeManager.MODE_NIGHT_YES;
+import static android.app.UiModeManager.PROJECTION_TYPE_ALL;
+import static android.app.UiModeManager.PROJECTION_TYPE_AUTOMOTIVE;
+import static android.app.UiModeManager.PROJECTION_TYPE_NONE;
+
import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertTrue;
+
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.empty;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
@@ -69,15 +42,60 @@
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
+import static org.testng.Assert.assertThrows;
+
+import android.Manifest;
+import android.app.AlarmManager;
+import android.app.IOnProjectionStateChangeListener;
+import android.app.IUiModeManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.PowerManagerInternal;
+import android.os.PowerSaveState;
+import android.os.RemoteException;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import com.android.server.twilight.TwilightListener;
+import com.android.server.twilight.TwilightManager;
+import com.android.server.twilight.TwilightState;
+import com.android.server.wm.WindowManagerInternal;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.util.List;
+import java.util.function.Consumer;
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class UiModeManagerServiceTest extends UiServiceTestCase {
+ private static final String PACKAGE_NAME = "Diane Coffee";
private UiModeManagerService mUiManagerService;
private IUiModeManager mService;
@Mock
@@ -100,6 +118,10 @@
private TwilightState mTwilightState;
@Mock
PowerManagerInternal mLocalPowerManager;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private IBinder mBinder;
private BroadcastReceiver mScreenOffCallback;
private BroadcastReceiver mTimeChangedCallback;
@@ -124,6 +146,7 @@
.thenReturn(new PowerSaveState.Builder().setBatterySaverEnabled(false).build());
when(mContext.getResources()).thenReturn(mResources);
when(mContext.getContentResolver()).thenReturn(mContentResolver);
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
when(mPowerManager.isInteractive()).thenReturn(true);
when(mPowerManager.newWakeLock(anyInt(), anyString())).thenReturn(mWakeLock);
when(mTwilightManager.getLastTwilightState()).thenReturn(mTwilightState);
@@ -156,8 +179,8 @@
addLocalService(PowerManagerInternal.class, mLocalPowerManager);
addLocalService(TwilightManager.class, mTwilightManager);
- mUiManagerService = new UiModeManagerService(mContext, true,
- mTwilightManager);
+ mUiManagerService = new UiModeManagerService(mContext, /* setupWizardComplete= */ true,
+ mTwilightManager, new TestInjector());
try {
mUiManagerService.onBootPhase(SystemService.PHASE_SYSTEM_SERVICES_READY);
} catch (SecurityException e) {/* ignore for permission denial */}
@@ -449,4 +472,353 @@
return (mUiManagerService.getConfiguration().uiMode
& Configuration.UI_MODE_NIGHT_YES) != 0;
}
+
+ @Test
+ public void requestProjection_failsForBogusPackageName() throws Exception {
+ when(mPackageManager.getPackageUid(PACKAGE_NAME, 0))
+ .thenReturn(TestInjector.CALLING_UID + 1);
+
+ assertThrows(SecurityException.class, () -> 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<IBinder.DeathRecipient> 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<IBinder.DeathRecipient> 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;
+ }
+ }
}