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