1 /* 2 * Copyright (C) 2021 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.google.android.tv.btservices.settings; 18 19 import static com.google.android.tv.btservices.pairing.BluetoothPairer.STATUS_CANCELLED; 20 import static com.google.android.tv.btservices.pairing.BluetoothPairer.STATUS_CONNECTING; 21 import static com.google.android.tv.btservices.pairing.BluetoothPairer.STATUS_DONE; 22 import static com.google.android.tv.btservices.pairing.BluetoothPairer.STATUS_ERROR; 23 import static com.google.android.tv.btservices.pairing.BluetoothPairer.STATUS_PAIRING; 24 import static com.google.android.tv.btservices.pairing.BluetoothPairer.STATUS_TIMEOUT; 25 26 import android.bluetooth.BluetoothDevice; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.os.Bundle; 30 import android.os.Handler; 31 import android.text.TextUtils; 32 import android.widget.Toast; 33 34 import androidx.leanback.preference.LeanbackPreferenceFragment; 35 import androidx.preference.Preference; 36 import androidx.preference.PreferenceGroup; 37 import androidx.preference.PreferenceScreen; 38 39 import com.google.android.tv.btservices.BluetoothUtils; 40 import com.google.android.tv.btservices.R; 41 import com.google.android.tv.btservices.pairing.BluetoothPairingService; 42 43 public class BluetoothDevicePreferenceFragment extends LeanbackPreferenceFragment 44 implements ResponseFragment.Listener, BluetoothDeviceProvider.Listener, 45 BluetoothPairingService.PairingListener { 46 47 private static final String TAG = "Atv.BtDevPrefFragment"; 48 49 public interface ServiceProvider { getBluetoothPairingServiceBinder()50 BluetoothPairingService.LocalBinder getBluetoothPairingServiceBinder(); 51 } 52 53 private PreferenceGroup mPrefGroup; 54 private BluetoothDevice mDevice; 55 56 private static final String ARG_BT_DEVICE = "bt_device"; 57 58 public static final String KEY_BT_DEVICE_PREF = "key_bt_device"; 59 static final String KEY_RENAME = "key_rename"; 60 static final String KEY_CONNECT = "key_connect"; 61 static final String KEY_DISCONNECT = "key_disconnect"; 62 static final String KEY_FORGET = "key_forget"; 63 static final String KEY_UPDATE= "key_update"; 64 private static final String KEY_RECONNECT = "key_reconnect"; 65 static final int YES = R.string.settings_choices_yes; 66 static final int NO = R.string.settings_choices_no; 67 static final int CONTINUE = R.string.settings_continue; 68 static final int CANCEL = R.string.settings_cancel; 69 70 static final boolean ENABLE_DISCONNECT_OPTION = false; 71 private static final int UPDATE_AFTER_RENAME_MS = 1000; 72 73 static final int[] YES_NO_ARGS = {YES, NO}; 74 static final int[] CONT_CANCEL_ARGS = {CONTINUE, CANCEL}; 75 76 private static final int ORDER_UPDATE = 9; 77 private static final int ORDER_RECONNECT = 10; 78 private static final int ORDER_RENAME = 11; 79 private static final int ORDER_DISCONNECT = 12; 80 private static final int ORDER_FORGET= 13; 81 private static final int ORDER_INFO = 14; 82 83 private boolean mVisible = false; 84 85 private final Handler mHandler = new Handler(); 86 87 private BluetoothDeviceInfoPreference mDeviceInfoPreference; 88 buildArgs(Bundle bundle, BluetoothDevice device)89 public static void buildArgs(Bundle bundle, BluetoothDevice device) { 90 bundle.putParcelable(ARG_BT_DEVICE, device); 91 } 92 newInstance(BluetoothDevice device)93 public static BluetoothDevicePreferenceFragment newInstance(BluetoothDevice device) { 94 BluetoothDevicePreferenceFragment fragment = new BluetoothDevicePreferenceFragment(); 95 Bundle args = new Bundle(); 96 args.putParcelable(ARG_BT_DEVICE, device); 97 fragment.setArguments(args); 98 return fragment; 99 } 100 update()101 private void update() { 102 if (!mVisible) { 103 return; 104 } 105 106 if (mDevice != null) { 107 mPrefGroup.setTitle(BluetoothUtils.getName(mDevice)); 108 } 109 110 updateRemoteUpdate(); 111 updateReconnect(); 112 113 if (mDeviceInfoPreference != null) { 114 mDeviceInfoPreference.update(); 115 } 116 } 117 updateRemoteUpdate()118 private void updateRemoteUpdate() { 119 final Context preferenceContext = getPreferenceManager().getContext(); 120 BluetoothUtils.isRemoteClass(mDevice); 121 if (!BluetoothUtils.isRemote(preferenceContext, mDevice)) { 122 return; 123 } 124 BluetoothDeviceProvider btDeviceProvider = getBluetoothDeviceProvider(); 125 Preference pref = mPrefGroup.findPreference(KEY_UPDATE); 126 if (pref == null) { 127 pref = new Preference(preferenceContext); 128 pref.setKey(KEY_UPDATE); 129 pref.setOrder(ORDER_UPDATE); 130 131 ResponseFragment.prepareArgs( 132 pref.getExtras(), 133 KEY_UPDATE, 134 R.string.settings_bt_update, 135 R.string.settings_bt_update_summary, 136 0, 137 CONT_CANCEL_ARGS, 138 null, 139 ResponseFragment.DEFAULT_CHOICE_UNDEFINED 140 ); 141 pref.setFragment(ResponseFragment.class.getCanonicalName()); 142 mPrefGroup.addPreference(pref); 143 } 144 145 if (btDeviceProvider.hasUpgrade(mDevice)) { 146 pref.setEnabled(true); 147 pref.setSelectable(true); 148 pref.setTitle(R.string.settings_bt_update); 149 if (btDeviceProvider.isBatteryLow(mDevice)) { 150 pref.setSummary(R.string.settings_bt_battery_low); 151 pref.setEnabled(false); 152 } else { 153 pref.setSummary(R.string.settings_bt_update_software_available); 154 } 155 } else { 156 pref.setTitle(R.string.settings_bt_update_not_necessary); 157 pref.setSummary(null); 158 pref.setEnabled(false); 159 pref.setSelectable(false); 160 } 161 } 162 updateReconnect()163 private void updateReconnect() { 164 final Context preferenceContext = getPreferenceManager().getContext(); 165 if (mDevice == null || BluetoothUtils.isRemote(preferenceContext, mDevice)) { 166 return; 167 } 168 Preference reconnectPref = mPrefGroup.findPreference(KEY_RECONNECT); 169 if (getPairingServiceBinder() != null && BluetoothUtils.isBonded(mDevice)) { 170 if (reconnectPref == null) { 171 reconnectPref = new Preference(preferenceContext); 172 reconnectPref.setKey(KEY_RECONNECT); 173 reconnectPref.setTitle(R.string.bluetooth_connect); 174 reconnectPref.setOrder(ORDER_RECONNECT); 175 reconnectPref.setOnPreferenceClickListener((pref) -> { 176 BluetoothPairingService.LocalBinder pairingService = getPairingServiceBinder(); 177 if (pairingService != null) { 178 pairingService.connectPairedDevice(mDevice); 179 pref.setEnabled(false); 180 pref.setTitle(R.string.settings_bt_pair_status_connecting); 181 } 182 return true; 183 }); 184 mPrefGroup.addPreference(reconnectPref); 185 } 186 reconnectPref.setEnabled(true); 187 reconnectPref.setVisible(true); 188 } else if (reconnectPref != null) { 189 reconnectPref.setEnabled(false); 190 reconnectPref.setVisible(false); 191 } 192 } 193 updatePairingStatusImpl(int status)194 private void updatePairingStatusImpl(int status) { 195 if (!mVisible) { 196 return; 197 } 198 Preference pref = mPrefGroup.findPreference(KEY_RECONNECT); 199 String resStr; 200 String text; 201 switch (status) { 202 case STATUS_PAIRING: 203 case STATUS_CONNECTING: 204 pref.setEnabled(false); 205 pref.setTitle(R.string.settings_bt_pair_status_connecting); 206 break; 207 case STATUS_CANCELLED: 208 case STATUS_TIMEOUT: 209 case STATUS_ERROR: 210 pref.setEnabled(true); 211 pref.setTitle(R.string.bluetooth_connect); 212 resStr = getResources().getString(R.string.settings_bt_pair_toast_fail); 213 text = String.format(resStr, mDevice.getName()); 214 Toast.makeText(getActivity(), text, Toast.LENGTH_LONG).show(); 215 break; 216 case STATUS_DONE: 217 pref.setEnabled(true); 218 pref.setTitle(R.string.bluetooth_connect); 219 resStr = getResources().getString(R.string.settings_bt_pair_toast_connected); 220 text = String.format(resStr, mDevice.getName()); 221 Toast.makeText(getActivity(), text, Toast.LENGTH_LONG).show(); 222 break; 223 } 224 } 225 getBluetoothDeviceProvider()226 private BluetoothDeviceProvider getBluetoothDeviceProvider() { 227 if (!(getTargetFragment() instanceof ConnectedDevicesPreferenceFragment)) { 228 return null; 229 } 230 return ((ConnectedDevicesPreferenceFragment) getTargetFragment()) 231 .getBluetoothDeviceProvider(); 232 } 233 getPairingServiceBinder()234 private BluetoothPairingService.LocalBinder getPairingServiceBinder() { 235 if (!(getActivity() instanceof ServiceProvider)) { 236 return null; 237 } 238 return ((ServiceProvider) getActivity()).getBluetoothPairingServiceBinder(); 239 } 240 getDevice()241 public BluetoothDevice getDevice() { 242 return mDevice; 243 } 244 245 // ResponseFragment.Listener 246 @Override onChoice(String key, int choice)247 public void onChoice(String key, int choice) { 248 BluetoothDeviceProvider provider = getBluetoothDeviceProvider(); 249 getFragmentManager().popBackStackImmediate(); 250 251 if (provider == null) { 252 return; 253 } 254 if (KEY_DISCONNECT.equals(key)) { 255 if (choice == YES) { 256 // TODO: disconnect device. 257 } 258 } else if (KEY_FORGET.equals(key)) { 259 if (choice == YES) { 260 provider.forgetDevice(mDevice); 261 } 262 } else if (KEY_UPDATE.equals(key)) { 263 if (choice == CONTINUE && mDevice != null) { 264 Context context = getContext(); 265 Intent intent = new Intent(context, RemoteDfuActivity.class); 266 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 267 intent.putExtra(RemoteDfuActivity.EXTRA_BT_ADDRESS, mDevice.getAddress()); 268 context.startActivity(intent); 269 } 270 } 271 } 272 273 // ResponseFragment.Listener 274 @Override onText(String key, String text)275 public void onText(String key, String text) { 276 getFragmentManager().popBackStackImmediate(); 277 BluetoothDeviceProvider provider = getBluetoothDeviceProvider(); 278 if (provider == null) { 279 return; 280 } 281 if (KEY_RENAME.equals(key)) { 282 if (mDevice != null) { 283 provider.renameDevice(mDevice, text); 284 mHandler.postDelayed(this::update, UPDATE_AFTER_RENAME_MS); 285 } 286 } 287 } 288 289 // BluetoothDeviceProvider.Listener 290 @Override onDeviceUpdated(BluetoothDevice device)291 public void onDeviceUpdated(BluetoothDevice device) { 292 if (mDevice == null || !TextUtils.equals(mDevice.getAddress(), device.getAddress())) { 293 return; 294 } 295 mHandler.post(this::update); 296 } 297 298 // BluetoothPairingService.PairingListener implementation 299 @Override updatePairingStatus(BluetoothDevice device, int status)300 public void updatePairingStatus(BluetoothDevice device, int status) { 301 if (!TextUtils.equals(mDevice.getAddress(), device.getAddress())) { 302 return; 303 } 304 mHandler.post(() -> this.updatePairingStatusImpl(status)); 305 } 306 307 @Override onCreate(Bundle savedInstanceState)308 public void onCreate(Bundle savedInstanceState) { 309 super.onCreate(savedInstanceState); 310 BluetoothDeviceProvider provider = getBluetoothDeviceProvider(); 311 if (provider != null) { 312 provider.addListener(this); 313 } 314 BluetoothPairingService.LocalBinder pairingService = getPairingServiceBinder(); 315 if (pairingService != null) { 316 pairingService.addPairingListener(this); 317 } 318 } 319 320 @Override onDestroy()321 public void onDestroy() { 322 BluetoothDeviceProvider provider = getBluetoothDeviceProvider(); 323 if (provider != null) { 324 provider.removeListener(this); 325 } 326 BluetoothPairingService.LocalBinder pairingService = getPairingServiceBinder(); 327 if (pairingService != null) { 328 pairingService.removePairingListener(this); 329 } 330 super.onDestroy(); 331 } 332 333 @Override onResume()334 public void onResume() { 335 super.onResume(); 336 mVisible = true; 337 update(); 338 } 339 340 @Override onPause()341 public void onPause() { 342 mVisible = false; 343 super.onPause(); 344 } 345 346 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)347 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 348 final Context preferenceContext = getPreferenceManager().getContext(); 349 BluetoothDeviceProvider btDeviceProvider = getBluetoothDeviceProvider(); 350 351 Bundle args = getArguments(); 352 mDevice = args.getParcelable(ARG_BT_DEVICE); 353 mPrefGroup = getPreferenceManager().createPreferenceScreen(preferenceContext); 354 mPrefGroup.setOrderingAsAdded(false); 355 mPrefGroup.setKey(KEY_BT_DEVICE_PREF); 356 357 String deviceName = ""; 358 if (mDevice != null) { 359 deviceName = BluetoothUtils.getName(mDevice); 360 mPrefGroup.setTitle(deviceName); 361 } 362 363 updateRemoteUpdate(); 364 updateReconnect(); 365 366 Preference renamePref = new Preference(preferenceContext); 367 renamePref.setKey(KEY_RENAME); 368 renamePref.setTitle(R.string.bluetooth_rename); 369 renamePref.setOrder(ORDER_RENAME); 370 ResponseFragment.prepareArgs( 371 renamePref.getExtras(), 372 KEY_RENAME, 373 R.string.settings_bt_rename, 374 0, 375 R.drawable.ic_baseline_bluetooth_searching_large, 376 null, 377 deviceName, 378 ResponseFragment.DEFAULT_CHOICE_UNDEFINED 379 ); 380 renamePref.setFragment(ResponseFragment.class.getCanonicalName()); 381 mPrefGroup.addPreference(renamePref); 382 383 if (ENABLE_DISCONNECT_OPTION) { 384 Preference disconnectPref = new Preference(preferenceContext); 385 disconnectPref.setKey(KEY_DISCONNECT); 386 disconnectPref.setTitle(R.string.bluetooth_disconnect); 387 disconnectPref.setOrder(ORDER_DISCONNECT); 388 ResponseFragment.prepareArgs( 389 disconnectPref.getExtras(), 390 KEY_DISCONNECT, 391 R.string.settings_bt_disconnect, 392 0, 393 R.drawable.ic_baseline_bluetooth_searching_large, 394 YES_NO_ARGS, 395 deviceName, 396 1 /* default to NO (index 1) */ 397 ); 398 disconnectPref.setFragment(ResponseFragment.class.getCanonicalName()); 399 mPrefGroup.addPreference(disconnectPref); 400 } 401 402 Preference forgetPref = new Preference(preferenceContext); 403 forgetPref.setKey(KEY_FORGET); 404 forgetPref.setTitle(R.string.bluetooth_forget); 405 forgetPref.setOrder(ORDER_FORGET); 406 ResponseFragment.prepareArgs( 407 forgetPref.getExtras(), 408 KEY_FORGET, 409 R.string.settings_bt_forget, 410 0, 411 R.drawable.ic_baseline_bluetooth_searching_large, 412 YES_NO_ARGS, 413 deviceName, 414 1 /* default to NO (index 1) */ 415 ); 416 forgetPref.setFragment(ResponseFragment.class.getCanonicalName()); 417 mPrefGroup.addPreference(forgetPref); 418 419 mDeviceInfoPreference = 420 new BluetoothDeviceInfoPreference(preferenceContext, btDeviceProvider, mDevice); 421 mDeviceInfoPreference.setOrder(ORDER_INFO); 422 mPrefGroup.addPreference(mDeviceInfoPreference); 423 424 setPreferenceScreen((PreferenceScreen) mPrefGroup); 425 } 426 } 427