1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.connecteddevice.stylus;
18 
19 import android.content.Context;
20 import android.hardware.BatteryState;
21 import android.hardware.input.InputManager;
22 import android.os.Bundle;
23 import android.os.Handler;
24 import android.os.Looper;
25 import android.util.Log;
26 import android.view.InputDevice;
27 
28 import androidx.annotation.NonNull;
29 import androidx.annotation.VisibleForTesting;
30 import androidx.preference.Preference;
31 
32 import com.android.settings.R;
33 import com.android.settings.connecteddevice.DevicePreferenceCallback;
34 import com.android.settings.core.SubSettingLauncher;
35 import com.android.settings.dashboard.DashboardFragment;
36 import com.android.settings.overlay.FeatureFactory;
37 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
38 
39 import java.util.ArrayList;
40 import java.util.List;
41 
42 /**
43  * Controller to maintain available USI stylus devices. Listens to bluetooth
44  * stylus connection to determine whether to show the USI preference.
45  */
46 public class StylusDeviceUpdater implements InputManager.InputDeviceListener,
47         InputManager.InputDeviceBatteryListener {
48 
49     private static final String TAG = "StylusDeviceUpdater";
50     private static final String PREF_KEY = "stylus_usi_device";
51     private static final String INPUT_ID_ARG = "device_input_id";
52 
53     private final DevicePreferenceCallback mDevicePreferenceCallback;
54     private final List<Integer> mRegisteredBatteryCallbackIds;
55     private final DashboardFragment mFragment;
56     private final InputManager mInputManager;
57     private final MetricsFeatureProvider mMetricsFeatureProvider;
58 
59     private Context mContext;
60 
61     @VisibleForTesting
62     Integer mLastDetectedUsiId;
63     BatteryState mLastBatteryState;
64 
65     @VisibleForTesting
66     Preference mUsiPreference;
67 
68 
StylusDeviceUpdater(Context context, DashboardFragment fragment, DevicePreferenceCallback devicePreferenceCallback)69     public StylusDeviceUpdater(Context context, DashboardFragment fragment,
70             DevicePreferenceCallback devicePreferenceCallback) {
71         mFragment = fragment;
72         mRegisteredBatteryCallbackIds = new ArrayList<>();
73         mDevicePreferenceCallback = devicePreferenceCallback;
74         mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
75         mContext = context;
76         mInputManager = context.getSystemService(InputManager.class);
77     }
78 
79     /**
80      * Register the stylus event callback and update the list
81      */
registerCallback()82     public void registerCallback() {
83         for (int deviceId : mInputManager.getInputDeviceIds()) {
84             onInputDeviceAdded(deviceId);
85         }
86         mInputManager.registerInputDeviceListener(this, new Handler(Looper.myLooper()));
87         forceUpdate();
88     }
89 
90     /**
91      * Unregister the stylus event callback
92      */
unregisterCallback()93     public void unregisterCallback() {
94         for (int deviceId : mRegisteredBatteryCallbackIds) {
95             mInputManager.removeInputDeviceBatteryListener(deviceId, this);
96         }
97         mInputManager.unregisterInputDeviceListener(this);
98     }
99 
100     @Override
onInputDeviceAdded(int deviceId)101     public void onInputDeviceAdded(int deviceId) {
102         InputDevice inputDevice = mInputManager.getInputDevice(deviceId);
103         if (inputDevice == null) return;
104 
105         if (inputDevice.supportsSource(InputDevice.SOURCE_STYLUS)
106                 && !inputDevice.isExternal()) {
107             try {
108                 mInputManager.addInputDeviceBatteryListener(deviceId,
109                         mContext.getMainExecutor(), this);
110                 mRegisteredBatteryCallbackIds.add(deviceId);
111             } catch (IllegalArgumentException e) {
112                 Log.e(TAG, e.getMessage());
113             }
114         }
115         forceUpdate();
116     }
117 
118     @Override
onInputDeviceRemoved(int deviceId)119     public void onInputDeviceRemoved(int deviceId) {
120         Log.d(TAG, String.format("Input device removed %d", deviceId));
121         forceUpdate();
122     }
123 
124     @Override
onInputDeviceChanged(int deviceId)125     public void onInputDeviceChanged(int deviceId) {
126         InputDevice inputDevice = mInputManager.getInputDevice(deviceId);
127         if (inputDevice == null) return;
128 
129         if (inputDevice.supportsSource(InputDevice.SOURCE_STYLUS)) {
130             forceUpdate();
131         }
132     }
133 
134 
135     @Override
onBatteryStateChanged(int deviceId, long eventTimeMillis, @NonNull BatteryState batteryState)136     public void onBatteryStateChanged(int deviceId, long eventTimeMillis,
137             @NonNull BatteryState batteryState) {
138         mLastBatteryState = batteryState;
139         mLastDetectedUsiId = deviceId;
140         forceUpdate();
141     }
142 
143     /**
144      * Set the context to generate the {@link Preference}, so it could get the correct theme.
145      */
setPreferenceContext(Context context)146     public void setPreferenceContext(Context context) {
147         mContext = context;
148     }
149 
150     /**
151      * Force update to add or remove stylus preference
152      */
forceUpdate()153     public void forceUpdate() {
154         if (shouldShowUsiPreference()) {
155             addOrUpdateUsiPreference();
156         } else {
157             removeUsiPreference();
158         }
159     }
160 
addOrUpdateUsiPreference()161     private synchronized void addOrUpdateUsiPreference() {
162         if (mUsiPreference == null) {
163             mUsiPreference = new Preference(mContext);
164             mDevicePreferenceCallback.onDeviceAdded(mUsiPreference);
165         }
166         mUsiPreference.setKey(PREF_KEY);
167         mUsiPreference.setTitle(R.string.stylus_connected_devices_title);
168         mUsiPreference.setIcon(R.drawable.ic_stylus);
169         mUsiPreference.setOnPreferenceClickListener((Preference p) -> {
170             mMetricsFeatureProvider.logClickedPreference(p, mFragment.getMetricsCategory());
171             launchDeviceDetails();
172             return true;
173         });
174     }
175 
removeUsiPreference()176     private synchronized void removeUsiPreference() {
177         if (mUsiPreference != null) {
178             mDevicePreferenceCallback.onDeviceRemoved(mUsiPreference);
179             mUsiPreference = null;
180         }
181     }
182 
shouldShowUsiPreference()183     private boolean shouldShowUsiPreference() {
184         return isUsiBatteryValid() && !hasConnectedBluetoothStylusDevice();
185     }
186 
187     @VisibleForTesting
getPreference()188     public Preference getPreference() {
189         return mUsiPreference;
190     }
191 
192     @VisibleForTesting
hasConnectedBluetoothStylusDevice()193     boolean hasConnectedBluetoothStylusDevice() {
194         for (int deviceId : mInputManager.getInputDeviceIds()) {
195             InputDevice device = mInputManager.getInputDevice(deviceId);
196             if (device == null) continue;
197 
198             if (device.supportsSource(InputDevice.SOURCE_STYLUS)
199                     && mInputManager.getInputDeviceBluetoothAddress(deviceId) != null) {
200                 return true;
201             }
202         }
203 
204         return false;
205     }
206 
207     @VisibleForTesting
isUsiBatteryValid()208     boolean isUsiBatteryValid() {
209         return mLastBatteryState != null
210                 && mLastBatteryState.isPresent() && mLastBatteryState.getCapacity() > 0f;
211     }
212 
launchDeviceDetails()213     private void launchDeviceDetails() {
214         final Bundle args = new Bundle();
215         args.putInt(INPUT_ID_ARG, mLastDetectedUsiId);
216 
217         new SubSettingLauncher(mFragment.getContext())
218                 .setDestination(StylusUsiDetailsFragment.class.getName())
219                 .setArguments(args)
220                 .setSourceMetricsCategory(mFragment.getMetricsCategory()).launch();
221     }
222 }
223