summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xapi/system-current.txt43
-rw-r--r--api/test-current.txt44
-rw-r--r--core/java/android/app/SystemServiceRegistry.java8
-rw-r--r--core/java/android/content/Context.java10
-rw-r--r--core/java/android/hardware/lights/ILightsManager.aidl33
-rw-r--r--core/java/android/hardware/lights/Light.aidl20
-rw-r--r--core/java/android/hardware/lights/Light.java105
-rw-r--r--core/java/android/hardware/lights/LightState.aidl20
-rw-r--r--core/java/android/hardware/lights/LightState.java84
-rw-r--r--core/java/android/hardware/lights/LightsManager.java204
-rw-r--r--core/java/android/hardware/lights/LightsRequest.java95
-rw-r--r--core/res/AndroidManifest.xml7
-rw-r--r--services/core/java/com/android/server/lights/LightsService.java236
-rw-r--r--services/tests/servicestests/src/com/android/server/lights/LightsServiceTest.java173
14 files changed, 1068 insertions, 14 deletions
diff --git a/api/system-current.txt b/api/system-current.txt
index 831163910930..7fb2802432d0 100755
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -62,6 +62,7 @@ package android {
field public static final String CONFIGURE_WIFI_DISPLAY = "android.permission.CONFIGURE_WIFI_DISPLAY";
field @Deprecated public static final String CONNECTIVITY_INTERNAL = "android.permission.CONNECTIVITY_INTERNAL";
field public static final String CONNECTIVITY_USE_RESTRICTED_NETWORKS = "android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS";
+ field public static final String CONTROL_DEVICE_LIGHTS = "android.permission.CONTROL_DEVICE_LIGHTS";
field public static final String CONTROL_DISPLAY_COLOR_TRANSFORMS = "android.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS";
field public static final String CONTROL_DISPLAY_SATURATION = "android.permission.CONTROL_DISPLAY_SATURATION";
field public static final String CONTROL_INCALL_EXPERIENCE = "android.permission.CONTROL_INCALL_EXPERIENCE";
@@ -2922,6 +2923,48 @@ package android.hardware.hdmi {
}
+package android.hardware.lights {
+
+ public final class Light implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getId();
+ method public int getOrdinal();
+ method public int getType();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.hardware.lights.Light> CREATOR;
+ }
+
+ public final class LightState implements android.os.Parcelable {
+ ctor public LightState(@ColorInt int);
+ method public int describeContents();
+ method @ColorInt public int getColor();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.hardware.lights.LightState> CREATOR;
+ }
+
+ public final class LightsManager {
+ method @NonNull @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_LIGHTS) public java.util.List<android.hardware.lights.Light> getLights();
+ method @NonNull @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_LIGHTS) public android.hardware.lights.LightsManager.LightsSession openSession();
+ field public static final int LIGHT_TYPE_MICROPHONE = 8; // 0x8
+ }
+
+ public final class LightsManager.LightsSession implements java.lang.AutoCloseable {
+ method @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_LIGHTS) public void close();
+ method @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_LIGHTS) public void setLights(@NonNull android.hardware.lights.LightsRequest);
+ }
+
+ public final class LightsRequest {
+ }
+
+ public static final class LightsRequest.Builder {
+ ctor public LightsRequest.Builder();
+ method @NonNull public android.hardware.lights.LightsRequest build();
+ method @NonNull public android.hardware.lights.LightsRequest.Builder clearLight(@NonNull android.hardware.lights.Light);
+ method @NonNull public android.hardware.lights.LightsRequest.Builder setLight(@NonNull android.hardware.lights.Light, @NonNull android.hardware.lights.LightState);
+ }
+
+}
+
package android.hardware.location {
public class ContextHubClient implements java.io.Closeable {
diff --git a/api/test-current.txt b/api/test-current.txt
index dc6b5154e62d..29669f64aa19 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -10,6 +10,7 @@ package android {
field public static final String CHANGE_APP_IDLE_STATE = "android.permission.CHANGE_APP_IDLE_STATE";
field public static final String CLEAR_APP_USER_DATA = "android.permission.CLEAR_APP_USER_DATA";
field public static final String CONFIGURE_DISPLAY_BRIGHTNESS = "android.permission.CONFIGURE_DISPLAY_BRIGHTNESS";
+ field public static final String CONTROL_DEVICE_LIGHTS = "android.permission.CONTROL_DEVICE_LIGHTS";
field public static final String FORCE_STOP_PACKAGES = "android.permission.FORCE_STOP_PACKAGES";
field public static final String MANAGE_ACTIVITY_STACKS = "android.permission.MANAGE_ACTIVITY_STACKS";
field public static final String MANAGE_CRATES = "android.permission.MANAGE_CRATES";
@@ -1132,6 +1133,49 @@ package android.hardware.display {
}
+package android.hardware.lights {
+
+ public final class Light implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getId();
+ method public int getOrdinal();
+ method public int getType();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.hardware.lights.Light> CREATOR;
+ }
+
+ public final class LightState implements android.os.Parcelable {
+ ctor public LightState(@ColorInt int);
+ method public int describeContents();
+ method @ColorInt public int getColor();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.hardware.lights.LightState> CREATOR;
+ }
+
+ public final class LightsManager {
+ method @NonNull @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_LIGHTS) public android.hardware.lights.LightState getLightState(@NonNull android.hardware.lights.Light);
+ method @NonNull @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_LIGHTS) public java.util.List<android.hardware.lights.Light> getLights();
+ method @NonNull @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_LIGHTS) public android.hardware.lights.LightsManager.LightsSession openSession();
+ field public static final int LIGHT_TYPE_MICROPHONE = 8; // 0x8
+ }
+
+ public final class LightsManager.LightsSession implements java.lang.AutoCloseable {
+ method @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_LIGHTS) public void close();
+ method @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_LIGHTS) public void setLights(@NonNull android.hardware.lights.LightsRequest);
+ }
+
+ public final class LightsRequest {
+ }
+
+ public static final class LightsRequest.Builder {
+ ctor public LightsRequest.Builder();
+ method @NonNull public android.hardware.lights.LightsRequest build();
+ method @NonNull public android.hardware.lights.LightsRequest.Builder clearLight(@NonNull android.hardware.lights.Light);
+ method @NonNull public android.hardware.lights.LightsRequest.Builder setLight(@NonNull android.hardware.lights.Light, @NonNull android.hardware.lights.LightState);
+ }
+
+}
+
package android.location {
public final class GnssClock implements android.os.Parcelable {
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index c1e535643ddf..cc72938da449 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -89,6 +89,7 @@ import android.hardware.hdmi.IHdmiControlService;
import android.hardware.input.InputManager;
import android.hardware.iris.IIrisService;
import android.hardware.iris.IrisManager;
+import android.hardware.lights.LightsManager;
import android.hardware.location.ContextHubManager;
import android.hardware.radio.RadioManager;
import android.hardware.usb.IUsbManager;
@@ -1262,6 +1263,13 @@ public final class SystemServiceRegistry {
Context.DATA_LOADER_MANAGER_SERVICE);
return new DataLoaderManager(IDataLoaderManager.Stub.asInterface(b));
}});
+ registerService(Context.LIGHTS_SERVICE, LightsManager.class,
+ new CachedServiceFetcher<LightsManager>() {
+ @Override
+ public LightsManager createService(ContextImpl ctx)
+ throws ServiceNotFoundException {
+ return new LightsManager(ctx);
+ }});
//TODO(b/136132412): refactor this: 1) merge IIncrementalManager.aidl and
//IIncrementalManagerNative.aidl, 2) implement the binder interface in
//IncrementalManagerService.java, 3) use JNI to call native functions
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 2943e398dd87..8921b13f62a3 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -3480,6 +3480,7 @@ public abstract class Context {
//@hide: TIME_DETECTOR_SERVICE,
//@hide: TIME_ZONE_DETECTOR_SERVICE,
PERMISSION_SERVICE,
+ LIGHTS_SERVICE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ServiceName {}
@@ -5111,6 +5112,15 @@ public abstract class Context {
public static final String FILE_INTEGRITY_SERVICE = "file_integrity";
/**
+ * Use with {@link #getSystemService(String)} to retrieve a
+ * {@link android.hardware.lights.LightsManager} for controlling device lights.
+ *
+ * @see #getSystemService(String)
+ * @hide
+ */
+ public static final String LIGHTS_SERVICE = "lights";
+
+ /**
* Determine whether the given permission is allowed for a particular
* process and user ID running in the system.
*
diff --git a/core/java/android/hardware/lights/ILightsManager.aidl b/core/java/android/hardware/lights/ILightsManager.aidl
new file mode 100644
index 000000000000..6ea24b74a4a3
--- /dev/null
+++ b/core/java/android/hardware/lights/ILightsManager.aidl
@@ -0,0 +1,33 @@
+/**
+ * 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.hardware.lights;
+
+import android.hardware.lights.Light;
+import android.hardware.lights.LightState;
+
+/**
+ * API to lights manager service.
+ *
+ * {@hide}
+ */
+interface ILightsManager {
+ List<Light> getLights();
+ LightState getLightState(int lightId);
+ void openSession(in IBinder sessionToken);
+ void closeSession(in IBinder sessionToken);
+ void setLightStates(in IBinder sessionToken, in int[] lightIds, in LightState[] states);
+}
diff --git a/core/java/android/hardware/lights/Light.aidl b/core/java/android/hardware/lights/Light.aidl
new file mode 100644
index 000000000000..946e06d44cf3
--- /dev/null
+++ b/core/java/android/hardware/lights/Light.aidl
@@ -0,0 +1,20 @@
+/**
+ * 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.hardware.lights;
+
+/** @hide */
+parcelable Light;
diff --git a/core/java/android/hardware/lights/Light.java b/core/java/android/hardware/lights/Light.java
new file mode 100644
index 000000000000..c5cb8037d4db
--- /dev/null
+++ b/core/java/android/hardware/lights/Light.java
@@ -0,0 +1,105 @@
+/**
+ * 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.hardware.lights;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents a logical light on the device.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class Light implements Parcelable {
+ private final int mId;
+ private final int mOrdinal;
+ private final int mType;
+
+ /**
+ * Creates a new light with the given data.
+ *
+ * @hide */
+ public Light(int id, int ordinal, int type) {
+ mId = id;
+ mOrdinal = ordinal;
+ mType = type;
+ }
+
+ private Light(@NonNull Parcel in) {
+ mId = in.readInt();
+ mOrdinal = in.readInt();
+ mType = in.readInt();
+ }
+
+ /** Implement the Parcelable interface */
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mId);
+ dest.writeInt(mOrdinal);
+ dest.writeInt(mType);
+ }
+
+ /** Implement the Parcelable interface */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Implement the Parcelable interface */
+ public static final @android.annotation.NonNull Parcelable.Creator<Light> CREATOR =
+ new Parcelable.Creator<Light>() {
+ public Light createFromParcel(Parcel in) {
+ return new Light(in);
+ }
+
+ public Light[] newArray(int size) {
+ return new Light[size];
+ }
+ };
+
+ /**
+ * Returns the id of the light.
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the ordinal of the light.
+ *
+ * <p>This represents the physical order of the lights on the device. The exact values are
+ * device-dependent, but for example, if there are lights in a row, sorting the Light objects
+ * by ordinal should match the order in which they appear on the device. If the device has
+ * 4 lights, the ordinals could be [1, 2, 3, 4] or [0, 10, 20, 30] or any other values that
+ * have the same sort order.
+ */
+ public int getOrdinal() {
+ return mOrdinal;
+ }
+
+ /**
+ * Returns the logical type of the light.
+ */
+ public @LightsManager.LightType int getType() {
+ return mType;
+ }
+}
diff --git a/core/java/android/hardware/lights/LightState.aidl b/core/java/android/hardware/lights/LightState.aidl
new file mode 100644
index 000000000000..d598336ae40c
--- /dev/null
+++ b/core/java/android/hardware/lights/LightState.aidl
@@ -0,0 +1,20 @@
+/**
+ * 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.hardware.lights;
+
+/** @hide */
+parcelable LightState;
diff --git a/core/java/android/hardware/lights/LightState.java b/core/java/android/hardware/lights/LightState.java
new file mode 100644
index 000000000000..e55aa702f15c
--- /dev/null
+++ b/core/java/android/hardware/lights/LightState.java
@@ -0,0 +1,84 @@
+/**
+ * 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.hardware.lights;
+
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents the state of a device light.
+ *
+ * <p>Controlling the color and brightness of a light is done on a best-effort basis. Each of the R,
+ * G and B channels represent the intensities of the respective part of an RGB LED, if that is
+ * supported. For devices that only support on or off lights, everything that's not off will turn
+ * the light on. If the light is monochrome and only the brightness can be controlled, the RGB color
+ * will be converted to only a brightness value and that will be used for the light's single
+ * channel.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class LightState implements Parcelable {
+ private final int mColor;
+
+ /**
+ * Creates a new LightState with the desired color and intensity.
+ *
+ * @param color the desired color and intensity in ARGB format.
+ */
+ public LightState(@ColorInt int color) {
+ mColor = color;
+ }
+
+ private LightState(@NonNull Parcel in) {
+ mColor = in.readInt();
+ }
+
+ /**
+ * Return the color and intensity associated with this LightState.
+ * @return the color and intensity in ARGB format. The A channel is ignored.
+ */
+ public @ColorInt int getColor() {
+ return mColor;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mColor);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @NonNull Parcelable.Creator<LightState> CREATOR =
+ new Parcelable.Creator<LightState>() {
+ public LightState createFromParcel(Parcel in) {
+ return new LightState(in);
+ }
+
+ public LightState[] newArray(int size) {
+ return new LightState[size];
+ }
+ };
+}
diff --git a/core/java/android/hardware/lights/LightsManager.java b/core/java/android/hardware/lights/LightsManager.java
new file mode 100644
index 000000000000..1bc051b977a8
--- /dev/null
+++ b/core/java/android/hardware/lights/LightsManager.java
@@ -0,0 +1,204 @@
+/*
+ * 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.hardware.lights;
+
+import android.Manifest;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceManager.ServiceNotFoundException;
+import android.util.CloseGuard;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.Reference;
+import java.util.List;
+
+/**
+ * The LightsManager class allows control over device lights.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+@SystemService(Context.LIGHTS_SERVICE)
+public final class LightsManager {
+ private static final String TAG = "LightsManager";
+
+ // These enum values copy the values from {@link com.android.server.lights.LightsManager}
+ // and the light HAL. Since 0-7 are lights reserved for system use, only the microphone light
+ // is available through this API.
+ /** Type for lights that indicate microphone usage */
+ public static final int LIGHT_TYPE_MICROPHONE = 8;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"LIGHT_TYPE_"},
+ value = {
+ LIGHT_TYPE_MICROPHONE,
+ })
+ public @interface LightType {}
+
+ @NonNull private final Context mContext;
+ @NonNull private final ILightsManager mService;
+
+ /**
+ * Creates a LightsManager.
+ *
+ * @hide
+ */
+ public LightsManager(@NonNull Context context) throws ServiceNotFoundException {
+ this(context, ILightsManager.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(Context.LIGHTS_SERVICE)));
+ }
+
+ /**
+ * Creates a LightsManager with a provided service implementation.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public LightsManager(@NonNull Context context, @NonNull ILightsManager service) {
+ mContext = Preconditions.checkNotNull(context);
+ mService = Preconditions.checkNotNull(service);
+ }
+
+ /**
+ * Returns the lights available on the device.
+ *
+ * @return A list of available lights
+ */
+ @RequiresPermission(Manifest.permission.CONTROL_DEVICE_LIGHTS)
+ public @NonNull List<Light> getLights() {
+ try {
+ return mService.getLights();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the state of a specified light.
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.CONTROL_DEVICE_LIGHTS)
+ @TestApi
+ public @NonNull LightState getLightState(@NonNull Light light) {
+ Preconditions.checkNotNull(light);
+ try {
+ return mService.getLightState(light.getId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Creates a new LightsSession that can be used to control the device lights.
+ */
+ @RequiresPermission(Manifest.permission.CONTROL_DEVICE_LIGHTS)
+ public @NonNull LightsSession openSession() {
+ try {
+ final LightsSession session = new LightsSession();
+ mService.openSession(session.mToken);
+ return session;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Encapsulates a session that can be used to control device lights and represents the lifetime
+ * of the requests.
+ */
+ public final class LightsSession implements AutoCloseable {
+
+ private final IBinder mToken = new Binder();
+
+ private final CloseGuard mCloseGuard = new CloseGuard();
+ private boolean mClosed = false;
+
+ /**
+ * Instantiated by {@link LightsManager#openSession()}.
+ */
+ @RequiresPermission(Manifest.permission.CONTROL_DEVICE_LIGHTS)
+ private LightsSession() {
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * Sends a request to modify the states of multiple lights.
+ *
+ * <p>This method only controls lights that aren't overridden by higher-priority sessions.
+ * Additionally, lights not controlled by this session can be controlled by lower-priority
+ * sessions.
+ *
+ * @param request the settings for lights that should change
+ */
+ @RequiresPermission(Manifest.permission.CONTROL_DEVICE_LIGHTS)
+ public void setLights(@NonNull LightsRequest request) {
+ Preconditions.checkNotNull(request);
+ if (!mClosed) {
+ try {
+ mService.setLightStates(mToken, request.mLightIds, request.mLightStates);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Closes the session, reverting all changes made through it.
+ */
+ @RequiresPermission(Manifest.permission.CONTROL_DEVICE_LIGHTS)
+ @Override
+ public void close() {
+ if (!mClosed) {
+ try {
+ mService.closeSession(mToken);
+ mClosed = true;
+ mCloseGuard.close();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ Reference.reachabilityFence(this);
+ }
+
+ /** @hide */
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ mCloseGuard.warnIfOpen();
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+ }
+}
diff --git a/core/java/android/hardware/lights/LightsRequest.java b/core/java/android/hardware/lights/LightsRequest.java
new file mode 100644
index 000000000000..a36da4c7d85d
--- /dev/null
+++ b/core/java/android/hardware/lights/LightsRequest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.hardware.lights;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.util.SparseArray;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Encapsulates a request to modify the state of multiple lights.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class LightsRequest {
+
+ /** Visible to {@link LightsManager.Session}. */
+ final int[] mLightIds;
+
+ /** Visible to {@link LightsManager.Session}. */
+ final LightState[] mLightStates;
+
+ /**
+ * Can only be constructed via {@link LightsRequest.Builder#build()}.
+ */
+ private LightsRequest(SparseArray<LightState> changes) {
+ final int n = changes.size();
+ mLightIds = new int[n];
+ mLightStates = new LightState[n];
+ for (int i = 0; i < n; i++) {
+ mLightIds[i] = changes.keyAt(i);
+ mLightStates[i] = changes.valueAt(i);
+ }
+ }
+
+ /**
+ * Builder for creating device light change requests.
+ */
+ public static final class Builder {
+
+ private final SparseArray<LightState> mChanges = new SparseArray<>();
+
+ /**
+ * Overrides the color and intensity of a given light.
+ *
+ * @param light the light to modify
+ * @param state the desired color and intensity of the light
+ */
+ public @NonNull Builder setLight(@NonNull Light light, @NonNull LightState state) {
+ Preconditions.checkNotNull(light);
+ Preconditions.checkNotNull(state);
+ mChanges.put(light.getId(), state);
+ return this;
+ }
+
+ /**
+ * Removes the override for the color and intensity of a given light.
+ *
+ * @param light the light to modify
+ */
+ public @NonNull Builder clearLight(@NonNull Light light) {
+ Preconditions.checkNotNull(light);
+ mChanges.put(light.getId(), null);
+ return this;
+ }
+
+ /**
+ * Create a LightsRequest object used to override lights on the device.
+ *
+ * <p>The generated {@link LightsRequest} should be used in
+ * {@link LightsManager.Session#setLights(LightsLightsRequest).
+ */
+ public @NonNull LightsRequest build() {
+ return new LightsRequest(mChanges);
+ }
+ }
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 3c657530061b..c794cf1a79d4 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3715,6 +3715,13 @@
<permission android:name="android.permission.CONFIGURE_DISPLAY_COLOR_MODE"
android:protectionLevel="signature" />
+ <!-- Allows an application to control the lights on the device.
+ @hide
+ @SystemApi
+ @TestApi -->
+ <permission android:name="android.permission.CONTROL_DEVICE_LIGHTS"
+ android:protectionLevel="signature|privileged" />
+
<!-- Allows an application to control the color saturation of the display.
@hide
@SystemApi -->
diff --git a/services/core/java/com/android/server/lights/LightsService.java b/services/core/java/com/android/server/lights/LightsService.java
index 9b348e9f8178..5683e6901a31 100644
--- a/services/core/java/com/android/server/lights/LightsService.java
+++ b/services/core/java/com/android/server/lights/LightsService.java
@@ -15,32 +15,217 @@
package com.android.server.lights;
+import android.Manifest;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.Context;
import android.hardware.light.HwLight;
import android.hardware.light.HwLightState;
import android.hardware.light.ILights;
+import android.hardware.lights.ILightsManager;
+import android.hardware.lights.Light;
+import android.hardware.lights.LightState;
import android.os.Handler;
import android.os.IBinder;
-import android.os.Message;
+import android.os.Looper;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.Trace;
import android.provider.Settings;
import android.util.Slog;
+import android.util.SparseArray;
import android.view.SurfaceControl;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
import com.android.server.SystemService;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
public class LightsService extends SystemService {
static final String TAG = "LightsService";
static final boolean DEBUG = false;
private LightImpl[] mLights = null;
+ private SparseArray<LightImpl> mLightsById = null;
private ILights mVintfLights = null;
+ @VisibleForTesting
+ final LightsManagerBinderService mManagerService;
+
+ private Handler mH;
+
+ private final class LightsManagerBinderService extends ILightsManager.Stub {
+
+ private final class Session {
+ final IBinder mToken;
+ final SparseArray<LightState> mRequests = new SparseArray<>();
+
+ Session(IBinder token) {
+ mToken = token;
+ }
+
+ void setRequest(int lightId, LightState state) {
+ if (state != null) {
+ mRequests.put(lightId, state);
+ } else {
+ mRequests.remove(lightId);
+ }
+ }
+ }
+
+ @GuardedBy("LightsService.this")
+ private final List<Session> mSessions = new ArrayList<>();
+
+ /**
+ * Returns the lights available for apps to control on the device. Only lights that aren't
+ * reserved for system use are available to apps.
+ */
+ @Override
+ public List<Light> getLights() {
+ getContext().enforceCallingOrSelfPermission(Manifest.permission.CONTROL_DEVICE_LIGHTS,
+ "getLights requires CONTROL_DEVICE_LIGHTS_PERMISSION");
+
+ synchronized (LightsService.this) {
+ final List<Light> lights = new ArrayList<Light>();
+ for (int i = 0; i < mLightsById.size(); i++) {
+ HwLight hwLight = mLightsById.valueAt(i).getHwLight();
+ if (!isSystemLight(hwLight)) {
+ lights.add(new Light(hwLight.id, hwLight.ordinal, hwLight.type));
+ }
+ }
+ return lights;
+ }
+ }
+
+ /**
+ * Updates the set of light requests for {@param token} with additions and removals from
+ * {@param lightIds} and {@param lightStates}.
+ *
+ * <p>Null values mean that the request should be removed, and the light turned off if it
+ * is not being used by anything else.
+ */
+ @Override
+ public void setLightStates(IBinder token, int[] lightIds, LightState[] lightStates) {
+ getContext().enforceCallingOrSelfPermission(Manifest.permission.CONTROL_DEVICE_LIGHTS,
+ "setLightStates requires CONTROL_DEVICE_LIGHTS permission");
+ Preconditions.checkState(lightIds.length == lightStates.length);
+
+ synchronized (LightsService.this) {
+ Session session = getSessionLocked(Preconditions.checkNotNull(token));
+ Preconditions.checkState(session != null, "not registered");
+
+ checkRequestIsValid(lightIds);
+
+ for (int i = 0; i < lightIds.length; i++) {
+ session.setRequest(lightIds[i], lightStates[i]);
+ }
+ invalidateLightStatesLocked();
+ }
+ }
+
+ @Override
+ public @Nullable LightState getLightState(int lightId) {
+ getContext().enforceCallingOrSelfPermission(Manifest.permission.CONTROL_DEVICE_LIGHTS,
+ "getLightState(@TestApi) requires CONTROL_DEVICE_LIGHTS permission");
+
+ synchronized (LightsService.this) {
+ final LightImpl light = mLightsById.get(lightId);
+ if (light == null || isSystemLight(light.getHwLight())) {
+ throw new IllegalArgumentException("Invalid light: " + lightId);
+ }
+ return new LightState(light.getColor());
+ }
+ }
+
+ @Override
+ public void openSession(IBinder token) {
+ getContext().enforceCallingOrSelfPermission(Manifest.permission.CONTROL_DEVICE_LIGHTS,
+ "openSession requires CONTROL_DEVICE_LIGHTS permission");
+ Preconditions.checkNotNull(token);
+
+ synchronized (LightsService.this) {
+ Preconditions.checkState(getSessionLocked(token) == null, "already registered");
+ try {
+ token.linkToDeath(() -> closeSessionInternal(token), 0);
+ mSessions.add(new Session(token));
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Couldn't open session, client already died" , e);
+ throw new IllegalArgumentException("Client is already dead.");
+ }
+ }
+ }
+
+ @Override
+ public void closeSession(IBinder token) {
+ getContext().enforceCallingOrSelfPermission(Manifest.permission.CONTROL_DEVICE_LIGHTS,
+ "closeSession requires CONTROL_DEVICE_LIGHTS permission");
+ Preconditions.checkNotNull(token);
+ closeSessionInternal(token);
+ }
+
+ private void closeSessionInternal(IBinder token) {
+ synchronized (LightsService.this) {
+ final Session session = getSessionLocked(token);
+ if (session != null) {
+ mSessions.remove(session);
+ invalidateLightStatesLocked();
+ }
+ }
+ }
+
+ private void checkRequestIsValid(int[] lightIds) {
+ for (int i = 0; i < lightIds.length; i++) {
+ final LightImpl light = mLightsById.get(lightIds[i]);
+ final HwLight hwLight = light.getHwLight();
+ Preconditions.checkState(light != null && !isSystemLight(hwLight),
+ "invalid lightId " + hwLight.id);
+ }
+ }
+
+ /**
+ * Apply light state requests for all light IDs.
+ *
+ * <p>In case of conflict, the session that started earliest wins.
+ */
+ private void invalidateLightStatesLocked() {
+ final Map<Integer, LightState> states = new HashMap<>();
+ for (int i = mSessions.size() - 1; i >= 0; i--) {
+ SparseArray<LightState> requests = mSessions.get(i).mRequests;
+ for (int j = 0; j < requests.size(); j++) {
+ states.put(requests.keyAt(j), requests.valueAt(j));
+ }
+ }
+ for (int i = 0; i < mLightsById.size(); i++) {
+ LightImpl light = mLightsById.valueAt(i);
+ HwLight hwLight = light.getHwLight();
+ if (!isSystemLight(hwLight)) {
+ LightState state = states.get(hwLight.id);
+ if (state != null) {
+ light.setColor(state.getColor());
+ } else {
+ light.turnOff();
+ }
+ }
+ }
+ }
+
+ private @Nullable Session getSessionLocked(IBinder token) {
+ for (int i = 0; i < mSessions.size(); i++) {
+ if (token.equals(mSessions.get(i).mToken)) {
+ return mSessions.get(i);
+ }
+ }
+ return null;
+ }
+ }
+
private final class LightImpl extends LogicalLight {
private final IBinder mDisplayToken;
private final int mSurfaceControlMaximumBrightness;
@@ -144,7 +329,7 @@ public class LightsService extends SystemService {
setLightLocked(color, LIGHT_FLASH_HARDWARE, onMS, 1000,
BRIGHTNESS_MODE_USER);
mColor = 0;
- mH.sendMessageDelayed(Message.obtain(mH, 1, this), onMS);
+ mH.postDelayed(this::stopFlashing, onMS);
}
}
}
@@ -237,6 +422,14 @@ public class LightsService extends SystemService {
return mVrModeEnabled && mUseLowPersistenceForVR;
}
+ private HwLight getHwLight() {
+ return mHwLight;
+ }
+
+ private int getColor() {
+ return mColor;
+ }
+
private HwLight mHwLight;
private int mColor;
private int mMode;
@@ -252,16 +445,25 @@ public class LightsService extends SystemService {
}
public LightsService(Context context) {
- super(context);
-
- IBinder service = ServiceManager.getService("android.hardware.light.ILights/default");
- mVintfLights = ILights.Stub.asInterface(service);
+ this(context,
+ ILights.Stub.asInterface(
+ ServiceManager.getService("android.hardware.light.ILights/default")),
+ Looper.myLooper());
+ }
+ @VisibleForTesting
+ LightsService(Context context, ILights service, Looper looper) {
+ super(context);
+ mH = new Handler(looper);
+ mVintfLights = service;
+ mManagerService = new LightsManagerBinderService();
populateAvailableLights(context);
}
private void populateAvailableLights(Context context) {
mLights = new LightImpl[LightsManager.LIGHT_ID_COUNT];
+ mLightsById = new SparseArray<>();
+
if (mVintfLights != null) {
try {
for (HwLight availableLight : mVintfLights.getLights()) {
@@ -270,6 +472,7 @@ public class LightsService extends SystemService {
if (0 <= type && type < mLights.length && mLights[type] == null) {
mLights[type] = light;
}
+ mLightsById.put(availableLight.id, light);
}
} catch (RemoteException ex) {
Slog.e(TAG, "Unable to get lights for initialization", ex);
@@ -286,6 +489,7 @@ public class LightsService extends SystemService {
light.type = (byte) i;
mLights[i] = new LightImpl(context, light);
+ mLightsById.put(i, mLights[i]);
}
}
}
@@ -293,6 +497,7 @@ public class LightsService extends SystemService {
@Override
public void onStart() {
publishLocalService(LightsManager.class, mService);
+ publishBinderService(Context.LIGHTS_SERVICE, mManagerService);
}
@Override
@@ -310,7 +515,7 @@ public class LightsService extends SystemService {
private final LightsManager mService = new LightsManager() {
@Override
public LogicalLight getLight(int lightType) {
- if (mLights != null && 0 <= lightType && lightType < LIGHT_ID_COUNT) {
+ if (mLights != null && 0 <= lightType && lightType < mLights.length) {
return mLights[lightType];
} else {
return null;
@@ -318,13 +523,16 @@ public class LightsService extends SystemService {
}
};
- private Handler mH = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- LightImpl light = (LightImpl)msg.obj;
- light.stopFlashing();
- }
- };
+ /**
+ * Returns whether a light is system-use-only or should be accessible to
+ * applications using the {@link android.hardware.lights.LightsManager} API.
+ */
+ private static boolean isSystemLight(HwLight light) {
+ // LIGHT_ID_COUNT comes from the 2.0 HIDL HAL and only contains system
+ // lights. Newly added lights will be made available via the
+ // LightsManager API.
+ return 0 <= light.type && light.type < LightsManager.LIGHT_ID_COUNT;
+ }
static native void setLight_native(int light, int color, int mode,
int onMS, int offMS, int brightnessMode);
diff --git a/services/tests/servicestests/src/com/android/server/lights/LightsServiceTest.java b/services/tests/servicestests/src/com/android/server/lights/LightsServiceTest.java
new file mode 100644
index 000000000000..b0def605db79
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/lights/LightsServiceTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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 com.android.server.lights;
+
+import static android.hardware.lights.LightsRequest.Builder;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.hardware.light.HwLight;
+import android.hardware.light.HwLightState;
+import android.hardware.light.ILights;
+import android.hardware.lights.Light;
+import android.hardware.lights.LightState;
+import android.hardware.lights.LightsManager;
+import android.os.Looper;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class LightsServiceTest {
+
+ private final ILights mHal = new ILights.Stub() {
+ @Override
+ public void setLightState(int id, HwLightState state) {
+ return;
+ }
+
+ @Override
+ public HwLight[] getLights() {
+ return new HwLight[] {
+ fakeHwLight(101, 3, 1),
+ fakeHwLight(102, LightsManager.LIGHT_TYPE_MICROPHONE, 4),
+ fakeHwLight(103, LightsManager.LIGHT_TYPE_MICROPHONE, 3),
+ fakeHwLight(104, LightsManager.LIGHT_TYPE_MICROPHONE, 1),
+ fakeHwLight(105, LightsManager.LIGHT_TYPE_MICROPHONE, 2)
+ };
+ }
+ };
+
+ private static HwLight fakeHwLight(int id, int type, int ordinal) {
+ HwLight light = new HwLight();
+ light.id = id;
+ light.type = (byte) type;
+ light.ordinal = ordinal;
+ return light;
+ }
+
+ @Mock
+ Context mContext;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testGetLights_filtersSystemLights() {
+ LightsService service = new LightsService(mContext, mHal, Looper.getMainLooper());
+ LightsManager manager = new LightsManager(mContext, service.mManagerService);
+
+ // When lights are listed, only the 4 MICROPHONE lights should be visible.
+ assertThat(manager.getLights().size()).isEqualTo(4);
+ }
+
+ @Test
+ public void testControlMultipleLights() {
+ LightsService service = new LightsService(mContext, mHal, Looper.getMainLooper());
+ LightsManager manager = new LightsManager(mContext, service.mManagerService);
+
+ // When the session requests to turn 3/4 lights on:
+ LightsManager.LightsSession session = manager.openSession();
+ session.setLights(new Builder()
+ .setLight(manager.getLights().get(0), new LightState(0xf1))
+ .setLight(manager.getLights().get(1), new LightState(0xf2))
+ .setLight(manager.getLights().get(2), new LightState(0xf3))
+ .build());
+
+ // Then all 3 should turn on.
+ assertThat(manager.getLightState(manager.getLights().get(0)).getColor()).isEqualTo(0xf1);
+ assertThat(manager.getLightState(manager.getLights().get(1)).getColor()).isEqualTo(0xf2);
+ assertThat(manager.getLightState(manager.getLights().get(2)).getColor()).isEqualTo(0xf3);
+
+ // And the 4th should remain off.
+ assertThat(manager.getLightState(manager.getLights().get(3)).getColor()).isEqualTo(0x00);
+ }
+
+ @Test
+ public void testControlLights_onlyEffectiveForLifetimeOfClient() {
+ LightsService service = new LightsService(mContext, mHal, Looper.getMainLooper());
+ LightsManager manager = new LightsManager(mContext, service.mManagerService);
+ Light micLight = manager.getLights().get(0);
+
+ // The light should begin by being off.
+ assertThat(manager.getLightState(micLight).getColor()).isEqualTo(0x00000000);
+
+ // When a session commits changes:
+ LightsManager.LightsSession session = manager.openSession();
+ session.setLights(new Builder().setLight(micLight, new LightState(0xff00ff00)).build());
+ // Then the light should turn on.
+ assertThat(manager.getLightState(micLight).getColor()).isEqualTo(0xff00ff00);
+
+ // When the session goes away:
+ session.close();
+ // Then the light should turn off.
+ assertThat(manager.getLightState(micLight).getColor()).isEqualTo(0x00000000);
+ }
+
+ @Test
+ public void testControlLights_firstCallerWinsContention() {
+ LightsService service = new LightsService(mContext, mHal, Looper.getMainLooper());
+ LightsManager manager = new LightsManager(mContext, service.mManagerService);
+ Light micLight = manager.getLights().get(0);
+
+ LightsManager.LightsSession session1 = manager.openSession();
+ LightsManager.LightsSession session2 = manager.openSession();
+
+ // When session1 and session2 both request the same light:
+ session1.setLights(new Builder().setLight(micLight, new LightState(0xff0000ff)).build());
+ session2.setLights(new Builder().setLight(micLight, new LightState(0xffffffff)).build());
+ // Then session1 should win because it was created first.
+ assertThat(manager.getLightState(micLight).getColor()).isEqualTo(0xff0000ff);
+
+ // When session1 goes away:
+ session1.close();
+ // Then session2 should have its request go into effect.
+ assertThat(manager.getLightState(micLight).getColor()).isEqualTo(0xffffffff);
+
+ // When session2 goes away:
+ session2.close();
+ // Then the light should turn off because there are no more sessions.
+ assertThat(manager.getLightState(micLight).getColor()).isEqualTo(0);
+ }
+
+ @Test
+ public void testClearLight() {
+ LightsService service = new LightsService(mContext, mHal, Looper.getMainLooper());
+ LightsManager manager = new LightsManager(mContext, service.mManagerService);
+ Light micLight = manager.getLights().get(0);
+
+ // When the session turns a light on:
+ LightsManager.LightsSession session = manager.openSession();
+ session.setLights(new Builder().setLight(micLight, new LightState(0xffffffff)).build());
+
+ // And then the session clears it again:
+ session.setLights(new Builder().clearLight(micLight).build());
+
+ // Then the light should turn back off.
+ assertThat(manager.getLightState(micLight).getColor()).isEqualTo(0);
+ }
+}