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