diff options
17 files changed, 1196 insertions, 193 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 1296dd19cc7e..fec073f1199c 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -18575,6 +18575,58 @@ package android.hardware.input { } +package android.hardware.lights { + + public final class Light implements android.os.Parcelable { + method public int describeContents(); + method public int getId(); + method @NonNull public String getName(); + 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; + field public static final int LIGHT_TYPE_INPUT_PLAYER_ID = 10; // 0xa + field public static final int LIGHT_TYPE_INPUT_RGB = 11; // 0xb + field public static final int LIGHT_TYPE_INPUT_SINGLE = 9; // 0x9 + field public static final int LIGHT_TYPE_MICROPHONE = 8; // 0x8 + } + + public final class LightState implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public static android.hardware.lights.LightState forColor(@ColorInt int); + method @NonNull public static android.hardware.lights.LightState forPlayerId(int); + method @ColorInt public int getColor(); + method public int getPlayerId(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.hardware.lights.LightState> CREATOR; + } + + public abstract class LightsManager { + method @NonNull public abstract android.hardware.lights.LightState getLightState(@NonNull android.hardware.lights.Light); + method @NonNull public abstract java.util.List<android.hardware.lights.Light> getLights(); + method @NonNull public abstract android.hardware.lights.LightsManager.LightsSession openSession(); + } + + public abstract static class LightsManager.LightsSession implements java.lang.AutoCloseable { + ctor public LightsManager.LightsSession(); + method public abstract void close(); + method public abstract void requestLights(@NonNull android.hardware.lights.LightsRequest); + } + + public final class LightsRequest { + method @NonNull public java.util.List<android.hardware.lights.LightState> getLightStates(); + method @NonNull public java.util.List<java.lang.Integer> getLights(); + } + + public static final class LightsRequest.Builder { + ctor public LightsRequest.Builder(); + method @NonNull public android.hardware.lights.LightsRequest.Builder addLight(@NonNull android.hardware.lights.Light, @NonNull android.hardware.lights.LightState); + method @NonNull public android.hardware.lights.LightsRequest build(); + method @NonNull public android.hardware.lights.LightsRequest.Builder clearLight(@NonNull android.hardware.lights.Light); + } + +} + package android.hardware.usb { public class UsbAccessory implements android.os.Parcelable { @@ -46508,6 +46560,7 @@ package android.view { method public int getId(); method public android.view.KeyCharacterMap getKeyCharacterMap(); method public int getKeyboardType(); + method @NonNull public android.hardware.lights.LightsManager getLightsManager(); method public android.view.InputDevice.MotionRange getMotionRange(int); method public android.view.InputDevice.MotionRange getMotionRange(int, int); method public java.util.List<android.view.InputDevice.MotionRange> getMotionRanges(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index d7dc6ef45cb4..f8f591457355 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -3341,42 +3341,12 @@ 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 requestLights(@NonNull android.hardware.lights.LightsRequest); - } - - public final class LightsRequest { + ctor @Deprecated public LightState(@ColorInt int); } - 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); + public abstract class LightsManager { + field @Deprecated public static final int LIGHT_TYPE_MICROPHONE = 8; // 0x8 } } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 694507db6fe7..01417ae3c0ff 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -998,14 +998,6 @@ package android.hardware.input { } -package android.hardware.lights { - - public final class LightsManager { - method @NonNull @RequiresPermission(android.Manifest.permission.CONTROL_DEVICE_LIGHTS) public android.hardware.lights.LightState getLightState(@NonNull android.hardware.lights.Light); - } - -} - package android.hardware.soundtrigger { public class KeyphraseEnrollmentInfo { diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index d5e95708a805..8b0c41cb28e3 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -99,6 +99,7 @@ import android.hardware.input.InputManager; import android.hardware.iris.IIrisService; import android.hardware.iris.IrisManager; import android.hardware.lights.LightsManager; +import android.hardware.lights.SystemLightsManager; import android.hardware.location.ContextHubManager; import android.hardware.radio.RadioManager; import android.hardware.usb.IUsbManager; @@ -1331,7 +1332,7 @@ public final class SystemServiceRegistry { @Override public LightsManager createService(ContextImpl ctx) throws ServiceNotFoundException { - return new LightsManager(ctx); + return new SystemLightsManager(ctx); }}); registerService(Context.INCREMENTAL_SERVICE, IncrementalManager.class, new CachedServiceFetcher<IncrementalManager>() { diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index eaa38f3e862c..4743fee3257b 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -25,6 +25,8 @@ import android.hardware.input.TouchCalibration; import android.os.CombinedVibrationEffect; import android.hardware.input.IInputSensorEventListener; import android.hardware.input.InputSensorInfo; +import android.hardware.lights.Light; +import android.hardware.lights.LightState; import android.os.IBinder; import android.os.IVibratorStateListener; import android.os.VibrationEffect; @@ -127,4 +129,14 @@ interface IInputManager { void disableSensor(int deviceId, int sensorType); boolean flushSensor(int deviceId, int sensorType); + + List<Light> getLights(int deviceId); + + LightState getLightState(int deviceId, int lightId); + + void setLightStates(int deviceId, in int[] lightIds, in LightState[] states, in IBinder token); + + void openLightSession(int deviceId, String opPkg, in IBinder token); + + void closeLightSession(int deviceId, in IBinder token); } diff --git a/core/java/android/hardware/input/InputDeviceLightsManager.java b/core/java/android/hardware/input/InputDeviceLightsManager.java new file mode 100644 index 000000000000..a3b91a99fdb7 --- /dev/null +++ b/core/java/android/hardware/input/InputDeviceLightsManager.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2021 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.input; + +import android.annotation.NonNull; +import android.app.ActivityThread; +import android.hardware.lights.Light; +import android.hardware.lights.LightState; +import android.hardware.lights.LightsManager; +import android.hardware.lights.LightsRequest; +import android.util.CloseGuard; + +import com.android.internal.util.Preconditions; + +import java.lang.ref.Reference; +import java.util.List; + +/** + * LightsManager manages an input device's lights {@link android.hardware.input.Light}. + */ +class InputDeviceLightsManager extends LightsManager { + private static final String TAG = "InputDeviceLightsManager"; + private static final boolean DEBUG = false; + + private final InputManager mInputManager; + + // The input device ID. + private final int mDeviceId; + // Package name + private final String mPackageName; + + InputDeviceLightsManager(InputManager inputManager, int deviceId) { + super(ActivityThread.currentActivityThread().getSystemContext()); + mInputManager = inputManager; + mDeviceId = deviceId; + mPackageName = ActivityThread.currentPackageName(); + } + + /** + * Returns the lights available on the device. + * + * @return A list of available lights + */ + @Override + public @NonNull List<Light> getLights() { + return mInputManager.getLights(mDeviceId); + } + + /** + * Returns the state of a specified light. + * + * @hide + */ + @Override + public @NonNull LightState getLightState(@NonNull Light light) { + Preconditions.checkNotNull(light); + return mInputManager.getLightState(mDeviceId, light); + } + + /** + * Creates a new LightsSession that can be used to control the device lights. + */ + @Override + public @NonNull LightsSession openSession() { + final LightsSession session = new InputDeviceLightsSession(); + mInputManager.openLightSession(mDeviceId, mPackageName, session.getToken()); + return session; + } + + /** + * Encapsulates a session that can be used to control device lights and represents the lifetime + * of the requests. + */ + public final class InputDeviceLightsSession extends LightsManager.LightsSession + implements AutoCloseable { + + private final CloseGuard mCloseGuard = new CloseGuard(); + private boolean mClosed = false; + + /** + * Instantiated by {@link LightsManager#openSession()}. + */ + private InputDeviceLightsSession() { + mCloseGuard.open("close"); + } + + /** + * Sends a request to modify the states of multiple lights. + * + * @param request the settings for lights that should change + */ + @Override + public void requestLights(@NonNull LightsRequest request) { + Preconditions.checkNotNull(request); + Preconditions.checkArgument(!mClosed); + + mInputManager.requestLights(mDeviceId, request, getToken()); + } + + /** + * Closes the session, reverting all changes made through it. + */ + @Override + public void close() { + if (!mClosed) { + mInputManager.closeLightSession(mDeviceId, getToken()); + mClosed = true; + mCloseGuard.close(); + } + Reference.reachabilityFence(this); + } + + /** @hide */ + @Override + protected void finalize() throws Throwable { + try { + mCloseGuard.warnIfOpen(); + close(); + } finally { + super.finalize(); + } + } + } + +} diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 8a01c660ebd0..e15d6298d63d 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -30,6 +30,10 @@ import android.compat.annotation.ChangeId; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.hardware.SensorManager; +import android.hardware.lights.Light; +import android.hardware.lights.LightState; +import android.hardware.lights.LightsManager; +import android.hardware.lights.LightsRequest; import android.os.BlockUntrustedTouchesMode; import android.os.Build; import android.os.CombinedVibrationEffect; @@ -1409,7 +1413,7 @@ public final class InputManager { } /** - * Gets a vibrator service associated with an input device, always create a new instance. + * Gets a vibrator service associated with an input device, always creates a new instance. * @return The vibrator, never null. * @hide */ @@ -1418,7 +1422,7 @@ public final class InputManager { } /** - * Gets a vibrator manager service associated with an input device, always create a new + * Gets a vibrator manager service associated with an input device, always creates a new * instance. * @return The vibrator manager, never null. * @hide @@ -1486,10 +1490,8 @@ public final class InputManager { /** * Register input device vibrator state listener - * - * @hide */ - public boolean registerVibratorStateListener(int deviceId, IVibratorStateListener listener) { + boolean registerVibratorStateListener(int deviceId, IVibratorStateListener listener) { try { return mIm.registerVibratorStateListener(deviceId, listener); } catch (RemoteException ex) { @@ -1499,10 +1501,8 @@ public final class InputManager { /** * Unregister input device vibrator state listener - * - * @hide */ - public boolean unregisterVibratorStateListener(int deviceId, IVibratorStateListener listener) { + boolean unregisterVibratorStateListener(int deviceId, IVibratorStateListener listener) { try { return mIm.unregisterVibratorStateListener(deviceId, listener); } catch (RemoteException ex) { @@ -1511,7 +1511,7 @@ public final class InputManager { } /** - * Gets a sensor manager service associated with an input device, always create a new instance. + * Gets a sensor manager service associated with an input device, always creates a new instance. * @return The sensor manager, never null. * @hide */ @@ -1533,6 +1533,86 @@ public final class InputManager { } /** + * Gets a lights manager associated with an input device, always creates a new instance. + * @return The lights manager, never null. + * @hide + */ + @NonNull + public LightsManager getInputDeviceLightsManager(int deviceId) { + return new InputDeviceLightsManager(InputManager.this, deviceId); + } + + /** + * Gets a list of light objects associated with an input device. + * @return The list of lights, never null. + */ + @NonNull List<Light> getLights(int deviceId) { + try { + return mIm.getLights(deviceId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the state of an input device light. + * @return the light state + */ + @NonNull LightState getLightState(int deviceId, @NonNull Light light) { + try { + return mIm.getLightState(deviceId, light.getId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Request to modify the states of multiple lights. + * + * @param request the settings for lights that should change + */ + void requestLights(int deviceId, @NonNull LightsRequest request, IBinder token) { + try { + List<Integer> lightIdList = request.getLights(); + int[] lightIds = new int[lightIdList.size()]; + for (int i = 0; i < lightIds.length; i++) { + lightIds[i] = lightIdList.get(i); + } + List<LightState> lightStateList = request.getLightStates(); + mIm.setLightStates(deviceId, lightIds, + lightStateList.toArray(new LightState[lightStateList.size()]), + token); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Open light session for input device manager + * + * @param token The token for the light session + */ + void openLightSession(int deviceId, String opPkg, @NonNull IBinder token) { + try { + mIm.openLightSession(deviceId, opPkg, token); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Close light session + * + */ + void closeLightSession(int deviceId, @NonNull IBinder token) { + try { + mIm.closeLightSession(deviceId, token); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Listens for changes in input devices. */ public interface InputDeviceListener { diff --git a/core/java/android/hardware/lights/Light.java b/core/java/android/hardware/lights/Light.java index da270182052d..7bfff5d3af97 100644 --- a/core/java/android/hardware/lights/Light.java +++ b/core/java/android/hardware/lights/Light.java @@ -16,22 +16,56 @@ package android.hardware.lights; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * Represents a logical light on the device. * - * @hide */ -@SystemApi public final class Light implements Parcelable { + // 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, 8 for microphone light is + // defined in {@link android.hardware.lights.LightsManager}, following types are available + // through this API. + /** Type for lights that indicate microphone usage */ + public static final int LIGHT_TYPE_MICROPHONE = 8; + + /** + * Type for lights that indicate a monochrome color LED light. + */ + public static final int LIGHT_TYPE_INPUT_SINGLE = 9; + + /** + * Type for lights that indicate a group of LED lights representing player ID. + */ + public static final int LIGHT_TYPE_INPUT_PLAYER_ID = 10; + + /** + * Type for lights that indicate a color LED light. + */ + public static final int LIGHT_TYPE_INPUT_RGB = 11; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"LIGHT_TYPE_"}, + value = { + LIGHT_TYPE_INPUT_PLAYER_ID, + LIGHT_TYPE_INPUT_SINGLE, + LIGHT_TYPE_INPUT_RGB, + }) + public @interface LightType {} + private final int mId; private final int mOrdinal; private final int mType; + private final String mName; /** * Creates a new light with the given data. @@ -39,15 +73,26 @@ public final class Light implements Parcelable { * @hide */ public Light(int id, int ordinal, int type) { + this(id, ordinal, type, "Light"); + } + + /** + * Creates a new light with the given data. + * + * @hide + */ + public Light(int id, int ordinal, int type, String name) { mId = id; mOrdinal = ordinal; mType = type; + mName = name; } private Light(@NonNull Parcel in) { mId = in.readInt(); mOrdinal = in.readInt(); mType = in.readInt(); + mName = in.readString(); } /** Implement the Parcelable interface */ @@ -56,6 +101,7 @@ public final class Light implements Parcelable { dest.writeInt(mId); dest.writeInt(mOrdinal); dest.writeInt(mType); + dest.writeString(mName); } /** Implement the Parcelable interface */ @@ -100,6 +146,14 @@ public final class Light implements Parcelable { } /** + * Returns the name of the light. + */ + @NonNull + public String getName() { + return mName; + } + + /** * Returns the ordinal of the light. * * <p>This is a sort key that represents the physical order of lights on the device with the diff --git a/core/java/android/hardware/lights/LightState.java b/core/java/android/hardware/lights/LightState.java index cd39e6df91a9..650b383eeb0f 100644 --- a/core/java/android/hardware/lights/LightState.java +++ b/core/java/android/hardware/lights/LightState.java @@ -32,36 +32,93 @@ import android.os.Parcelable; * will be converted to only a brightness value and that will be used for the light's single * channel. * - * @hide */ -@SystemApi public final class LightState implements Parcelable { private final int mColor; + private final int mPlayerId; /** - * Creates a new LightState with the desired color and intensity. + * Creates a new LightState with the desired color and intensity, for a light type + * of RBG color or monochrome color. * * @param color the desired color and intensity in ARGB format. + * @deprecated this has been replaced with {@link android.hardware.lights.LightState#forColor } + * @hide */ + @Deprecated + @SystemApi public LightState(@ColorInt int color) { + this(color, 0); + } + + /** + * Creates a new LightState with the desired color and intensity, and the player Id. + * Player Id will only be applied on Light type + * {@link android.hardware.lights.Light#LIGHT_TYPE_INPUT_PLAYER_ID} + * + * @param color the desired color and intensity in ARGB format. + * @hide + */ + public LightState(@ColorInt int color, int playerId) { mColor = color; + mPlayerId = playerId; + } + + /** + * Creates a new LightState with the desired color and intensity, for a light type + * of RBG color or single monochrome color. + * + * @param color the desired color and intensity in ARGB format. + * @return The LightState object contains the color. + */ + @NonNull + public static LightState forColor(@ColorInt int color) { + return new LightState(color, 0); + } + + /** + * Creates a new LightState with the desired player id, for a light of type + * {@link android.hardware.lights.Light#LIGHT_TYPE_INPUT_PLAYER_ID}. + * + * @param playerId the desired player id. + * @return The LightState object contains the player id. + */ + @NonNull + public static LightState forPlayerId(int playerId) { + return new LightState(0, playerId); } + /** + * Creates a new LightState from a parcel object. + */ private LightState(@NonNull Parcel in) { mColor = in.readInt(); + mPlayerId = in.readInt(); } /** - * Return the color and intensity associated with this LightState. - * @return the color and intensity in ARGB format. The A channel is ignored. + * Returns the color and intensity associated with this LightState. + * @return the color and intensity in ARGB format. The A channel is ignored. return 0 when + * calling LightsManager.getLightState with LIGHT_TYPE_INPUT_PLAYER_ID. */ public @ColorInt int getColor() { return mColor; } + /** + * Returns the player ID associated with this LightState for Light type + * {@link android.hardware.lights.Light#LIGHT_TYPE_INPUT_PLAYER_ID}, + * or 0 for other types. + * @return the player ID. + */ + public int getPlayerId() { + return mPlayerId; + } + @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mColor); + dest.writeInt(mPlayerId); } @Override @@ -69,6 +126,12 @@ public final class LightState implements Parcelable { return 0; } + @Override + public String toString() { + return "LightState{Color=0x" + Integer.toHexString(mColor) + ", PlayerId=" + + mPlayerId + "}"; + } + public static final @NonNull Parcelable.Creator<LightState> CREATOR = new Parcelable.Creator<LightState>() { public LightState createFromParcel(Parcel in) { diff --git a/core/java/android/hardware/lights/LightsManager.java b/core/java/android/hardware/lights/LightsManager.java index 33e5fcaf2abb..8fd56db33c4b 100644 --- a/core/java/android/hardware/lights/LightsManager.java +++ b/core/java/android/hardware/lights/LightsManager.java @@ -16,43 +16,38 @@ 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 @SystemService(Context.LIGHTS_SERVICE) -public final class LightsManager { +public abstract class LightsManager { private static final String TAG = "LightsManager"; + @NonNull private final Context mContext; // 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 */ + // and following types are available through this API. + /** Type for lights that indicate microphone usage + * @deprecated this has been moved to {@link android.hardware.lights.Light } + * @hide + */ + @Deprecated + @SystemApi public static final int LIGHT_TYPE_MICROPHONE = 8; /** @hide */ @@ -63,28 +58,11 @@ public final class LightsManager { }) public @interface LightType {} - @NonNull private final Context mContext; - @NonNull private final ILightsManager mService; - /** - * Creates a LightsManager. - * - * @hide + * @hide to prevent subclassing from outside of the framework */ - 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) { + public LightsManager(Context context) { mContext = Preconditions.checkNotNull(context); - mService = Preconditions.checkNotNull(service); } /** @@ -92,112 +70,44 @@ public final class LightsManager { * * @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(); - } - } + public @NonNull abstract List<Light> getLights(); /** * 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(); - } - } + public abstract @NonNull LightState getLightState(@NonNull Light light); /** * 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(); - } - } + public abstract @NonNull LightsSession openSession(); /** * Encapsulates a session that can be used to control device lights and represents the lifetime * of the requests. */ - public final class LightsSession implements AutoCloseable { - + public abstract static 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 requestLights(@NonNull LightsRequest request) { - Preconditions.checkNotNull(request); - if (!mClosed) { - try { - mService.setLightStates(mToken, request.mLightIds, request.mLightStates); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - } + public abstract void requestLights(@NonNull LightsRequest request); - /** - * 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); - } + public abstract void close(); - /** @hide */ - @Override - protected void finalize() throws Throwable { - try { - mCloseGuard.warnIfOpen(); - close(); - } finally { - super.finalize(); - } + /** + * Get the token of a light session. + * + * @return Binder token of the light session. + * @hide + */ + public @NonNull IBinder getToken() { + return mToken; } } + } diff --git a/core/java/android/hardware/lights/LightsRequest.java b/core/java/android/hardware/lights/LightsRequest.java index a318992c35ee..2626a461aaf5 100644 --- a/core/java/android/hardware/lights/LightsRequest.java +++ b/core/java/android/hardware/lights/LightsRequest.java @@ -17,17 +17,17 @@ package android.hardware.lights; import android.annotation.NonNull; -import android.annotation.SystemApi; import android.util.SparseArray; import com.android.internal.util.Preconditions; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; /** * Encapsulates a request to modify the state of multiple lights. * - * @hide */ -@SystemApi public final class LightsRequest { /** Visible to {@link LightsManager.Session}. */ @@ -50,6 +50,30 @@ public final class LightsRequest { } /** + * Get a list of Light as ids. The ids will returned in same order as the lights passed + * in Builder. + * + * @return List of light ids + */ + public @NonNull List<Integer> getLights() { + List<Integer> lightList = new ArrayList<Integer>(mLightIds.length); + for (int i = 0; i < mLightIds.length; i++) { + lightList.add(mLightIds[i]); + } + return lightList; + } + + /** + * Get a list of LightState. The states will be returned in same order as the light states + * passed in Builder. + * + * @return List of light states + */ + public @NonNull List<LightState> getLightStates() { + return Arrays.asList(mLightStates); + } + + /** * Builder for creating device light change requests. */ public static final class Builder { @@ -62,7 +86,7 @@ public final class LightsRequest { * @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) { + public @NonNull Builder addLight(@NonNull Light light, @NonNull LightState state) { Preconditions.checkNotNull(light); Preconditions.checkNotNull(state); mChanges.put(light.getId(), state); diff --git a/core/java/android/hardware/lights/SystemLightsManager.java b/core/java/android/hardware/lights/SystemLightsManager.java new file mode 100644 index 000000000000..726a61359c01 --- /dev/null +++ b/core/java/android/hardware/lights/SystemLightsManager.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2021 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.NonNull; +import android.annotation.RequiresPermission; +import android.content.Context; +import android.hardware.lights.LightsManager.LightsSession; +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.ref.Reference; +import java.util.List; + +/** + * The LightsManager class allows control over device lights. + * + * @hide + */ +public final class SystemLightsManager extends LightsManager { + private static final String TAG = "LightsManager"; + + @NonNull private final ILightsManager mService; + + /** + * Creates a SystemLightsManager. + * + * @hide + */ + public SystemLightsManager(@NonNull Context context) throws ServiceNotFoundException { + this(context, ILightsManager.Stub.asInterface( + ServiceManager.getServiceOrThrow(Context.LIGHTS_SERVICE))); + } + + /** + * Creates a SystemLightsManager with a provided service implementation. + * + * @hide + */ + @VisibleForTesting + public SystemLightsManager(@NonNull Context context, @NonNull ILightsManager service) { + super(context); + mService = Preconditions.checkNotNull(service); + } + + /** + * Returns the lights available on the device. + * + * @return A list of available lights + */ + @RequiresPermission(Manifest.permission.CONTROL_DEVICE_LIGHTS) + @Override + 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) + @Override + 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) + @Override + public @NonNull LightsSession openSession() { + try { + final LightsSession session = new SystemLightsSession(); + mService.openSession(session.getToken()); + 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 SystemLightsSession extends LightsManager.LightsSession + implements AutoCloseable { + + private final CloseGuard mCloseGuard = new CloseGuard(); + private boolean mClosed = false; + + /** + * Instantiated by {@link LightsManager#openSession()}. + */ + @RequiresPermission(Manifest.permission.CONTROL_DEVICE_LIGHTS) + private SystemLightsSession() { + 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) + @Override + public void requestLights(@NonNull LightsRequest request) { + Preconditions.checkNotNull(request); + if (!mClosed) { + try { + mService.setLightStates(getToken(), 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(getToken()); + 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/view/InputDevice.java b/core/java/android/view/InputDevice.java index 59e493191711..fafb885c58e0 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -26,6 +26,7 @@ import android.hardware.Battery; import android.hardware.SensorManager; import android.hardware.input.InputDeviceIdentifier; import android.hardware.input.InputManager; +import android.hardware.lights.LightsManager; import android.os.Build; import android.os.NullVibrator; import android.os.Parcel; @@ -89,6 +90,9 @@ public final class InputDevice implements Parcelable { @GuardedBy("mMotionRanges") private Battery mBattery; + @GuardedBy("mMotionRanges") + private LightsManager mLightsManager; + /** * A mask for input source classes. * @@ -859,6 +863,21 @@ public final class InputDevice implements Parcelable { } /** + * Gets the lights manager associated with the device, if there is one. + * Even if the device does not have lights, the result is never null. + * Use {@link LightsManager#getLights} to determine whether any lights is + * present. + * + * @return The lights manager associated with the device, never null. + */ + public @NonNull LightsManager getLightsManager() { + if (mLightsManager == null) { + mLightsManager = InputManager.getInstance().getInputDeviceLightsManager(mId); + } + return mLightsManager; + } + + /** * Gets the sensor manager service associated with the input device. * Even if the device does not have a sensor, the result is never null. * Use {@link SensorManager#getSensorList} to get a full list of all supported sensors. diff --git a/core/tests/coretests/src/android/hardware/input/InputDeviceLightsManagerTest.java b/core/tests/coretests/src/android/hardware/input/InputDeviceLightsManagerTest.java new file mode 100644 index 000000000000..412b36713fa2 --- /dev/null +++ b/core/tests/coretests/src/android/hardware/input/InputDeviceLightsManagerTest.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2021 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.input; + +import static android.hardware.lights.LightsRequest.Builder; + +import static com.google.common.truth.Truth.assertThat; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNotNull; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.lights.Light; +import android.hardware.lights.LightState; +import android.hardware.lights.LightsManager; +import android.os.IBinder; +import android.platform.test.annotations.Presubmit; +import android.util.ArrayMap; +import android.view.InputDevice; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.junit.MockitoRule; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Tests for {@link InputDeviceLightsManager}. + * + * Build/Install/Run: + * atest FrameworksCoreTests:InputDeviceLightsManagerTest + */ +@Presubmit +@RunWith(MockitoJUnitRunner.class) +public class InputDeviceLightsManagerTest { + private static final String TAG = "InputDeviceLightsManagerTest"; + + private static final int DEVICE_ID = 1000; + private static final int PLAYER_ID = 3; + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private InputManager mInputManager; + + @Mock private IInputManager mIInputManagerMock; + + @Before + public void setUp() throws Exception { + when(mIInputManagerMock.getInputDeviceIds()).thenReturn(new int[]{DEVICE_ID}); + + when(mIInputManagerMock.getInputDevice(eq(DEVICE_ID))).thenReturn( + createInputDevice(DEVICE_ID)); + + mInputManager = InputManager.resetInstance(mIInputManagerMock); + + ArrayMap<Integer, LightState> lightStatesById = new ArrayMap<>(); + doAnswer(invocation -> { + final int[] lightIds = (int[]) invocation.getArguments()[1]; + final LightState[] lightStates = + (LightState[]) invocation.getArguments()[2]; + for (int i = 0; i < lightIds.length; i++) { + lightStatesById.put(lightIds[i], lightStates[i]); + } + return null; + }).when(mIInputManagerMock).setLightStates(eq(DEVICE_ID), + any(int[].class), any(LightState[].class), any(IBinder.class)); + + doAnswer(invocation -> { + int lightId = (int) invocation.getArguments()[1]; + if (lightStatesById.containsKey(lightId)) { + return lightStatesById.get(lightId); + } + return new LightState(0); + }).when(mIInputManagerMock).getLightState(eq(DEVICE_ID), anyInt()); + } + + @After + public void tearDown() { + InputManager.clearInstance(); + } + + private InputDevice createInputDevice(int id) { + return new InputDevice(id, 0 /* generation */, 0 /* controllerNumber */, "name", + 0 /* vendorId */, 0 /* productId */, "descriptor", true /* isExternal */, + 0 /* sources */, 0 /* keyboardType */, null /* keyCharacterMap */, + false /* hasVibrator */, false /* hasMicrophone */, false /* hasButtonUnderpad */, + false /* hasSensor */, false /* hasBattery */); + } + + private void mockLights(Light[] lights) throws Exception { + // Mock the Lights returned form InputManagerService + when(mIInputManagerMock.getLights(eq(DEVICE_ID))).thenReturn( + new ArrayList(Arrays.asList(lights))); + } + + @Test + public void testGetInputDeviceLights() throws Exception { + InputDevice device = mInputManager.getInputDevice(DEVICE_ID); + assertNotNull(device); + + Light[] mockedLights = { + new Light(1 /* id */, 0 /* ordinal */, Light.LIGHT_TYPE_INPUT_SINGLE), + new Light(2 /* id */, 0 /* ordinal */, Light.LIGHT_TYPE_INPUT_RGB), + new Light(3 /* id */, 0 /* ordinal */, Light.LIGHT_TYPE_INPUT_PLAYER_ID) + }; + mockLights(mockedLights); + + LightsManager lightsManager = device.getLightsManager(); + List<Light> lights = lightsManager.getLights(); + verify(mIInputManagerMock).getLights(eq(DEVICE_ID)); + assertEquals(lights, Arrays.asList(mockedLights)); + } + + @Test + public void testControlMultipleLights() throws Exception { + InputDevice device = mInputManager.getInputDevice(DEVICE_ID); + assertNotNull(device); + + Light[] mockedLights = { + new Light(1 /* id */, 0 /* ordinal */, Light.LIGHT_TYPE_INPUT_RGB), + new Light(2 /* id */, 0 /* ordinal */, Light.LIGHT_TYPE_INPUT_RGB), + new Light(3 /* id */, 0 /* ordinal */, Light.LIGHT_TYPE_INPUT_RGB), + new Light(4 /* id */, 0 /* ordinal */, Light.LIGHT_TYPE_INPUT_RGB) + }; + mockLights(mockedLights); + + LightsManager lightsManager = device.getLightsManager(); + List<Light> lightList = lightsManager.getLights(); + LightState[] states = new LightState[]{new LightState(0xf1), new LightState(0xf2), + new LightState(0xf3)}; + // Open a session to request turn 3/4 lights on: + LightsManager.LightsSession session = lightsManager.openSession(); + session.requestLights(new Builder() + .addLight(lightsManager.getLights().get(0), states[0]) + .addLight(lightsManager.getLights().get(1), states[1]) + .addLight(lightsManager.getLights().get(2), states[2]) + .build()); + IBinder token = session.getToken(); + + verify(mIInputManagerMock).openLightSession(eq(DEVICE_ID), + any(String.class), eq(token)); + verify(mIInputManagerMock).setLightStates(eq(DEVICE_ID), eq(new int[]{1, 2, 3}), + eq(states), eq(token)); + + // Then all 3 should turn on. + assertThat(lightsManager.getLightState(lightsManager.getLights().get(0)).getColor()) + .isEqualTo(0xf1); + assertThat(lightsManager.getLightState(lightsManager.getLights().get(1)).getColor()) + .isEqualTo(0xf2); + assertThat(lightsManager.getLightState(lightsManager.getLights().get(2)).getColor()) + .isEqualTo(0xf3); + + // And the 4th should remain off. + assertThat(lightsManager.getLightState(lightsManager.getLights().get(3)).getColor()) + .isEqualTo(0x00); + + // close session + session.close(); + verify(mIInputManagerMock).closeLightSession(eq(DEVICE_ID), eq(token)); + } + + @Test + public void testControlPlayerIdLight() throws Exception { + InputDevice device = mInputManager.getInputDevice(DEVICE_ID); + assertNotNull(device); + + Light[] mockedLights = { + new Light(1 /* id */, 0 /* ordinal */, Light.LIGHT_TYPE_INPUT_PLAYER_ID), + new Light(2 /* id */, 0 /* ordinal */, Light.LIGHT_TYPE_INPUT_SINGLE), + new Light(3 /* id */, 0 /* ordinal */, Light.LIGHT_TYPE_INPUT_RGB), + }; + mockLights(mockedLights); + + LightsManager lightsManager = device.getLightsManager(); + List<Light> lightList = lightsManager.getLights(); + LightState[] states = new LightState[]{new LightState(0xf1, PLAYER_ID)}; + // Open a session to request set Player ID light: + LightsManager.LightsSession session = lightsManager.openSession(); + session.requestLights(new Builder() + .addLight(lightsManager.getLights().get(0), states[0]) + .build()); + IBinder token = session.getToken(); + + verify(mIInputManagerMock).openLightSession(eq(DEVICE_ID), + any(String.class), eq(token)); + verify(mIInputManagerMock).setLightStates(eq(DEVICE_ID), eq(new int[]{1}), + eq(states), eq(token)); + + // Verify the light state + assertThat(lightsManager.getLightState(lightsManager.getLights().get(0)).getColor()) + .isEqualTo(0xf1); + assertThat(lightsManager.getLightState(lightsManager.getLights().get(0)).getPlayerId()) + .isEqualTo(PLAYER_ID); + + // close session + session.close(); + verify(mIInputManagerMock).closeLightSession(eq(DEVICE_ID), eq(token)); + } +} diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 4e974112a5c3..096cb0743670 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -17,6 +17,7 @@ package com.android.server.input; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -50,6 +51,8 @@ import android.hardware.input.InputManagerInternal.LidSwitchCallback; import android.hardware.input.InputSensorInfo; import android.hardware.input.KeyboardLayout; import android.hardware.input.TouchCalibration; +import android.hardware.lights.Light; +import android.hardware.lights.LightState; import android.media.AudioManager; import android.os.Binder; import android.os.Bundle; @@ -232,6 +235,13 @@ public class InputManagerService extends IInputManager.Stub // List of vibrator states by device id. @GuardedBy("mVibratorLock") private final SparseBooleanArray mIsVibrating = new SparseBooleanArray(); + private Object mLightLock = new Object(); + // State for light tokens. A light token marks a lights manager session, it is generated + // by light session open() and deleted in session close(). + // When lights session requests light states, the token will be used to find the light session. + @GuardedBy("mLightLock") + private final ArrayMap<IBinder, LightSession> mLightSessions = + new ArrayMap<IBinder, LightSession>(); // State for lid switch private final Object mLidSwitchLock = new Object(); @@ -298,6 +308,12 @@ public class InputManagerService extends IInputManager.Stub private static native int[] nativeGetVibratorIds(long ptr, int deviceId); private static native int nativeGetBatteryCapacity(long ptr, int deviceId); private static native int nativeGetBatteryStatus(long ptr, int deviceId); + private static native List<Light> nativeGetLights(long ptr, int deviceId); + private static native int nativeGetLightPlayerId(long ptr, int deviceId, int lightId); + private static native int nativeGetLightColor(long ptr, int deviceId, int lightId); + private static native void nativeSetLightPlayerId(long ptr, int deviceId, int lightId, + int playerId); + private static native void nativeSetLightColor(long ptr, int deviceId, int lightId, int color); private static native void nativeReloadKeyboardLayouts(long ptr); private static native void nativeReloadDeviceAliases(long ptr); private static native String nativeDump(long ptr); @@ -2246,6 +2262,151 @@ public class InputManagerService extends IInputManager.Stub } } + /** + * LightSession represents a light session for lights manager. + */ + private final class LightSession implements DeathRecipient { + private final int mDeviceId; + private final IBinder mToken; + private final String mOpPkg; + // The light ids and states that are requested by the light seesion + private int[] mLightIds; + private LightState[] mLightStates; + + LightSession(int deviceId, String opPkg, IBinder token) { + mDeviceId = deviceId; + mOpPkg = opPkg; + mToken = token; + } + + @Override + public void binderDied() { + if (DEBUG) { + Slog.d(TAG, "Light token died."); + } + synchronized (mLightLock) { + closeLightSession(mDeviceId, mToken); + mLightSessions.remove(mToken); + } + } + } + + /** + * Returns the lights available for apps to control on the specified input device. + * Only lights that aren't reserved for system use are available to apps. + */ + @Override // Binder call + public List<Light> getLights(int deviceId) { + return nativeGetLights(mPtr, deviceId); + } + + /** + * Set specified light state with for a specific input device. + */ + private void setLightStateInternal(int deviceId, Light light, LightState lightState) { + Preconditions.checkNotNull(light, "light does not exist"); + if (DEBUG) { + Slog.d(TAG, "setLightStateInternal device " + deviceId + " light " + light + + "lightState " + lightState); + } + if (light.getType() == Light.LIGHT_TYPE_INPUT_PLAYER_ID) { + nativeSetLightPlayerId(mPtr, deviceId, light.getId(), lightState.getPlayerId()); + } else if (light.getType() == Light.LIGHT_TYPE_INPUT_SINGLE + || light.getType() == Light.LIGHT_TYPE_INPUT_RGB) { + // Set ARGB format color to input device light + // Refer to https://developer.android.com/reference/kotlin/android/graphics/Color + nativeSetLightColor(mPtr, deviceId, light.getId(), lightState.getColor()); + } else { + Slog.e(TAG, "setLightStates for unsupported light type " + light.getType()); + } + } + + /** + * Set multiple light states with multiple light ids for a specific input device. + */ + private void setLightStatesInternal(int deviceId, int[] lightIds, LightState[] lightStates) { + final List<Light> lights = nativeGetLights(mPtr, deviceId); + SparseArray<Light> lightArray = new SparseArray<>(); + for (int i = 0; i < lights.size(); i++) { + lightArray.put(lights.get(i).getId(), lights.get(i)); + } + for (int i = 0; i < lightIds.length; i++) { + if (lightArray.contains(lightIds[i])) { + setLightStateInternal(deviceId, lightArray.get(lightIds[i]), lightStates[i]); + } + } + } + + /** + * Set states for multiple lights for an opened light session. + */ + @Override + public void setLightStates(int deviceId, int[] lightIds, LightState[] lightStates, + IBinder token) { + Preconditions.checkArgument(lightIds.length == lightStates.length, + "lights and light states are not same length"); + synchronized (mLightLock) { + LightSession lightSession = mLightSessions.get(token); + Preconditions.checkArgument(lightSession != null, "not registered"); + Preconditions.checkState(lightSession.mDeviceId == deviceId, "Incorrect device ID"); + lightSession.mLightIds = lightIds.clone(); + lightSession.mLightStates = lightStates.clone(); + if (DEBUG) { + Slog.d(TAG, "setLightStates for " + lightSession.mOpPkg + " device " + deviceId); + } + } + setLightStatesInternal(deviceId, lightIds, lightStates); + } + + @Override + public @Nullable LightState getLightState(int deviceId, int lightId) { + synchronized (mLightLock) { + int color = nativeGetLightColor(mPtr, deviceId, lightId); + int playerId = nativeGetLightPlayerId(mPtr, deviceId, lightId); + + return new LightState(color, playerId); + } + } + + @Override + public void openLightSession(int deviceId, String opPkg, IBinder token) { + Preconditions.checkNotNull(token); + synchronized (mLightLock) { + Preconditions.checkState(mLightSessions.get(token) == null, "already registered"); + LightSession lightSession = new LightSession(deviceId, opPkg, token); + try { + token.linkToDeath(lightSession, 0); + } catch (RemoteException ex) { + // give up + ex.rethrowAsRuntimeException(); + } + mLightSessions.put(token, lightSession); + if (DEBUG) { + Slog.d(TAG, "Open light session for " + opPkg + " device " + deviceId); + } + } + } + + @Override + public void closeLightSession(int deviceId, IBinder token) { + Preconditions.checkNotNull(token); + synchronized (mLightLock) { + LightSession lightSession = mLightSessions.get(token); + Preconditions.checkState(lightSession != null, "not registered"); + // Turn off the lights that were previously requested by the session to be closed. + Arrays.fill(lightSession.mLightStates, new LightState(0)); + setLightStatesInternal(deviceId, lightSession.mLightIds, + lightSession.mLightStates); + mLightSessions.remove(token); + // If any other session is still pending with light request, apply the first session's + // request. + if (!mLightSessions.isEmpty()) { + LightSession nextSession = mLightSessions.valueAt(0); + setLightStatesInternal(deviceId, nextSession.mLightIds, nextSession.mLightStates); + } + } + } + @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 643503d18bed..21d57d8c189f 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -158,6 +158,20 @@ static struct { static struct { jclass clazz; jmethodID constructor; + jfieldID lightTypeSingle; + jfieldID lightTypePlayerId; + jfieldID lightTypeRgb; +} gLightClassInfo; + +static struct { + jclass clazz; + jmethodID constructor; + jmethodID add; +} gArrayListClassInfo; + +static struct { + jclass clazz; + jmethodID constructor; jmethodID keyAt; jmethodID valueAt; jmethodID size; @@ -1923,6 +1937,79 @@ static jintArray nativeGetVibratorIds(JNIEnv* env, jclass clazz, jlong ptr, jint return vibIdArray; } +static jobject nativeGetLights(JNIEnv* env, jclass clazz, jlong ptr, jint deviceId) { + NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr); + jobject jLights = env->NewObject(gArrayListClassInfo.clazz, gArrayListClassInfo.constructor); + + std::vector<int> lightIds = im->getInputManager()->getReader()->getLightIds(deviceId); + + for (size_t i = 0; i < lightIds.size(); i++) { + const InputDeviceLightInfo* lightInfo = + im->getInputManager()->getReader()->getLightInfo(deviceId, lightIds[i]); + if (lightInfo == nullptr) { + ALOGW("Failed to get input device %d light info for id %d", deviceId, lightIds[i]); + continue; + } + + jint jTypeId = 0; + if (lightInfo->type == InputDeviceLightType::SINGLE) { + jTypeId = + env->GetStaticIntField(gLightClassInfo.clazz, gLightClassInfo.lightTypeSingle); + } else if (lightInfo->type == InputDeviceLightType::PLAYER_ID) { + jTypeId = env->GetStaticIntField(gLightClassInfo.clazz, + gLightClassInfo.lightTypePlayerId); + } else if (lightInfo->type == InputDeviceLightType::RGB || + lightInfo->type == InputDeviceLightType::MULTI_COLOR) { + jTypeId = env->GetStaticIntField(gLightClassInfo.clazz, gLightClassInfo.lightTypeRgb); + } else { + ALOGW("Unknown light type %d", lightInfo->type); + continue; + } + ScopedLocalRef<jobject> + lightObj(env, + env->NewObject(gLightClassInfo.clazz, gLightClassInfo.constructor, + (jint)lightInfo->id, (jint)lightInfo->ordinal, jTypeId, + env->NewStringUTF(lightInfo->name.c_str()))); + // Add light object to list + env->CallBooleanMethod(jLights, gArrayListClassInfo.add, lightObj.get()); + } + + return jLights; +} + +static jint nativeGetLightPlayerId(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId, + jint lightId) { + NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr); + + std::optional<int32_t> ret = + im->getInputManager()->getReader()->getLightPlayerId(deviceId, lightId); + + return static_cast<jint>(ret.value_or(0)); +} + +static jint nativeGetLightColor(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId, + jint lightId) { + NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr); + + std::optional<int32_t> ret = + im->getInputManager()->getReader()->getLightColor(deviceId, lightId); + return static_cast<jint>(ret.value_or(0)); +} + +static void nativeSetLightPlayerId(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId, + jint lightId, jint playerId) { + NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr); + + im->getInputManager()->getReader()->setLightPlayerId(deviceId, lightId, playerId); +} + +static void nativeSetLightColor(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId, + jint lightId, jint color) { + NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr); + + im->getInputManager()->getReader()->setLightColor(deviceId, lightId, color); +} + static jint nativeGetBatteryCapacity(JNIEnv* env, jclass /* clazz */, jlong ptr, jint deviceId) { NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr); @@ -2192,6 +2279,11 @@ static const JNINativeMethod gInputManagerMethods[] = { {"nativeCancelVibrate", "(JII)V", (void*)nativeCancelVibrate}, {"nativeIsVibrating", "(JI)Z", (void*)nativeIsVibrating}, {"nativeGetVibratorIds", "(JI)[I", (void*)nativeGetVibratorIds}, + {"nativeGetLights", "(JI)Ljava/util/List;", (void*)nativeGetLights}, + {"nativeGetLightPlayerId", "(JII)I", (void*)nativeGetLightPlayerId}, + {"nativeGetLightColor", "(JII)I", (void*)nativeGetLightColor}, + {"nativeSetLightPlayerId", "(JIII)V", (void*)nativeSetLightPlayerId}, + {"nativeSetLightColor", "(JIII)V", (void*)nativeSetLightColor}, {"nativeGetBatteryCapacity", "(JI)I", (void*)nativeGetBatteryCapacity}, {"nativeGetBatteryStatus", "(JI)I", (void*)nativeGetBatteryStatus}, {"nativeReloadKeyboardLayouts", "(J)V", (void*)nativeReloadKeyboardLayouts}, @@ -2386,6 +2478,27 @@ int register_android_server_InputManager(JNIEnv* env) { GET_METHOD_ID(gTouchCalibrationClassInfo.getAffineTransform, gTouchCalibrationClassInfo.clazz, "getAffineTransform", "()[F"); + // Light + FIND_CLASS(gLightClassInfo.clazz, "android/hardware/lights/Light"); + gLightClassInfo.clazz = jclass(env->NewGlobalRef(gLightClassInfo.clazz)); + GET_METHOD_ID(gLightClassInfo.constructor, gLightClassInfo.clazz, "<init>", + "(IIILjava/lang/String;)V"); + + gLightClassInfo.clazz = jclass(env->NewGlobalRef(gLightClassInfo.clazz)); + gLightClassInfo.lightTypeSingle = + env->GetStaticFieldID(gLightClassInfo.clazz, "LIGHT_TYPE_INPUT_SINGLE", "I"); + gLightClassInfo.lightTypePlayerId = + env->GetStaticFieldID(gLightClassInfo.clazz, "LIGHT_TYPE_INPUT_PLAYER_ID", "I"); + gLightClassInfo.lightTypeRgb = + env->GetStaticFieldID(gLightClassInfo.clazz, "LIGHT_TYPE_INPUT_RGB", "I"); + + // ArrayList + FIND_CLASS(gArrayListClassInfo.clazz, "java/util/ArrayList"); + gArrayListClassInfo.clazz = jclass(env->NewGlobalRef(gArrayListClassInfo.clazz)); + GET_METHOD_ID(gArrayListClassInfo.constructor, gArrayListClassInfo.clazz, "<init>", "()V"); + GET_METHOD_ID(gArrayListClassInfo.add, gArrayListClassInfo.clazz, "add", + "(Ljava/lang/Object;)Z"); + // SparseArray FIND_CLASS(gSparseArrayClassInfo.clazz, "android/util/SparseArray"); gSparseArrayClassInfo.clazz = jclass(env->NewGlobalRef(gSparseArrayClassInfo.clazz)); diff --git a/services/tests/servicestests/src/com/android/server/lights/LightsServiceTest.java b/services/tests/servicestests/src/com/android/server/lights/LightsServiceTest.java index 3e9709d55268..f0a9a0089ec9 100644 --- a/services/tests/servicestests/src/com/android/server/lights/LightsServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/lights/LightsServiceTest.java @@ -31,6 +31,7 @@ import android.hardware.light.ILights; import android.hardware.lights.Light; import android.hardware.lights.LightState; import android.hardware.lights.LightsManager; +import android.hardware.lights.SystemLightsManager; import android.os.Looper; import androidx.test.filters.SmallTest; @@ -93,7 +94,7 @@ public class LightsServiceTest { @Test public void testGetLights_filtersSystemLights() { LightsService service = new LightsService(mContext, () -> mHal, Looper.getMainLooper()); - LightsManager manager = new LightsManager(mContext, service.mManagerService); + LightsManager manager = new SystemLightsManager(mContext, service.mManagerService); // When lights are listed, only the 4 MICROPHONE lights should be visible. assertThat(manager.getLights().size()).isEqualTo(4); @@ -102,14 +103,14 @@ public class LightsServiceTest { @Test public void testControlMultipleLights() { LightsService service = new LightsService(mContext, () -> mHal, Looper.getMainLooper()); - LightsManager manager = new LightsManager(mContext, service.mManagerService); + LightsManager manager = new SystemLightsManager(mContext, service.mManagerService); // When the session requests to turn 3/4 lights on: LightsManager.LightsSession session = manager.openSession(); session.requestLights(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)) + .addLight(manager.getLights().get(0), new LightState(0xf1)) + .addLight(manager.getLights().get(1), new LightState(0xf2)) + .addLight(manager.getLights().get(2), new LightState(0xf3)) .build()); // Then all 3 should turn on. @@ -122,9 +123,9 @@ public class LightsServiceTest { } @Test - public void testControlLights_onlyEffectiveForLifetimeOfClient() { + public void testControlLights_onlyEffectiveForLifetimeOfClient() throws Exception { LightsService service = new LightsService(mContext, () -> mHal, Looper.getMainLooper()); - LightsManager manager = new LightsManager(mContext, service.mManagerService); + LightsManager manager = new SystemLightsManager(mContext, service.mManagerService); Light micLight = manager.getLights().get(0); // The light should begin by being off. @@ -132,38 +133,41 @@ public class LightsServiceTest { // When a session commits changes: LightsManager.LightsSession session = manager.openSession(); - session.requestLights(new Builder().setLight(micLight, new LightState(GREEN)).build()); + session.requestLights(new Builder().addLight(micLight, new LightState(GREEN)).build()); // Then the light should turn on. assertThat(manager.getLightState(micLight).getColor()).isEqualTo(GREEN); // When the session goes away: session.close(); + // Then the light should turn off. assertThat(manager.getLightState(micLight).getColor()).isEqualTo(TRANSPARENT); } @Test - public void testControlLights_firstCallerWinsContention() { + public void testControlLights_firstCallerWinsContention() throws Exception { LightsService service = new LightsService(mContext, () -> mHal, Looper.getMainLooper()); - LightsManager manager = new LightsManager(mContext, service.mManagerService); + LightsManager manager = new SystemLightsManager(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.requestLights(new Builder().setLight(micLight, new LightState(BLUE)).build()); - session2.requestLights(new Builder().setLight(micLight, new LightState(WHITE)).build()); + session1.requestLights(new Builder().addLight(micLight, new LightState(BLUE)).build()); + session2.requestLights(new Builder().addLight(micLight, new LightState(WHITE)).build()); // Then session1 should win because it was created first. assertThat(manager.getLightState(micLight).getColor()).isEqualTo(BLUE); // When session1 goes away: session1.close(); + // Then session2 should have its request go into effect. assertThat(manager.getLightState(micLight).getColor()).isEqualTo(WHITE); // 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); } @@ -171,12 +175,12 @@ public class LightsServiceTest { @Test public void testClearLight() { LightsService service = new LightsService(mContext, () -> mHal, Looper.getMainLooper()); - LightsManager manager = new LightsManager(mContext, service.mManagerService); + LightsManager manager = new SystemLightsManager(mContext, service.mManagerService); Light micLight = manager.getLights().get(0); // When the session turns a light on: LightsManager.LightsSession session = manager.openSession(); - session.requestLights(new Builder().setLight(micLight, new LightState(WHITE)).build()); + session.requestLights(new Builder().addLight(micLight, new LightState(WHITE)).build()); // And then the session clears it again: session.requestLights(new Builder().clearLight(micLight).build()); |