Merge "Add projection state APIs to UiModeManager and UiModeManagerService."

This commit is contained in:
Kurt Dresner
2020-11-07 05:10:39 +00:00
committed by Android (Google) Code Review
18 changed files with 1268 additions and 146 deletions

View File

@@ -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<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 @@ 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);
}
}
}

View File

@@ -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<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 @@ 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<java.lang.String>);
}
public final class Vr2dDisplayProperties implements android.os.Parcelable {
ctor public Vr2dDisplayProperties(int, int, int);
method public int describeContents();

View File

@@ -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<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 @@ 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<java.lang.String>);
}
public final class Vr2dDisplayProperties implements android.os.Parcelable {
ctor public Vr2dDisplayProperties(int, int, int);
method public int describeContents();

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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<String> getProjectingPackages(int projectionType);
/**
* Returns currently set projection types.
*/
int getActiveProjectionTypes();
}

View File

@@ -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.
*
*
* <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 @@ 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.
*
*
* <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 @@ 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<String> 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<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 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<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);
}
}
}

View File

@@ -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 {

View File

@@ -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 -->

View File

@@ -164,7 +164,6 @@ applications that come with the platform
<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 @@ applications that come with the platform
<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 @@ applications that come with the platform
<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"/>

View File

@@ -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"/>

View File

@@ -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<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 @@ 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<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 @@ final class UiModeManagerService extends SystemService {
}
}
}
@VisibleForTesting
public static class Injector {
public int getCallingUid() {
return Binder.getCallingUid();
}
}
}

View File

@@ -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<BatterySaverPolicyListener> 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();
}
}
}
}

View File

@@ -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);
}

View File

@@ -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"

View File

@@ -117,7 +117,7 @@ public class BatterySaverPolicyTest extends AndroidTestCase {
@SmallTest
public void testGetBatterySaverPolicy_PolicyVibration_WithAccessibilityEnabled() {
mBatterySaverPolicy.setAccessibilityEnabled(true);
mBatterySaverPolicy.mAccessibilityEnabled.update(true);
testServiceDefaultValue_Off(ServiceType.VIBRATION);
}
@@ -340,7 +340,7 @@ public class BatterySaverPolicyTest extends AndroidTestCase {
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 @@ public class BatterySaverPolicyTest extends AndroidTestCase {
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 @@ public class BatterySaverPolicyTest extends AndroidTestCase {
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 @@ public class BatterySaverPolicyTest extends AndroidTestCase {
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 @@ public class BatterySaverPolicyTest extends AndroidTestCase {
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 @@ public class BatterySaverPolicyTest extends AndroidTestCase {
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);

View File

@@ -25,6 +25,9 @@ android_test {
"hamcrest-library",
"testables",
"truth-prebuilt",
// TODO: remove once Android migrates to JUnit 4.12,
// which provides assertThrows
"testng",
],
libs: [

View File

@@ -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.atLeast;
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 @@ public class UiModeManagerServiceTest extends UiServiceTestCase {
private TwilightState mTwilightState;
@Mock
PowerManagerInternal mLocalPowerManager;
@Mock
private PackageManager mPackageManager;
@Mock
private IBinder mBinder;
private BroadcastReceiver mScreenOffCallback;
private BroadcastReceiver mTimeChangedCallback;
@@ -124,6 +146,7 @@ public class UiModeManagerServiceTest extends UiServiceTestCase {
.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 @@ public class UiModeManagerServiceTest extends UiServiceTestCase {
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 @@ public class UiModeManagerServiceTest extends UiServiceTestCase {
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;
}
}
}