1 /* 2 * Copyright (C) 2011 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.bluetooth; 18 19 import android.app.Activity; 20 import android.bluetooth.BluetoothAdapter; 21 import android.bluetooth.BluetoothDevice; 22 import android.content.BroadcastReceiver; 23 import android.content.ContentResolver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.content.res.Resources; 28 import android.os.Bundle; 29 import android.provider.Settings; 30 import android.support.annotation.VisibleForTesting; 31 import android.support.v7.preference.PreferenceCategory; 32 import android.support.v7.preference.PreferenceGroup; 33 import android.support.v7.preference.PreferenceScreen; 34 import android.text.BidiFormatter; 35 import android.text.Spannable; 36 import android.text.style.TextAppearanceSpan; 37 import android.util.Log; 38 import android.view.Menu; 39 import android.view.MenuInflater; 40 import android.view.MenuItem; 41 import android.view.View; 42 import android.widget.TextView; 43 44 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 45 import com.android.settings.LinkifyUtils; 46 import com.android.settings.R; 47 import com.android.settings.SettingsActivity; 48 import com.android.settings.dashboard.SummaryLoader; 49 import com.android.settings.location.ScanningSettings; 50 import com.android.settings.search.BaseSearchIndexProvider; 51 import com.android.settings.search.Indexable; 52 import com.android.settings.search.SearchIndexableRaw; 53 import com.android.settings.widget.FooterPreference; 54 import com.android.settings.widget.GearPreference; 55 import com.android.settings.widget.SummaryUpdater.OnSummaryChangeListener; 56 import com.android.settings.widget.SwitchBar; 57 import com.android.settings.widget.SwitchBarController; 58 import com.android.settingslib.bluetooth.BluetoothDeviceFilter; 59 import com.android.settingslib.bluetooth.CachedBluetoothDevice; 60 import com.android.settingslib.bluetooth.LocalBluetoothManager; 61 62 import java.util.ArrayList; 63 import java.util.List; 64 import java.util.Locale; 65 import java.util.Set; 66 67 import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH; 68 69 /** 70 * BluetoothSettings is the Settings screen for Bluetooth configuration and 71 * connection management. 72 */ 73 public final class BluetoothSettings extends DeviceListPreferenceFragment implements Indexable { 74 private static final String TAG = "BluetoothSettings"; 75 76 private static final int MENU_ID_SCAN = Menu.FIRST; 77 private static final int MENU_ID_RENAME_DEVICE = Menu.FIRST + 1; 78 private static final int MENU_ID_SHOW_RECEIVED = Menu.FIRST + 2; 79 80 /* Private intent to show the list of received files */ 81 private static final String BTOPP_ACTION_OPEN_RECEIVED_FILES = 82 "android.btopp.intent.action.OPEN_RECEIVED_FILES"; 83 private static final String BTOPP_PACKAGE = 84 "com.android.bluetooth"; 85 86 private static final String KEY_PAIRED_DEVICES = "paired_devices"; 87 88 private static View mSettingsDialogView = null; 89 90 private BluetoothEnabler mBluetoothEnabler; 91 92 private PreferenceGroup mPairedDevicesCategory; 93 private PreferenceGroup mAvailableDevicesCategory; 94 private boolean mAvailableDevicesCategoryIsPresent; 95 96 private boolean mInitialScanStarted; 97 private boolean mInitiateDiscoverable; 98 99 private SwitchBar mSwitchBar; 100 101 private final IntentFilter mIntentFilter; 102 103 // For Search 104 private static final String DATA_KEY_REFERENCE = "main_toggle_bluetooth"; 105 106 // accessed from inner class (not private to avoid thunks) 107 FooterPreference mMyDevicePreference; 108 109 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 110 @Override 111 public void onReceive(Context context, Intent intent) { 112 final String action = intent.getAction(); 113 final int state = 114 intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); 115 116 if (action.equals(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED)) { 117 updateDeviceName(context); 118 } 119 120 if (state == BluetoothAdapter.STATE_ON) { 121 mInitiateDiscoverable = true; 122 } 123 } 124 125 private void updateDeviceName(Context context) { 126 if (mLocalAdapter.isEnabled() && mMyDevicePreference != null) { 127 final Resources res = context.getResources(); 128 final Locale locale = res.getConfiguration().getLocales().get(0); 129 final BidiFormatter bidiFormatter = BidiFormatter.getInstance(locale); 130 mMyDevicePreference.setTitle(res.getString( 131 R.string.bluetooth_is_visible_message, 132 bidiFormatter.unicodeWrap(mLocalAdapter.getName()))); 133 } 134 } 135 }; 136 137 public BluetoothSettings() { 138 super(DISALLOW_CONFIG_BLUETOOTH); 139 mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED); 140 } 141 142 @Override 143 public int getMetricsCategory() { 144 return MetricsEvent.BLUETOOTH; 145 } 146 147 @Override 148 public void onActivityCreated(Bundle savedInstanceState) { 149 super.onActivityCreated(savedInstanceState); 150 mInitialScanStarted = false; 151 mInitiateDiscoverable = true; 152 153 final SettingsActivity activity = (SettingsActivity) getActivity(); 154 mSwitchBar = activity.getSwitchBar(); 155 156 mBluetoothEnabler = new BluetoothEnabler(activity, new SwitchBarController(mSwitchBar), 157 mMetricsFeatureProvider, Utils.getLocalBtManager(activity), 158 MetricsEvent.ACTION_BLUETOOTH_TOGGLE); 159 mBluetoothEnabler.setupSwitchController(); 160 } 161 162 @Override 163 public void onDestroyView() { 164 super.onDestroyView(); 165 166 mBluetoothEnabler.teardownSwitchController(); 167 } 168 169 @Override 170 void addPreferencesForActivity() { 171 addPreferencesFromResource(R.xml.bluetooth_settings); 172 final Context prefContext = getPrefContext(); 173 mPairedDevicesCategory = new PreferenceCategory(prefContext); 174 mPairedDevicesCategory.setKey(KEY_PAIRED_DEVICES); 175 mPairedDevicesCategory.setOrder(1); 176 getPreferenceScreen().addPreference(mPairedDevicesCategory); 177 178 mAvailableDevicesCategory = new BluetoothProgressCategory(prefContext); 179 mAvailableDevicesCategory.setSelectable(false); 180 mAvailableDevicesCategory.setOrder(2); 181 getPreferenceScreen().addPreference(mAvailableDevicesCategory); 182 183 mMyDevicePreference = mFooterPreferenceMixin.createFooterPreference(); 184 mMyDevicePreference.setSelectable(false); 185 186 setHasOptionsMenu(true); 187 } 188 189 @Override 190 public void onStart() { 191 // resume BluetoothEnabler before calling super.onStart() so we don't get 192 // any onDeviceAdded() callbacks before setting up view in updateContent() 193 if (mBluetoothEnabler != null) { 194 mBluetoothEnabler.resume(getActivity()); 195 } 196 super.onStart(); 197 198 mInitiateDiscoverable = true; 199 200 if (isUiRestricted()) { 201 setDeviceListGroup(getPreferenceScreen()); 202 if (!isUiRestrictedByOnlyAdmin()) { 203 getEmptyTextView().setText(R.string.bluetooth_empty_list_user_restricted); 204 } 205 removeAllDevices(); 206 return; 207 } 208 209 getActivity().registerReceiver(mReceiver, mIntentFilter); 210 if (mLocalAdapter != null) { 211 updateContent(mLocalAdapter.getBluetoothState()); 212 } 213 } 214 215 @Override 216 public void onStop() { 217 super.onStop(); 218 if (mBluetoothEnabler != null) { 219 mBluetoothEnabler.pause(); 220 } 221 222 // Make the device only visible to connected devices. 223 mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE); 224 225 if (isUiRestricted()) { 226 return; 227 } 228 229 getActivity().unregisterReceiver(mReceiver); 230 } 231 232 @Override 233 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 234 if (mLocalAdapter == null) return; 235 // If the user is not allowed to configure bluetooth, do not show the menu. 236 if (isUiRestricted()) return; 237 238 boolean bluetoothIsEnabled = mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_ON; 239 boolean isDiscovering = mLocalAdapter.isDiscovering(); 240 int textId = isDiscovering ? R.string.bluetooth_searching_for_devices : 241 R.string.bluetooth_search_for_devices; 242 menu.add(Menu.NONE, MENU_ID_SCAN, 0, textId) 243 .setEnabled(bluetoothIsEnabled && !isDiscovering) 244 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); 245 menu.add(Menu.NONE, MENU_ID_RENAME_DEVICE, 0, R.string.bluetooth_rename_device) 246 .setEnabled(bluetoothIsEnabled) 247 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); 248 menu.add(Menu.NONE, MENU_ID_SHOW_RECEIVED, 0, R.string.bluetooth_show_received_files) 249 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); 250 super.onCreateOptionsMenu(menu, inflater); 251 } 252 253 @Override 254 public boolean onOptionsItemSelected(MenuItem item) { 255 switch (item.getItemId()) { 256 case MENU_ID_SCAN: 257 if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_ON) { 258 mMetricsFeatureProvider.action(getActivity(), 259 MetricsEvent.ACTION_BLUETOOTH_SCAN); 260 startScanning(); 261 } 262 return true; 263 264 case MENU_ID_RENAME_DEVICE: 265 mMetricsFeatureProvider.action(getActivity(), 266 MetricsEvent.ACTION_BLUETOOTH_RENAME); 267 new BluetoothNameDialogFragment().show( 268 getFragmentManager(), "rename device"); 269 return true; 270 271 case MENU_ID_SHOW_RECEIVED: 272 mMetricsFeatureProvider.action(getActivity(), 273 MetricsEvent.ACTION_BLUETOOTH_FILES); 274 Intent intent = new Intent(BTOPP_ACTION_OPEN_RECEIVED_FILES); 275 intent.setPackage(BTOPP_PACKAGE); 276 getActivity().sendBroadcast(intent); 277 return true; 278 } 279 return super.onOptionsItemSelected(item); 280 } 281 282 private void startScanning() { 283 if (isUiRestricted()) { 284 return; 285 } 286 287 if (!mAvailableDevicesCategoryIsPresent) { 288 getPreferenceScreen().addPreference(mAvailableDevicesCategory); 289 mAvailableDevicesCategoryIsPresent = true; 290 } 291 292 if (mAvailableDevicesCategory != null) { 293 setDeviceListGroup(mAvailableDevicesCategory); 294 removeAllDevices(); 295 } 296 297 mLocalManager.getCachedDeviceManager().clearNonBondedDevices(); 298 mAvailableDevicesCategory.removeAll(); 299 mInitialScanStarted = true; 300 mLocalAdapter.startScanning(true); 301 } 302 303 @Override 304 void onDevicePreferenceClick(BluetoothDevicePreference btPreference) { 305 mLocalAdapter.stopScanning(); 306 super.onDevicePreferenceClick(btPreference); 307 } 308 309 private void addDeviceCategory(PreferenceGroup preferenceGroup, int titleId, 310 BluetoothDeviceFilter.Filter filter, boolean addCachedDevices) { 311 cacheRemoveAllPrefs(preferenceGroup); 312 preferenceGroup.setTitle(titleId); 313 setFilter(filter); 314 setDeviceListGroup(preferenceGroup); 315 if (addCachedDevices) { 316 addCachedDevices(); 317 } 318 preferenceGroup.setEnabled(true); 319 removeCachedPrefs(preferenceGroup); 320 } 321 322 private void updateContent(int bluetoothState) { 323 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 324 int messageId = 0; 325 326 switch (bluetoothState) { 327 case BluetoothAdapter.STATE_ON: 328 mDevicePreferenceMap.clear(); 329 330 if (isUiRestricted()) { 331 messageId = R.string.bluetooth_empty_list_user_restricted; 332 break; 333 } 334 getPreferenceScreen().removeAll(); 335 getPreferenceScreen().addPreference(mPairedDevicesCategory); 336 getPreferenceScreen().addPreference(mAvailableDevicesCategory); 337 getPreferenceScreen().addPreference(mMyDevicePreference); 338 339 // Paired devices category 340 addDeviceCategory(mPairedDevicesCategory, 341 R.string.bluetooth_preference_paired_devices, 342 BluetoothDeviceFilter.BONDED_DEVICE_FILTER, true); 343 int numberOfPairedDevices = mPairedDevicesCategory.getPreferenceCount(); 344 345 if (isUiRestricted() || numberOfPairedDevices <= 0) { 346 if (preferenceScreen.findPreference(KEY_PAIRED_DEVICES) != null) { 347 preferenceScreen.removePreference(mPairedDevicesCategory); 348 } 349 } else { 350 if (preferenceScreen.findPreference(KEY_PAIRED_DEVICES) == null) { 351 preferenceScreen.addPreference(mPairedDevicesCategory); 352 } 353 } 354 355 // Available devices category 356 addDeviceCategory(mAvailableDevicesCategory, 357 R.string.bluetooth_preference_found_devices, 358 BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER, mInitialScanStarted); 359 360 if (!mInitialScanStarted) { 361 startScanning(); 362 } 363 364 final Resources res = getResources(); 365 final Locale locale = res.getConfiguration().getLocales().get(0); 366 final BidiFormatter bidiFormatter = BidiFormatter.getInstance(locale); 367 mMyDevicePreference.setTitle(res.getString( 368 R.string.bluetooth_is_visible_message, 369 bidiFormatter.unicodeWrap(mLocalAdapter.getName()))); 370 371 getActivity().invalidateOptionsMenu(); 372 373 // mLocalAdapter.setScanMode is internally synchronized so it is okay for multiple 374 // threads to execute. 375 if (mInitiateDiscoverable) { 376 // Make the device visible to other devices. 377 mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); 378 mInitiateDiscoverable = false; 379 } 380 return; // not break 381 382 case BluetoothAdapter.STATE_TURNING_OFF: 383 messageId = R.string.bluetooth_turning_off; 384 break; 385 386 case BluetoothAdapter.STATE_OFF: 387 setOffMessage(); 388 if (isUiRestricted()) { 389 messageId = R.string.bluetooth_empty_list_user_restricted; 390 } 391 break; 392 393 case BluetoothAdapter.STATE_TURNING_ON: 394 messageId = R.string.bluetooth_turning_on; 395 mInitialScanStarted = false; 396 break; 397 } 398 399 setDeviceListGroup(preferenceScreen); 400 removeAllDevices(); 401 if (messageId != 0) { 402 getEmptyTextView().setText(messageId); 403 } 404 if (!isUiRestricted()) { 405 getActivity().invalidateOptionsMenu(); 406 } 407 } 408 409 private void setOffMessage() { 410 final TextView emptyView = getEmptyTextView(); 411 if (emptyView == null) { 412 return; 413 } 414 final CharSequence briefText = getText(R.string.bluetooth_empty_list_bluetooth_off); 415 416 final ContentResolver resolver = getActivity().getContentResolver(); 417 final boolean bleScanningMode = Settings.Global.getInt( 418 resolver, Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE, 0) == 1; 419 420 if (!bleScanningMode) { 421 // Show only the brief text if the scanning mode has been turned off. 422 emptyView.setText(briefText, TextView.BufferType.SPANNABLE); 423 } else { 424 final StringBuilder contentBuilder = new StringBuilder(); 425 contentBuilder.append(briefText); 426 contentBuilder.append("\n\n"); 427 contentBuilder.append(getText(R.string.ble_scan_notify_text)); 428 LinkifyUtils.linkify(emptyView, contentBuilder, new LinkifyUtils.OnClickListener() { 429 @Override 430 public void onClick() { 431 final SettingsActivity activity = 432 (SettingsActivity) BluetoothSettings.this.getActivity(); 433 activity.startPreferencePanel(BluetoothSettings.this, 434 ScanningSettings.class.getName(), null, 435 R.string.location_scanning_screen_title, null, null, 0); 436 } 437 }); 438 } 439 getPreferenceScreen().removeAll(); 440 setTextSpan(emptyView.getText(), briefText); 441 } 442 443 @Override 444 public void onBluetoothStateChanged(int bluetoothState) { 445 super.onBluetoothStateChanged(bluetoothState); 446 // If BT is turned off/on staying in the same BT Settings screen 447 // discoverability to be set again 448 if (BluetoothAdapter.STATE_ON == bluetoothState) { 449 mInitiateDiscoverable = true; 450 } 451 updateContent(bluetoothState); 452 } 453 454 @Override 455 public void onScanningStateChanged(boolean started) { 456 super.onScanningStateChanged(started); 457 // Update options' enabled state 458 if (getActivity() != null) { 459 getActivity().invalidateOptionsMenu(); 460 } 461 } 462 463 @Override 464 public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { 465 setDeviceListGroup(getPreferenceScreen()); 466 removeAllDevices(); 467 updateContent(mLocalAdapter.getBluetoothState()); 468 } 469 470 @VisibleForTesting 471 void setTextSpan(CharSequence text, CharSequence briefText) { 472 if (text instanceof Spannable) { 473 Spannable boldSpan = (Spannable) text; 474 boldSpan.setSpan( 475 new TextAppearanceSpan(getActivity(), android.R.style.TextAppearance_Medium), 0, 476 briefText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 477 } 478 } 479 480 private final GearPreference.OnGearClickListener mDeviceProfilesListener = pref -> { 481 // User clicked on advanced options icon for a device in the list 482 if (!(pref instanceof BluetoothDevicePreference)) { 483 Log.w(TAG, "onClick() called for other View: " + pref); 484 return; 485 } 486 final CachedBluetoothDevice device = 487 ((BluetoothDevicePreference) pref).getBluetoothDevice(); 488 if (device == null) { 489 Log.w(TAG, "No BT device attached with this pref: " + pref); 490 return; 491 } 492 final Bundle args = new Bundle(); 493 args.putString(DeviceProfilesSettings.ARG_DEVICE_ADDRESS, 494 device.getDevice().getAddress()); 495 final DeviceProfilesSettings profileSettings = new DeviceProfilesSettings(); 496 profileSettings.setArguments(args); 497 profileSettings.show(getFragmentManager(), 498 DeviceProfilesSettings.class.getSimpleName()); 499 }; 500 501 /** 502 * Add a listener, which enables the advanced settings icon. 503 * 504 * @param preference the newly added preference 505 */ 506 @Override 507 void initDevicePreference(BluetoothDevicePreference preference) { 508 CachedBluetoothDevice cachedDevice = preference.getCachedDevice(); 509 if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) { 510 // Only paired device have an associated advanced settings screen 511 preference.setOnGearClickListener(mDeviceProfilesListener); 512 } 513 } 514 515 @Override 516 protected int getHelpResource() { 517 return R.string.help_url_bluetooth; 518 } 519 520 @VisibleForTesting 521 static class SummaryProvider implements SummaryLoader.SummaryProvider, OnSummaryChangeListener { 522 523 private final LocalBluetoothManager mBluetoothManager; 524 private final Context mContext; 525 private final SummaryLoader mSummaryLoader; 526 527 @VisibleForTesting 528 BluetoothSummaryUpdater mSummaryUpdater; 529 530 public SummaryProvider(Context context, SummaryLoader summaryLoader, 531 LocalBluetoothManager bluetoothManager) { 532 mBluetoothManager = bluetoothManager; 533 mContext = context; 534 mSummaryLoader = summaryLoader; 535 mSummaryUpdater = new BluetoothSummaryUpdater(mContext, this, mBluetoothManager); 536 } 537 538 @Override 539 public void setListening(boolean listening) { 540 mSummaryUpdater.register(listening); 541 } 542 543 @Override 544 public void onSummaryChanged(String summary) { 545 if (mSummaryLoader != null) { 546 mSummaryLoader.setSummary(this, summary); 547 } 548 } 549 } 550 551 public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY 552 = new SummaryLoader.SummaryProviderFactory() { 553 @Override 554 public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, 555 SummaryLoader summaryLoader) { 556 557 return new SummaryProvider(activity, summaryLoader, Utils.getLocalBtManager(activity)); 558 } 559 }; 560 561 public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 562 new BaseSearchIndexProvider() { 563 @Override 564 public List<SearchIndexableRaw> getRawDataToIndex(Context context, 565 boolean enabled) { 566 567 final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(); 568 569 final Resources res = context.getResources(); 570 571 // Add fragment title 572 SearchIndexableRaw data = new SearchIndexableRaw(context); 573 data.title = res.getString(R.string.bluetooth_settings); 574 data.screenTitle = res.getString(R.string.bluetooth_settings); 575 data.key = DATA_KEY_REFERENCE; 576 result.add(data); 577 578 // Add cached paired BT devices 579 LocalBluetoothManager lbtm = Utils.getLocalBtManager(context); 580 // LocalBluetoothManager.getInstance can return null if the device does not 581 // support bluetooth (e.g. the emulator). 582 if (lbtm != null) { 583 Set<BluetoothDevice> bondedDevices = 584 lbtm.getBluetoothAdapter().getBondedDevices(); 585 586 for (BluetoothDevice device : bondedDevices) { 587 data = new SearchIndexableRaw(context); 588 data.title = device.getName(); 589 data.screenTitle = res.getString(R.string.bluetooth_settings); 590 data.enabled = enabled; 591 result.add(data); 592 } 593 } 594 return result; 595 } 596 }; 597 } 598