diff options
23 files changed, 1203 insertions, 47 deletions
diff --git a/api/system-current.txt b/api/system-current.txt index c90112fecf67..0cd7d2a4bc1a 100755 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -64,6 +64,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"; @@ -2927,6 +2928,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 06755636fbdd..e2407d65ef5c 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"; @@ -1130,6 +1131,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 3abd5094e11d..7f698653bef7 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; @@ -1275,6 +1276,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 07b81ca7f21e..ebc5e0e4aa31 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 {} @@ -5121,6 +5122,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 82a28459a30e..4595bab82465 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3732,6 +3732,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/Android.bp b/services/core/Android.bp index 1691a96b0dc4..a603fa975b74 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -113,6 +113,7 @@ java_library_static { "android.hardware.broadcastradio-V2.0-java", "android.hardware.health-V1.0-java", "android.hardware.health-V2.0-java", + "android.hardware.light-java", "android.hardware.weaver-V1.0-java", "android.hardware.biometrics.face-V1.0-java", "android.hardware.biometrics.fingerprint-V2.1-java", diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java index a33fcd557369..8074900d2776 100644 --- a/services/core/java/com/android/server/BatteryService.java +++ b/services/core/java/com/android/server/BatteryService.java @@ -66,8 +66,8 @@ import com.android.internal.app.IBatteryStats; import com.android.internal.logging.MetricsLogger; import com.android.internal.util.DumpUtils; import com.android.server.am.BatteryStatsService; -import com.android.server.lights.Light; import com.android.server.lights.LightsManager; +import com.android.server.lights.LogicalLight; import java.io.File; import java.io.FileDescriptor; @@ -1065,7 +1065,7 @@ public final class BatteryService extends SystemService { } private final class Led { - private final Light mBatteryLight; + private final LogicalLight mBatteryLight; private final int mBatteryLowARGB; private final int mBatteryMediumARGB; @@ -1100,7 +1100,7 @@ public final class BatteryService extends SystemService { mBatteryLight.setColor(mBatteryLowARGB); } else { // Flash red when battery is low and not charging - mBatteryLight.setFlashing(mBatteryLowARGB, Light.LIGHT_FLASH_TIMED, + mBatteryLight.setFlashing(mBatteryLowARGB, LogicalLight.LIGHT_FLASH_TIMED, mBatteryLedOn, mBatteryLedOff); } } else if (status == BatteryManager.BATTERY_STATUS_CHARGING diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 1f17f9f1ca43..7e8fe3ae8428 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -41,8 +41,8 @@ import android.view.SurfaceControl; import com.android.internal.os.BackgroundThread; import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.LocalServices; -import com.android.server.lights.Light; import com.android.server.lights.LightsManager; +import com.android.server.lights.LogicalLight; import java.io.PrintWriter; import java.util.ArrayList; @@ -164,7 +164,7 @@ final class LocalDisplayAdapter extends DisplayAdapter { private final class LocalDisplayDevice extends DisplayDevice { private final long mPhysicalDisplayId; - private final Light mBacklight; + private final LogicalLight mBacklight; private final SparseArray<DisplayModeRecord> mSupportedModes = new SparseArray<>(); private final ArrayList<Integer> mSupportedColorModes = new ArrayList<>(); private final boolean mIsInternal; diff --git a/services/core/java/com/android/server/lights/LightsManager.java b/services/core/java/com/android/server/lights/LightsManager.java index be20a445432b..521913a0c439 100644 --- a/services/core/java/com/android/server/lights/LightsManager.java +++ b/services/core/java/com/android/server/lights/LightsManager.java @@ -29,5 +29,8 @@ public abstract class LightsManager { public static final int LIGHT_ID_WIFI = Type.WIFI; public static final int LIGHT_ID_COUNT = Type.COUNT; - public abstract Light getLight(int id); + /** + * Returns the logical light with the given type. + */ + public abstract LogicalLight getLight(int id); } diff --git a/services/core/java/com/android/server/lights/LightsService.java b/services/core/java/com/android/server/lights/LightsService.java index 8e6e1d6502de..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,223 @@ 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; - final LightImpl mLights[] = new LightImpl[LightsManager.LIGHT_ID_COUNT]; + 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 final class LightImpl extends Light { + 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; - private LightImpl(Context context, int id) { - mId = id; + private LightImpl(Context context, HwLight hwLight) { + mHwLight = hwLight; mDisplayToken = SurfaceControl.getInternalDisplayToken(); final boolean brightnessSupport = SurfaceControl.getDisplayBrightnessSupport( mDisplayToken); @@ -78,8 +269,8 @@ public class LightsService extends SystemService { synchronized (this) { // LOW_PERSISTENCE cannot be manually set if (brightnessMode == BRIGHTNESS_MODE_LOW_PERSISTENCE) { - Slog.w(TAG, "setBrightness with LOW_PERSISTENCE unexpected #" + mId + - ": brightness=0x" + Integer.toHexString(brightness)); + Slog.w(TAG, "setBrightness with LOW_PERSISTENCE unexpected #" + mHwLight.id + + ": brightness=0x" + Integer.toHexString(brightness)); return; } // Ideally, we'd like to set the brightness mode through the SF/HWC as well, but @@ -138,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); } } } @@ -185,8 +376,10 @@ public class LightsService extends SystemService { if (!mInitialized || color != mColor || mode != mMode || onMS != mOnMS || offMS != mOffMS || mBrightnessMode != brightnessMode) { - if (DEBUG) Slog.v(TAG, "setLight #" + mId + ": color=#" - + Integer.toHexString(color) + ": brightnessMode=" + brightnessMode); + if (DEBUG) { + Slog.v(TAG, "setLight #" + mHwLight.id + ": color=#" + + Integer.toHexString(color) + ": brightnessMode=" + brightnessMode); + } mInitialized = true; mLastColor = mColor; mColor = color; @@ -194,10 +387,31 @@ public class LightsService extends SystemService { mOnMS = onMS; mOffMS = offMS; mBrightnessMode = brightnessMode; - Trace.traceBegin(Trace.TRACE_TAG_POWER, "setLight(" + mId + ", 0x" - + Integer.toHexString(color) + ")"); + setLightUnchecked(color, mode, onMS, offMS, brightnessMode); + } + } + + private void setLightUnchecked(int color, int mode, int onMS, int offMS, + int brightnessMode) { + Trace.traceBegin(Trace.TRACE_TAG_POWER, "setLightState(" + mHwLight.id + ", 0x" + + Integer.toHexString(color) + ")"); + if (mVintfLights != null) { + HwLightState lightState = new HwLightState(); + lightState.color = color; + lightState.flashMode = (byte) mode; + lightState.flashOnMs = onMS; + lightState.flashOffMs = offMS; + lightState.brightnessMode = (byte) brightnessMode; try { - setLight_native(mId, color, mode, onMS, offMS, brightnessMode); + mVintfLights.setLightState(mHwLight.id, lightState); + } catch (RemoteException | UnsupportedOperationException ex) { + Slog.e(TAG, "Failed issuing setLightState", ex); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_POWER); + } + } else { + try { + setLight_native(mHwLight.id, color, mode, onMS, offMS, brightnessMode); } finally { Trace.traceEnd(Trace.TRACE_TAG_POWER); } @@ -208,7 +422,15 @@ public class LightsService extends SystemService { return mVrModeEnabled && mUseLowPersistenceForVR; } - private int mId; + private HwLight getHwLight() { + return mHwLight; + } + + private int getColor() { + return mColor; + } + + private HwLight mHwLight; private int mColor; private int mMode; private int mOnMS; @@ -223,16 +445,59 @@ public class LightsService extends SystemService { } public LightsService(Context context) { + 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); + } - for (int i = 0; i < LightsManager.LIGHT_ID_COUNT; i++) { - mLights[i] = new LightImpl(context, i); + private void populateAvailableLights(Context context) { + mLights = new LightImpl[LightsManager.LIGHT_ID_COUNT]; + mLightsById = new SparseArray<>(); + + if (mVintfLights != null) { + try { + for (HwLight availableLight : mVintfLights.getLights()) { + LightImpl light = new LightImpl(context, availableLight); + int type = (int) availableLight.type; + 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); + } + } + + // In the case where only the old HAL is available, all lights will be initialized here + for (int i = 0; i < mLights.length; i++) { + if (mLights[i] == null) { + // The ordinal can be anything if there is only 1 light of each type. Set it to 1. + HwLight light = new HwLight(); + light.id = (byte) i; + light.ordinal = 1; + light.type = (byte) i; + + mLights[i] = new LightImpl(context, light); + mLightsById.put(i, mLights[i]); + } } } @Override public void onStart() { publishLocalService(LightsManager.class, mService); + publishBinderService(Context.LIGHTS_SERVICE, mManagerService); } @Override @@ -249,22 +514,25 @@ public class LightsService extends SystemService { private final LightsManager mService = new LightsManager() { @Override - public Light getLight(int id) { - if (0 <= id && id < LIGHT_ID_COUNT) { - return mLights[id]; + public LogicalLight getLight(int lightType) { + if (mLights != null && 0 <= lightType && lightType < mLights.length) { + return mLights[lightType]; } else { return null; } } }; - 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/core/java/com/android/server/lights/Light.java b/services/core/java/com/android/server/lights/LogicalLight.java index 998c7c66b504..33dfbb4eea48 100644 --- a/services/core/java/com/android/server/lights/Light.java +++ b/services/core/java/com/android/server/lights/LogicalLight.java @@ -19,9 +19,24 @@ package com.android.server.lights; import android.hardware.light.V2_0.Brightness; import android.hardware.light.V2_0.Flash; -public abstract class Light { +/** + * Allow control over a logical light of a given type. The mapping of logical lights to physical + * lights is HAL implementation-dependent. + */ +public abstract class LogicalLight { + /** + * Keep the light steady on or off. + */ public static final int LIGHT_FLASH_NONE = Flash.NONE; + + /** + * Flash the light at specified rate. + */ public static final int LIGHT_FLASH_TIMED = Flash.TIMED; + + /** + * Flash the light using hardware assist. + */ public static final int LIGHT_FLASH_HARDWARE = Flash.HARDWARE; /** @@ -55,10 +70,33 @@ public abstract class Light { */ public abstract void setBrightnessFloat(float brightness); + /** + * Set the color of a light. + */ public abstract void setColor(int color); + + /** + * Set the color of a light and control flashing. + */ public abstract void setFlashing(int color, int mode, int onMS, int offMS); + + /** + * Pulses the light. + */ public abstract void pulse(); + + /** + * Pulses the light with a specified color for a specified duration. + */ public abstract void pulse(int color, int onMS); + + /** + * Turns off the light. + */ public abstract void turnOff(); + + /** + * Set the VR mode of a display. + */ public abstract void setVrMode(boolean enabled); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 6f43952a3f58..7cc67324032a 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -251,8 +251,8 @@ import com.android.server.IoThread; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.UiThread; -import com.android.server.lights.Light; import com.android.server.lights.LightsManager; +import com.android.server.lights.LogicalLight; import com.android.server.notification.ManagedServices.ManagedServiceInfo; import com.android.server.notification.ManagedServices.UserProfiles; import com.android.server.pm.PackageManagerService; @@ -413,8 +413,8 @@ public class NotificationManagerService extends SystemService { private final HandlerThread mRankingThread = new HandlerThread("ranker", Process.THREAD_PRIORITY_BACKGROUND); - private Light mNotificationLight; - Light mAttentionLight; + private LogicalLight mNotificationLight; + LogicalLight mAttentionLight; private long[] mFallbackVibrationPattern; private boolean mUseAttentionLight; @@ -1753,7 +1753,7 @@ public class NotificationManagerService extends SystemService { } @VisibleForTesting - void setLights(Light light) { + void setLights(LogicalLight light) { mNotificationLight = light; mAttentionLight = light; mNotificationPulseEnabled = true; @@ -7875,7 +7875,7 @@ public class NotificationManagerService extends SystemService { NotificationRecord.Light light = ledNotification.getLight(); if (light != null && mNotificationPulseEnabled) { // pulse repeatedly - mNotificationLight.setFlashing(light.color, Light.LIGHT_FLASH_TIMED, + mNotificationLight.setFlashing(light.color, LogicalLight.LIGHT_FLASH_TIMED, light.onMs, light.offMs); } } diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index ca368694ca92..3f3a13368ffc 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -16,8 +16,8 @@ package com.android.server.power; -import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; +import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT; import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP; import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE; import static android.os.PowerManagerInternal.WAKEFULNESS_DOZING; @@ -96,8 +96,8 @@ import com.android.server.SystemService; import com.android.server.UiThread; import com.android.server.Watchdog; import com.android.server.am.BatteryStatsService; -import com.android.server.lights.Light; import com.android.server.lights.LightsManager; +import com.android.server.lights.LogicalLight; import com.android.server.policy.WindowManagerPolicy; import com.android.server.power.batterysaver.BatterySaverController; import com.android.server.power.batterysaver.BatterySaverPolicy; @@ -257,7 +257,7 @@ public final class PowerManagerService extends SystemService private WirelessChargerDetector mWirelessChargerDetector; private SettingsObserver mSettingsObserver; private DreamManagerInternal mDreamManager; - private Light mAttentionLight; + private LogicalLight mAttentionLight; private InattentiveSleepWarningController mInattentiveSleepWarningOverlayController; @@ -3347,7 +3347,7 @@ public final class PowerManagerService extends SystemService } private void setAttentionLightInternal(boolean on, int color) { - Light light; + LogicalLight light; synchronized (mLock) { if (!mSystemReady) { return; @@ -3356,7 +3356,7 @@ public final class PowerManagerService extends SystemService } // Control light outside of lock. - light.setFlashing(color, Light.LIGHT_FLASH_HARDWARE, (on ? 3 : 0), 0); + light.setFlashing(color, LogicalLight.LIGHT_FLASH_HARDWARE, (on ? 3 : 0), 0); } private void setDozeAfterScreenOffInternal(boolean on) { 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); + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java index 1e55b1521956..587cfbf062fb 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java @@ -74,7 +74,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.IntPair; import com.android.server.UiServiceTestCase; -import com.android.server.lights.Light; +import com.android.server.lights.LogicalLight; import org.junit.Before; import org.junit.Test; @@ -91,7 +91,7 @@ public class BuzzBeepBlinkTest extends UiServiceTestCase { @Mock AudioManager mAudioManager; @Mock Vibrator mVibrator; @Mock android.media.IRingtonePlayer mRingtonePlayer; - @Mock Light mLight; + @Mock LogicalLight mLight; @Mock NotificationManagerService.WorkerHandler mHandler; @Mock diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 9260fbf97b86..93e09dfb3f57 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -153,8 +153,8 @@ import com.android.internal.util.FastXmlSerializer; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.UiServiceTestCase; -import com.android.server.lights.Light; import com.android.server.lights.LightsManager; +import com.android.server.lights.LogicalLight; import com.android.server.notification.NotificationManagerService.NotificationAssistants; import com.android.server.notification.NotificationManagerService.NotificationListeners; import com.android.server.uri.UriGrantsManagerInternal; @@ -372,7 +372,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { }); when(mPackageManagerClient.getPackageUidAsUser(any(), anyInt())).thenReturn(mUid); final LightsManager mockLightsManager = mock(LightsManager.class); - when(mockLightsManager.getLight(anyInt())).thenReturn(mock(Light.class)); + when(mockLightsManager.getLight(anyInt())).thenReturn(mock(LogicalLight.class)); when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL); when(mPackageManagerClient.hasSystemFeature(FEATURE_WATCH)).thenReturn(false); when(mUgmInternal.newUriPermissionOwner(anyString())).thenReturn(mPermOwner); |