diff options
| -rwxr-xr-x | api/system-current.txt | 43 | ||||
| -rw-r--r-- | api/test-current.txt | 44 | ||||
| -rw-r--r-- | core/java/android/app/SystemServiceRegistry.java | 8 | ||||
| -rw-r--r-- | core/java/android/content/Context.java | 10 | ||||
| -rw-r--r-- | core/java/android/hardware/lights/ILightsManager.aidl | 33 | ||||
| -rw-r--r-- | core/java/android/hardware/lights/Light.aidl | 20 | ||||
| -rw-r--r-- | core/java/android/hardware/lights/Light.java | 105 | ||||
| -rw-r--r-- | core/java/android/hardware/lights/LightState.aidl | 20 | ||||
| -rw-r--r-- | core/java/android/hardware/lights/LightState.java | 84 | ||||
| -rw-r--r-- | core/java/android/hardware/lights/LightsManager.java | 204 | ||||
| -rw-r--r-- | core/java/android/hardware/lights/LightsRequest.java | 95 | ||||
| -rw-r--r-- | core/res/AndroidManifest.xml | 7 | ||||
| -rw-r--r-- | services/core/java/com/android/server/lights/LightsService.java | 236 | ||||
| -rw-r--r-- | services/tests/servicestests/src/com/android/server/lights/LightsServiceTest.java | 173 |
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); + } +} |