| /* |
| * Copyright (C) 2022 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.input; |
| |
| import android.annotation.BinderThread; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothManager; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.hardware.BatteryState; |
| import android.hardware.input.IInputDeviceBatteryListener; |
| import android.hardware.input.IInputDeviceBatteryState; |
| import android.hardware.input.InputManager; |
| import android.os.Handler; |
| import android.os.HandlerExecutor; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.os.UEventObserver; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.IndentingPrintWriter; |
| import android.util.Log; |
| import android.util.Slog; |
| import android.view.InputDevice; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.io.PrintWriter; |
| import java.util.Arrays; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.concurrent.Executor; |
| import java.util.function.Consumer; |
| import java.util.function.Function; |
| import java.util.function.Predicate; |
| |
| /** |
| * A thread-safe component of {@link InputManagerService} responsible for managing the battery state |
| * of input devices. |
| * |
| * Interactions with BatteryController can happen on several threads, including Binder threads, the |
| * {@link UEventObserver}'s thread, or its own Handler thread, among others. All public methods, and |
| * private methods prefixed with "handle-" (e.g. {@link #handleListeningProcessDied(int)}), |
| * serve as entry points for these threads. |
| */ |
| final class BatteryController { |
| private static final String TAG = BatteryController.class.getSimpleName(); |
| |
| // To enable these logs, run: |
| // 'adb shell setprop log.tag.BatteryController DEBUG' (requires restart) |
| private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); |
| |
| @VisibleForTesting |
| static final long POLLING_PERIOD_MILLIS = 10_000; // 10 seconds |
| @VisibleForTesting |
| static final long USI_BATTERY_VALIDITY_DURATION_MILLIS = 60 * 60_000; // 1 hour |
| |
| private final Object mLock = new Object(); |
| private final Context mContext; |
| private final NativeInputManagerService mNative; |
| private final Handler mHandler; |
| private final UEventManager mUEventManager; |
| private final BluetoothBatteryManager mBluetoothBatteryManager; |
| |
| // Maps a pid to the registered listener record for that process. There can only be one battery |
| // listener per process. |
| @GuardedBy("mLock") |
| private final ArrayMap<Integer, ListenerRecord> mListenerRecords = new ArrayMap<>(); |
| |
| // Maps a deviceId that is being monitored to the monitor for the battery state of the device. |
| @GuardedBy("mLock") |
| private final ArrayMap<Integer, DeviceMonitor> mDeviceMonitors = new ArrayMap<>(); |
| |
| @GuardedBy("mLock") |
| private boolean mIsPolling = false; |
| @GuardedBy("mLock") |
| private boolean mIsInteractive = true; |
| @Nullable |
| @GuardedBy("mLock") |
| private BluetoothBatteryManager.BluetoothBatteryListener mBluetoothBatteryListener; |
| |
| BatteryController(Context context, NativeInputManagerService nativeService, Looper looper, |
| UEventManager uEventManager) { |
| this(context, nativeService, looper, uEventManager, |
| new LocalBluetoothBatteryManager(context, looper)); |
| } |
| |
| @VisibleForTesting |
| BatteryController(Context context, NativeInputManagerService nativeService, Looper looper, |
| UEventManager uEventManager, BluetoothBatteryManager bbm) { |
| mContext = context; |
| mNative = nativeService; |
| mHandler = new Handler(looper); |
| mUEventManager = uEventManager; |
| mBluetoothBatteryManager = bbm; |
| } |
| |
| public void systemRunning() { |
| final InputManager inputManager = |
| Objects.requireNonNull(mContext.getSystemService(InputManager.class)); |
| inputManager.registerInputDeviceListener(mInputDeviceListener, mHandler); |
| for (int deviceId : inputManager.getInputDeviceIds()) { |
| mInputDeviceListener.onInputDeviceAdded(deviceId); |
| } |
| } |
| |
| /** |
| * Register the battery listener for the given input device and start monitoring its battery |
| * state. |
| */ |
| @BinderThread |
| public void registerBatteryListener(int deviceId, @NonNull IInputDeviceBatteryListener listener, |
| int pid) { |
| synchronized (mLock) { |
| ListenerRecord listenerRecord = mListenerRecords.get(pid); |
| |
| if (listenerRecord == null) { |
| listenerRecord = new ListenerRecord(pid, listener); |
| try { |
| listener.asBinder().linkToDeath(listenerRecord.mDeathRecipient, 0); |
| } catch (RemoteException e) { |
| Slog.i(TAG, "Client died before battery listener could be registered."); |
| return; |
| } |
| mListenerRecords.put(pid, listenerRecord); |
| if (DEBUG) Slog.d(TAG, "Battery listener added for pid " + pid); |
| } |
| |
| if (listenerRecord.mListener.asBinder() != listener.asBinder()) { |
| throw new SecurityException( |
| "Cannot register a new battery listener when there is already another " |
| + "registered listener for pid " |
| + pid); |
| } |
| if (!listenerRecord.mMonitoredDevices.add(deviceId)) { |
| throw new IllegalArgumentException( |
| "The battery listener for pid " + pid |
| + " is already monitoring deviceId " + deviceId); |
| } |
| |
| DeviceMonitor monitor = mDeviceMonitors.get(deviceId); |
| if (monitor == null) { |
| // This is the first listener that is monitoring this device. |
| monitor = new DeviceMonitor(deviceId); |
| mDeviceMonitors.put(deviceId, monitor); |
| updateBluetoothBatteryMonitoring(); |
| } |
| |
| if (DEBUG) { |
| Slog.d(TAG, "Battery listener for pid " + pid |
| + " is monitoring deviceId " + deviceId); |
| } |
| |
| updatePollingLocked(true /*delayStart*/); |
| notifyBatteryListener(listenerRecord, monitor.getBatteryStateForReporting()); |
| } |
| } |
| |
| private static void notifyBatteryListener(ListenerRecord listenerRecord, State state) { |
| try { |
| listenerRecord.mListener.onBatteryStateChanged(state); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "Failed to notify listener", e); |
| } |
| if (DEBUG) { |
| Slog.d(TAG, "Notified battery listener from pid " + listenerRecord.mPid |
| + " of state of deviceId " + state.deviceId); |
| } |
| } |
| |
| private void notifyAllListenersForDevice(State state) { |
| synchronized (mLock) { |
| if (DEBUG) Slog.d(TAG, "Notifying all listeners of battery state: " + state); |
| mListenerRecords.forEach((pid, listenerRecord) -> { |
| if (listenerRecord.mMonitoredDevices.contains(state.deviceId)) { |
| notifyBatteryListener(listenerRecord, state); |
| } |
| }); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void updatePollingLocked(boolean delayStart) { |
| if (!mIsInteractive || !anyOf(mDeviceMonitors, DeviceMonitor::requiresPolling)) { |
| // Stop polling. |
| mIsPolling = false; |
| mHandler.removeCallbacks(this::handlePollEvent); |
| return; |
| } |
| |
| if (mIsPolling) { |
| return; |
| } |
| // Start polling. |
| mIsPolling = true; |
| mHandler.postDelayed(this::handlePollEvent, delayStart ? POLLING_PERIOD_MILLIS : 0); |
| } |
| |
| private <R> R processInputDevice(int deviceId, R defaultValue, Function<InputDevice, R> func) { |
| final InputDevice device = |
| Objects.requireNonNull(mContext.getSystemService(InputManager.class)) |
| .getInputDevice(deviceId); |
| return device == null ? defaultValue : func.apply(device); |
| } |
| |
| private String getInputDeviceName(int deviceId) { |
| return processInputDevice(deviceId, "<none>" /*defaultValue*/, InputDevice::getName); |
| } |
| |
| private boolean hasBattery(int deviceId) { |
| return processInputDevice(deviceId, false /*defaultValue*/, InputDevice::hasBattery); |
| } |
| |
| private boolean isUsiDevice(int deviceId) { |
| return processInputDevice(deviceId, false /*defaultValue*/, |
| (device) -> device.getHostUsiVersion() != null); |
| } |
| |
| @Nullable |
| private BluetoothDevice getBluetoothDevice(int inputDeviceId) { |
| return getBluetoothDevice(mContext, |
| processInputDevice(inputDeviceId, null /*defaultValue*/, |
| InputDevice::getBluetoothAddress)); |
| } |
| |
| @Nullable |
| private static BluetoothDevice getBluetoothDevice(Context context, String address) { |
| if (address == null) return null; |
| final BluetoothAdapter adapter = |
| Objects.requireNonNull(context.getSystemService(BluetoothManager.class)) |
| .getAdapter(); |
| return adapter.getRemoteDevice(address); |
| } |
| |
| @GuardedBy("mLock") |
| private DeviceMonitor getDeviceMonitorOrThrowLocked(int deviceId) { |
| return Objects.requireNonNull(mDeviceMonitors.get(deviceId), |
| "Maps are out of sync: Cannot find device state for deviceId " + deviceId); |
| } |
| |
| /** |
| * Unregister the battery listener for the given input device and stop monitoring its battery |
| * state. If there are no other input devices that this listener is monitoring, the listener is |
| * removed. |
| */ |
| @BinderThread |
| public void unregisterBatteryListener(int deviceId, |
| @NonNull IInputDeviceBatteryListener listener, int pid) { |
| synchronized (mLock) { |
| final ListenerRecord listenerRecord = mListenerRecords.get(pid); |
| if (listenerRecord == null) { |
| throw new IllegalArgumentException( |
| "Cannot unregister battery callback: No listener registered for pid " |
| + pid); |
| } |
| |
| if (listenerRecord.mListener.asBinder() != listener.asBinder()) { |
| throw new IllegalArgumentException( |
| "Cannot unregister battery callback: The listener is not the one that " |
| + "is registered for pid " |
| + pid); |
| } |
| |
| if (!listenerRecord.mMonitoredDevices.contains(deviceId)) { |
| throw new IllegalArgumentException( |
| "Cannot unregister battery callback: The device is not being " |
| + "monitored for deviceId " + deviceId); |
| } |
| |
| unregisterRecordLocked(listenerRecord, deviceId); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void unregisterRecordLocked(ListenerRecord listenerRecord, int deviceId) { |
| final int pid = listenerRecord.mPid; |
| |
| if (!listenerRecord.mMonitoredDevices.remove(deviceId)) { |
| throw new IllegalStateException("Cannot unregister battery callback: The deviceId " |
| + deviceId |
| + " is not being monitored by pid " |
| + pid); |
| } |
| |
| if (!hasRegisteredListenerForDeviceLocked(deviceId)) { |
| // There are no more listeners monitoring this device. |
| final DeviceMonitor monitor = getDeviceMonitorOrThrowLocked(deviceId); |
| if (!monitor.isPersistent()) { |
| monitor.onMonitorDestroy(); |
| mDeviceMonitors.remove(deviceId); |
| } |
| } |
| |
| if (listenerRecord.mMonitoredDevices.isEmpty()) { |
| // There are no more devices being monitored by this listener. |
| listenerRecord.mListener.asBinder().unlinkToDeath(listenerRecord.mDeathRecipient, 0); |
| mListenerRecords.remove(pid); |
| if (DEBUG) Slog.d(TAG, "Battery listener removed for pid " + pid); |
| } |
| |
| updatePollingLocked(false /*delayStart*/); |
| } |
| |
| @GuardedBy("mLock") |
| private boolean hasRegisteredListenerForDeviceLocked(int deviceId) { |
| for (int i = 0; i < mListenerRecords.size(); i++) { |
| if (mListenerRecords.valueAt(i).mMonitoredDevices.contains(deviceId)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private void handleListeningProcessDied(int pid) { |
| synchronized (mLock) { |
| final ListenerRecord listenerRecord = mListenerRecords.get(pid); |
| if (listenerRecord == null) { |
| return; |
| } |
| if (DEBUG) { |
| Slog.d(TAG, |
| "Removing battery listener for pid " + pid + " because the process died"); |
| } |
| for (final int deviceId : listenerRecord.mMonitoredDevices) { |
| unregisterRecordLocked(listenerRecord, deviceId); |
| } |
| } |
| } |
| |
| private void handleUEventNotification(int deviceId, long eventTime) { |
| synchronized (mLock) { |
| final DeviceMonitor monitor = mDeviceMonitors.get(deviceId); |
| if (monitor == null) { |
| return; |
| } |
| monitor.onUEvent(eventTime); |
| } |
| } |
| |
| private void handlePollEvent() { |
| synchronized (mLock) { |
| if (!mIsPolling) { |
| return; |
| } |
| final long eventTime = SystemClock.uptimeMillis(); |
| mDeviceMonitors.forEach((deviceId, monitor) -> monitor.onPoll(eventTime)); |
| mHandler.postDelayed(this::handlePollEvent, POLLING_PERIOD_MILLIS); |
| } |
| } |
| |
| private void handleMonitorTimeout(int deviceId) { |
| synchronized (mLock) { |
| final DeviceMonitor monitor = mDeviceMonitors.get(deviceId); |
| if (monitor == null) { |
| return; |
| } |
| final long updateTime = SystemClock.uptimeMillis(); |
| monitor.onTimeout(updateTime); |
| } |
| } |
| |
| private void handleBluetoothBatteryLevelChange(long eventTime, String address, |
| int batteryLevel) { |
| synchronized (mLock) { |
| final DeviceMonitor monitor = findIf(mDeviceMonitors, (m) -> |
| (m.mBluetoothDevice != null |
| && address.equals(m.mBluetoothDevice.getAddress()))); |
| if (monitor != null) { |
| monitor.onBluetoothBatteryChanged(eventTime, batteryLevel); |
| } |
| } |
| } |
| |
| private void handleBluetoothMetadataChange(@NonNull BluetoothDevice device, int key, |
| @Nullable byte[] value) { |
| synchronized (mLock) { |
| final DeviceMonitor monitor = |
| findIf(mDeviceMonitors, (m) -> device.equals(m.mBluetoothDevice)); |
| if (monitor != null) { |
| final long eventTime = SystemClock.uptimeMillis(); |
| monitor.onBluetoothMetadataChanged(eventTime, key, value); |
| } |
| } |
| } |
| |
| /** Gets the current battery state of an input device. */ |
| public IInputDeviceBatteryState getBatteryState(int deviceId) { |
| synchronized (mLock) { |
| final long updateTime = SystemClock.uptimeMillis(); |
| final DeviceMonitor monitor = mDeviceMonitors.get(deviceId); |
| if (monitor == null) { |
| // The input device's battery is not being monitored by any listener. |
| return queryBatteryStateFromNative(deviceId, updateTime, hasBattery(deviceId)); |
| } |
| // Force the battery state to update, and notify listeners if necessary. |
| monitor.onPoll(updateTime); |
| return monitor.getBatteryStateForReporting(); |
| } |
| } |
| |
| public void onInteractiveChanged(boolean interactive) { |
| synchronized (mLock) { |
| mIsInteractive = interactive; |
| updatePollingLocked(false /*delayStart*/); |
| } |
| } |
| |
| public void notifyStylusGestureStarted(int deviceId, long eventTime) { |
| synchronized (mLock) { |
| final DeviceMonitor monitor = mDeviceMonitors.get(deviceId); |
| if (monitor == null) { |
| return; |
| } |
| |
| monitor.onStylusGestureStarted(eventTime); |
| } |
| } |
| |
| public void dump(PrintWriter pw) { |
| IndentingPrintWriter ipw = new IndentingPrintWriter(pw); |
| synchronized (mLock) { |
| ipw.println(TAG + ":"); |
| ipw.increaseIndent(); |
| ipw.println("State: Polling = " + mIsPolling |
| + ", Interactive = " + mIsInteractive); |
| |
| ipw.println("Listeners: " + mListenerRecords.size() + " battery listeners"); |
| ipw.increaseIndent(); |
| for (int i = 0; i < mListenerRecords.size(); i++) { |
| ipw.println(i + ": " + mListenerRecords.valueAt(i)); |
| } |
| ipw.decreaseIndent(); |
| |
| ipw.println("Device Monitors: " + mDeviceMonitors.size() + " monitors"); |
| ipw.increaseIndent(); |
| for (int i = 0; i < mDeviceMonitors.size(); i++) { |
| ipw.println(i + ": " + mDeviceMonitors.valueAt(i)); |
| } |
| ipw.decreaseIndent(); |
| ipw.decreaseIndent(); |
| } |
| } |
| |
| @SuppressWarnings("all") |
| public void monitor() { |
| synchronized (mLock) { |
| return; |
| } |
| } |
| |
| private final InputManager.InputDeviceListener mInputDeviceListener = |
| new InputManager.InputDeviceListener() { |
| @Override |
| public void onInputDeviceAdded(int deviceId) { |
| synchronized (mLock) { |
| if (isUsiDevice(deviceId) && !mDeviceMonitors.containsKey(deviceId)) { |
| // Start monitoring USI device immediately. |
| mDeviceMonitors.put(deviceId, new UsiDeviceMonitor(deviceId)); |
| } |
| } |
| } |
| |
| @Override |
| public void onInputDeviceRemoved(int deviceId) {} |
| |
| @Override |
| public void onInputDeviceChanged(int deviceId) { |
| synchronized (mLock) { |
| final DeviceMonitor monitor = mDeviceMonitors.get(deviceId); |
| if (monitor == null) { |
| return; |
| } |
| final long eventTime = SystemClock.uptimeMillis(); |
| monitor.onConfiguration(eventTime); |
| } |
| } |
| }; |
| |
| // A record of a registered battery listener from one process. |
| private class ListenerRecord { |
| public final int mPid; |
| public final IInputDeviceBatteryListener mListener; |
| public final IBinder.DeathRecipient mDeathRecipient; |
| // The set of deviceIds that are currently being monitored by this listener. |
| public final Set<Integer> mMonitoredDevices; |
| |
| ListenerRecord(int pid, IInputDeviceBatteryListener listener) { |
| mPid = pid; |
| mListener = listener; |
| mMonitoredDevices = new ArraySet<>(); |
| mDeathRecipient = () -> handleListeningProcessDied(pid); |
| } |
| |
| @Override |
| public String toString() { |
| return "pid=" + mPid |
| + ", monitored devices=" + Arrays.toString(mMonitoredDevices.toArray()); |
| } |
| } |
| |
| // Queries the battery state of an input device from native code. |
| private State queryBatteryStateFromNative(int deviceId, long updateTime, boolean isPresent) { |
| return new State( |
| deviceId, |
| updateTime, |
| isPresent, |
| isPresent ? mNative.getBatteryStatus(deviceId) : BatteryState.STATUS_UNKNOWN, |
| isPresent ? mNative.getBatteryCapacity(deviceId) / 100.f : Float.NaN); |
| } |
| |
| private void updateBluetoothBatteryMonitoring() { |
| synchronized (mLock) { |
| if (anyOf(mDeviceMonitors, (m) -> m.mBluetoothDevice != null)) { |
| // At least one input device being monitored is connected over Bluetooth. |
| if (mBluetoothBatteryListener == null) { |
| if (DEBUG) Slog.d(TAG, "Registering bluetooth battery listener"); |
| mBluetoothBatteryListener = this::handleBluetoothBatteryLevelChange; |
| mBluetoothBatteryManager.addBatteryListener(mBluetoothBatteryListener); |
| } |
| } else if (mBluetoothBatteryListener != null) { |
| // No Bluetooth input devices are monitored, so remove the registered listener. |
| if (DEBUG) Slog.d(TAG, "Unregistering bluetooth battery listener"); |
| mBluetoothBatteryManager.removeBatteryListener(mBluetoothBatteryListener); |
| mBluetoothBatteryListener = null; |
| } |
| } |
| } |
| |
| // Holds the state of an InputDevice for which battery changes are currently being monitored. |
| private class DeviceMonitor { |
| protected final State mState; |
| // Represents whether the input device has a sysfs battery node. |
| protected boolean mHasBattery = false; |
| |
| @Nullable |
| private BluetoothDevice mBluetoothDevice; |
| long mBluetoothEventTime = 0; |
| // The battery level reported by the Bluetooth Hands-Free Profile (HPF) obtained through |
| // BluetoothDevice#getBatteryLevel(). |
| int mBluetoothBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; |
| // The battery level and status reported through the Bluetooth device's metadata. |
| int mBluetoothMetadataBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; |
| int mBluetoothMetadataBatteryStatus = BatteryState.STATUS_UNKNOWN; |
| @Nullable |
| private BluetoothAdapter.OnMetadataChangedListener mBluetoothMetadataListener; |
| |
| @Nullable |
| private BatteryController.UEventBatteryListener mUEventBatteryListener; |
| |
| DeviceMonitor(int deviceId) { |
| mState = new State(deviceId); |
| |
| // Load the initial battery state and start monitoring. |
| final long eventTime = SystemClock.uptimeMillis(); |
| configureDeviceMonitor(eventTime); |
| } |
| |
| protected void processChangesAndNotify(long eventTime, Consumer<Long> changes) { |
| final State oldState = getBatteryStateForReporting(); |
| changes.accept(eventTime); |
| final State newState = getBatteryStateForReporting(); |
| if (!oldState.equalsIgnoringUpdateTime(newState)) { |
| notifyAllListenersForDevice(newState); |
| } |
| } |
| |
| public void onConfiguration(long eventTime) { |
| processChangesAndNotify(eventTime, this::configureDeviceMonitor); |
| } |
| |
| private void configureDeviceMonitor(long eventTime) { |
| final int deviceId = mState.deviceId; |
| if (mHasBattery != hasBattery(mState.deviceId)) { |
| mHasBattery = !mHasBattery; |
| if (mHasBattery) { |
| startNativeMonitoring(); |
| } else { |
| stopNativeMonitoring(); |
| } |
| updateBatteryStateFromNative(eventTime); |
| } |
| |
| final BluetoothDevice bluetoothDevice = getBluetoothDevice(deviceId); |
| if (!Objects.equals(mBluetoothDevice, bluetoothDevice)) { |
| if (DEBUG) { |
| Slog.d(TAG, "Bluetooth device is now " |
| + ((bluetoothDevice != null) ? "" : "not") |
| + " present for deviceId " + deviceId); |
| } |
| |
| mBluetoothBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; |
| stopBluetoothMetadataMonitoring(); |
| |
| mBluetoothDevice = bluetoothDevice; |
| updateBluetoothBatteryMonitoring(); |
| |
| if (mBluetoothDevice != null) { |
| mBluetoothBatteryLevel = mBluetoothBatteryManager.getBatteryLevel( |
| mBluetoothDevice.getAddress()); |
| startBluetoothMetadataMonitoring(eventTime); |
| } |
| } |
| } |
| |
| private void startNativeMonitoring() { |
| final String batteryPath = mNative.getBatteryDevicePath(mState.deviceId); |
| if (batteryPath == null) { |
| return; |
| } |
| final int deviceId = mState.deviceId; |
| mUEventBatteryListener = new BatteryController.UEventBatteryListener() { |
| @Override |
| public void onBatteryUEvent(long eventTime) { |
| handleUEventNotification(deviceId, eventTime); |
| } |
| }; |
| mUEventManager.addListener( |
| mUEventBatteryListener, "DEVPATH=" + formatDevPath(batteryPath)); |
| } |
| |
| private String formatDevPath(@NonNull String path) { |
| // Remove the "/sys" prefix if it has one. |
| return path.startsWith("/sys") ? path.substring(4) : path; |
| } |
| |
| private void stopNativeMonitoring() { |
| if (mUEventBatteryListener != null) { |
| mUEventManager.removeListener(mUEventBatteryListener); |
| mUEventBatteryListener = null; |
| } |
| } |
| |
| private void startBluetoothMetadataMonitoring(long eventTime) { |
| Objects.requireNonNull(mBluetoothDevice); |
| |
| mBluetoothMetadataListener = BatteryController.this::handleBluetoothMetadataChange; |
| mBluetoothBatteryManager.addMetadataListener(mBluetoothDevice.getAddress(), |
| mBluetoothMetadataListener); |
| updateBluetoothMetadataState(eventTime, BluetoothDevice.METADATA_MAIN_BATTERY, |
| mBluetoothBatteryManager.getMetadata(mBluetoothDevice.getAddress(), |
| BluetoothDevice.METADATA_MAIN_BATTERY)); |
| updateBluetoothMetadataState(eventTime, BluetoothDevice.METADATA_MAIN_CHARGING, |
| mBluetoothBatteryManager.getMetadata(mBluetoothDevice.getAddress(), |
| BluetoothDevice.METADATA_MAIN_CHARGING)); |
| } |
| |
| private void stopBluetoothMetadataMonitoring() { |
| if (mBluetoothMetadataListener == null) { |
| return; |
| } |
| Objects.requireNonNull(mBluetoothDevice); |
| |
| mBluetoothBatteryManager.removeMetadataListener( |
| mBluetoothDevice.getAddress(), mBluetoothMetadataListener); |
| mBluetoothMetadataListener = null; |
| mBluetoothMetadataBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; |
| mBluetoothMetadataBatteryStatus = BatteryState.STATUS_UNKNOWN; |
| } |
| |
| // This must be called when the device is no longer being monitored. |
| public void onMonitorDestroy() { |
| stopNativeMonitoring(); |
| stopBluetoothMetadataMonitoring(); |
| mBluetoothDevice = null; |
| updateBluetoothBatteryMonitoring(); |
| } |
| |
| protected void updateBatteryStateFromNative(long eventTime) { |
| mState.updateIfChanged( |
| queryBatteryStateFromNative(mState.deviceId, eventTime, mHasBattery)); |
| } |
| |
| public void onPoll(long eventTime) { |
| processChangesAndNotify(eventTime, this::updateBatteryStateFromNative); |
| } |
| |
| public void onUEvent(long eventTime) { |
| processChangesAndNotify(eventTime, this::updateBatteryStateFromNative); |
| } |
| |
| public void onBluetoothBatteryChanged(long eventTime, int bluetoothBatteryLevel) { |
| processChangesAndNotify(eventTime, (time) -> { |
| mBluetoothBatteryLevel = bluetoothBatteryLevel; |
| mBluetoothEventTime = time; |
| }); |
| } |
| |
| public void onBluetoothMetadataChanged(long eventTime, int key, @Nullable byte[] value) { |
| processChangesAndNotify(eventTime, |
| (time) -> updateBluetoothMetadataState(time, key, value)); |
| } |
| |
| private void updateBluetoothMetadataState(long eventTime, int key, |
| @Nullable byte[] value) { |
| switch (key) { |
| case BluetoothDevice.METADATA_MAIN_BATTERY: |
| mBluetoothEventTime = eventTime; |
| mBluetoothMetadataBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; |
| if (value != null) { |
| try { |
| mBluetoothMetadataBatteryLevel = Integer.parseInt( |
| new String(value)); |
| } catch (NumberFormatException e) { |
| Slog.wtf(TAG, |
| "Failed to parse bluetooth METADATA_MAIN_BATTERY with " |
| + "value '" |
| + new String(value) + "' for device " |
| + mBluetoothDevice); |
| } |
| } |
| break; |
| case BluetoothDevice.METADATA_MAIN_CHARGING: |
| mBluetoothEventTime = eventTime; |
| if (value != null) { |
| mBluetoothMetadataBatteryStatus = Boolean.parseBoolean( |
| new String(value)) |
| ? BatteryState.STATUS_CHARGING |
| : BatteryState.STATUS_DISCHARGING; |
| } else { |
| mBluetoothMetadataBatteryStatus = BatteryState.STATUS_UNKNOWN; |
| } |
| break; |
| default: |
| break; |
| } |
| } |
| |
| public boolean requiresPolling() { |
| return true; |
| } |
| |
| public boolean isPersistent() { |
| return false; |
| } |
| |
| public void onTimeout(long eventTime) {} |
| |
| public void onStylusGestureStarted(long eventTime) {} |
| |
| // Returns the current battery state that can be used to notify listeners BatteryController. |
| public State getBatteryStateForReporting() { |
| // Give precedence to the Bluetooth battery state, and fall back to the native state. |
| return Objects.requireNonNullElseGet(resolveBluetoothBatteryState(), |
| () -> new State(mState)); |
| } |
| |
| @Nullable |
| protected State resolveBluetoothBatteryState() { |
| final int level; |
| // Prefer battery level obtained from the metadata over the Bluetooth Hands-Free |
| // Profile (HFP). |
| if (mBluetoothMetadataBatteryLevel >= 0 && mBluetoothMetadataBatteryLevel <= 100) { |
| level = mBluetoothMetadataBatteryLevel; |
| } else if (mBluetoothBatteryLevel >= 0 && mBluetoothBatteryLevel <= 100) { |
| level = mBluetoothBatteryLevel; |
| } else { |
| return null; |
| } |
| return new State(mState.deviceId, mBluetoothEventTime, true, |
| mBluetoothMetadataBatteryStatus, level / 100.f); |
| } |
| |
| @Override |
| public String toString() { |
| return "DeviceId=" + mState.deviceId |
| + ", Name='" + getInputDeviceName(mState.deviceId) + "'" |
| + ", NativeBattery=" + mState |
| + ", UEventListener=" + (mUEventBatteryListener != null ? "added" : "none") |
| + ", BluetoothState=" + resolveBluetoothBatteryState(); |
| } |
| } |
| |
| // Battery monitoring logic that is specific to stylus devices that support the |
| // Universal Stylus Initiative (USI) protocol. |
| private class UsiDeviceMonitor extends DeviceMonitor { |
| |
| // For USI devices, we only treat the battery state as valid for a fixed amount of time |
| // after receiving a battery update. Once the timeout has passed, we signal to all listeners |
| // that there is no longer a battery present for the device. The battery state is valid |
| // as long as this callback is non-null. |
| @Nullable |
| private Runnable mValidityTimeoutCallback; |
| |
| UsiDeviceMonitor(int deviceId) { |
| super(deviceId); |
| } |
| |
| @Override |
| public void onPoll(long eventTime) { |
| // Disregard polling for USI devices. |
| } |
| |
| @Override |
| public void onUEvent(long eventTime) { |
| processChangesAndNotify(eventTime, (time) -> { |
| updateBatteryStateFromNative(time); |
| markUsiBatteryValid(); |
| }); |
| } |
| |
| @Override |
| public void onStylusGestureStarted(long eventTime) { |
| processChangesAndNotify(eventTime, (time) -> { |
| final boolean wasValid = mValidityTimeoutCallback != null; |
| if (!wasValid && mState.capacity == 0.f) { |
| // Handle a special case where the USI device reports a battery capacity of 0 |
| // at boot until the first battery update. To avoid wrongly sending out a |
| // battery capacity of 0 if we detect stylus presence before the capacity |
| // is first updated, do not validate the battery state when the state is not |
| // valid and the capacity is 0. |
| return; |
| } |
| markUsiBatteryValid(); |
| }); |
| } |
| |
| @Override |
| public void onTimeout(long eventTime) { |
| processChangesAndNotify(eventTime, (time) -> markUsiBatteryInvalid()); |
| } |
| |
| @Override |
| public void onConfiguration(long eventTime) { |
| super.onConfiguration(eventTime); |
| |
| if (!mHasBattery) { |
| throw new IllegalStateException( |
| "UsiDeviceMonitor: USI devices are always expected to " |
| + "report a valid battery, but no battery was detected!"); |
| } |
| } |
| |
| private void markUsiBatteryValid() { |
| if (mValidityTimeoutCallback != null) { |
| mHandler.removeCallbacks(mValidityTimeoutCallback); |
| } else { |
| final int deviceId = mState.deviceId; |
| mValidityTimeoutCallback = |
| () -> BatteryController.this.handleMonitorTimeout(deviceId); |
| } |
| mHandler.postDelayed(mValidityTimeoutCallback, USI_BATTERY_VALIDITY_DURATION_MILLIS); |
| } |
| |
| private void markUsiBatteryInvalid() { |
| if (mValidityTimeoutCallback == null) { |
| return; |
| } |
| mHandler.removeCallbacks(mValidityTimeoutCallback); |
| mValidityTimeoutCallback = null; |
| } |
| |
| @Override |
| public State getBatteryStateForReporting() { |
| // Give precedence to the Bluetooth battery state, and fall back to the native state. |
| return Objects.requireNonNullElseGet(resolveBluetoothBatteryState(), |
| () -> mValidityTimeoutCallback != null |
| ? new State(mState) : new State(mState.deviceId)); |
| } |
| |
| @Override |
| public boolean requiresPolling() { |
| // Do not poll the battery state for USI devices. |
| return false; |
| } |
| |
| @Override |
| public boolean isPersistent() { |
| // Do not remove the battery monitor for USI devices. |
| return true; |
| } |
| |
| @Override |
| public String toString() { |
| return super.toString() |
| + ", UsiStateIsValid=" + (mValidityTimeoutCallback != null); |
| } |
| } |
| |
| @VisibleForTesting |
| abstract static class UEventBatteryListener extends UEventManager.UEventListener { |
| @Override |
| public void onUEvent(UEventObserver.UEvent event) { |
| final long eventTime = SystemClock.uptimeMillis(); |
| if (DEBUG) { |
| Slog.d(TAG, |
| "UEventListener: Received UEvent: " |
| + event + " eventTime: " + eventTime); |
| } |
| if (!"CHANGE".equalsIgnoreCase(event.get("ACTION")) |
| || !"POWER_SUPPLY".equalsIgnoreCase(event.get("SUBSYSTEM"))) { |
| // Disregard any UEvents that do not correspond to battery changes. |
| return; |
| } |
| UEventBatteryListener.this.onBatteryUEvent(eventTime); |
| } |
| |
| public abstract void onBatteryUEvent(long eventTime); |
| } |
| |
| // An interface used to change the API of adding a bluetooth battery listener to a more |
| // test-friendly format. |
| @VisibleForTesting |
| interface BluetoothBatteryManager { |
| @VisibleForTesting |
| interface BluetoothBatteryListener { |
| void onBluetoothBatteryChanged(long eventTime, String address, int batteryLevel); |
| } |
| // Methods used for obtaining the Bluetooth battery level through Bluetooth HFP. |
| void addBatteryListener(BluetoothBatteryListener listener); |
| void removeBatteryListener(BluetoothBatteryListener listener); |
| int getBatteryLevel(String address); |
| |
| // Methods used for obtaining the battery level through Bluetooth metadata. |
| void addMetadataListener(String address, |
| BluetoothAdapter.OnMetadataChangedListener listener); |
| void removeMetadataListener(String address, |
| BluetoothAdapter.OnMetadataChangedListener listener); |
| byte[] getMetadata(String address, int key); |
| } |
| |
| private static class LocalBluetoothBatteryManager implements BluetoothBatteryManager { |
| private final Context mContext; |
| private final Executor mExecutor; |
| @Nullable |
| @GuardedBy("mBroadcastReceiver") |
| private BluetoothBatteryListener mRegisteredListener; |
| @GuardedBy("mBroadcastReceiver") |
| private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (!BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED.equals(intent.getAction())) { |
| return; |
| } |
| final BluetoothDevice bluetoothDevice = intent.getParcelableExtra( |
| BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class); |
| if (bluetoothDevice == null) { |
| return; |
| } |
| final int batteryLevel = intent.getIntExtra(BluetoothDevice.EXTRA_BATTERY_LEVEL, |
| BluetoothDevice.BATTERY_LEVEL_UNKNOWN); |
| synchronized (mBroadcastReceiver) { |
| if (mRegisteredListener != null) { |
| final long eventTime = SystemClock.uptimeMillis(); |
| mRegisteredListener.onBluetoothBatteryChanged( |
| eventTime, bluetoothDevice.getAddress(), batteryLevel); |
| } |
| } |
| } |
| }; |
| |
| LocalBluetoothBatteryManager(Context context, Looper looper) { |
| mContext = context; |
| mExecutor = new HandlerExecutor(new Handler(looper)); |
| } |
| |
| @Override |
| public void addBatteryListener(BluetoothBatteryListener listener) { |
| synchronized (mBroadcastReceiver) { |
| if (mRegisteredListener != null) { |
| throw new IllegalStateException( |
| "Only one bluetooth battery listener can be registered at once."); |
| } |
| mRegisteredListener = listener; |
| mContext.registerReceiver(mBroadcastReceiver, |
| new IntentFilter(BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED)); |
| } |
| } |
| |
| @Override |
| public void removeBatteryListener(BluetoothBatteryListener listener) { |
| synchronized (mBroadcastReceiver) { |
| if (!listener.equals(mRegisteredListener)) { |
| throw new IllegalStateException("Listener is not registered."); |
| } |
| mRegisteredListener = null; |
| mContext.unregisterReceiver(mBroadcastReceiver); |
| } |
| } |
| |
| @Override |
| public int getBatteryLevel(String address) { |
| return getBluetoothDevice(mContext, address).getBatteryLevel(); |
| } |
| |
| @Override |
| public void addMetadataListener(String address, |
| BluetoothAdapter.OnMetadataChangedListener listener) { |
| Objects.requireNonNull(mContext.getSystemService(BluetoothManager.class)) |
| .getAdapter().addOnMetadataChangedListener( |
| getBluetoothDevice(mContext, address), mExecutor, |
| listener); |
| } |
| |
| @Override |
| public void removeMetadataListener(String address, |
| BluetoothAdapter.OnMetadataChangedListener listener) { |
| Objects.requireNonNull(mContext.getSystemService(BluetoothManager.class)) |
| .getAdapter().removeOnMetadataChangedListener( |
| getBluetoothDevice(mContext, address), listener); |
| } |
| |
| @Override |
| public byte[] getMetadata(String address, int key) { |
| return getBluetoothDevice(mContext, address).getMetadata(key); |
| } |
| } |
| |
| // Helper class that adds copying and printing functionality to IInputDeviceBatteryState. |
| private static class State extends IInputDeviceBatteryState { |
| |
| State(int deviceId) { |
| reset(deviceId); |
| } |
| |
| State(IInputDeviceBatteryState s) { |
| copyFrom(s); |
| } |
| |
| State(int deviceId, long updateTime, boolean isPresent, int status, float capacity) { |
| initialize(deviceId, updateTime, isPresent, status, capacity); |
| } |
| |
| // Updates this from other if there is a difference between them, ignoring the updateTime. |
| public void updateIfChanged(IInputDeviceBatteryState other) { |
| if (!equalsIgnoringUpdateTime(other)) { |
| copyFrom(other); |
| } |
| } |
| |
| public void reset(int deviceId) { |
| initialize(deviceId, 0 /*updateTime*/, false /*isPresent*/, BatteryState.STATUS_UNKNOWN, |
| Float.NaN /*capacity*/); |
| } |
| |
| private void copyFrom(IInputDeviceBatteryState s) { |
| initialize(s.deviceId, s.updateTime, s.isPresent, s.status, s.capacity); |
| } |
| |
| private void initialize(int deviceId, long updateTime, boolean isPresent, int status, |
| float capacity) { |
| this.deviceId = deviceId; |
| this.updateTime = updateTime; |
| this.isPresent = isPresent; |
| this.status = status; |
| this.capacity = capacity; |
| } |
| |
| public boolean equalsIgnoringUpdateTime(IInputDeviceBatteryState other) { |
| long updateTime = this.updateTime; |
| this.updateTime = other.updateTime; |
| boolean eq = this.equals(other); |
| this.updateTime = updateTime; |
| return eq; |
| } |
| |
| @Override |
| public String toString() { |
| if (!isPresent) { |
| return "State{<not present>}"; |
| } |
| return "State{time=" + updateTime |
| + ", isPresent=" + isPresent |
| + ", status=" + status |
| + ", capacity=" + capacity |
| + "}"; |
| } |
| } |
| |
| // Check if any value in an ArrayMap matches the predicate in an optimized way. |
| private static <K, V> boolean anyOf(ArrayMap<K, V> arrayMap, Predicate<V> test) { |
| return findIf(arrayMap, test) != null; |
| } |
| |
| // Find the first value in an ArrayMap that matches the predicate in an optimized way. |
| private static <K, V> V findIf(ArrayMap<K, V> arrayMap, Predicate<V> test) { |
| for (int i = 0; i < arrayMap.size(); i++) { |
| final V value = arrayMap.valueAt(i); |
| if (test.test(value)) { |
| return value; |
| } |
| } |
| return null; |
| } |
| } |