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 if (getPreferenceScreen().getPreferenceCount() == 0) { 324 getPreferenceScreen().addPreference(mPairedDevicesCategory); 325 getPreferenceScreen().addPreference(mAvailableDevicesCategory); 326 getPreferenceScreen().addPreference(mMyDevicePreference); 327 } 328 329 // Paired devices category 330 addDeviceCategory(mPairedDevicesCategory, 331 R.string.bluetooth_preference_paired_devices, 332 BluetoothDeviceFilter.BONDED_DEVICE_FILTER, true); 333 int numberOfPairedDevices = mPairedDevicesCategory.getPreferenceCount(); 334 335 if (isUiRestricted() || numberOfPairedDevices <= 0) { 336 if (preferenceScreen.findPreference(KEY_PAIRED_DEVICES) != null) { 337 preferenceScreen.removePreference(mPairedDevicesCategory); 338 } 339 } else { 340 if (preferenceScreen.findPreference(KEY_PAIRED_DEVICES) == null) { 341 preferenceScreen.addPreference(mPairedDevicesCategory); 342 } 343 } 344 345 // Available devices category 346 addDeviceCategory(mAvailableDevicesCategory, 347 R.string.bluetooth_preference_found_devices, 348 BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER, mInitialScanStarted); 349 350 if (!mInitialScanStarted) { 351 startScanning(); 352 } 353 354 final Resources res = getResources(); 355 final Locale locale = res.getConfiguration().getLocales().get(0); 356 final BidiFormatter bidiFormatter = BidiFormatter.getInstance(locale); 357 mMyDevicePreference.setSummary(res.getString( 358 R.string.bluetooth_is_visible_message, 359 bidiFormatter.unicodeWrap(mLocalAdapter.getName()))); 360 361 getActivity().invalidateOptionsMenu(); 362 363 // mLocalAdapter.setScanMode is internally synchronized so it is okay for multiple 364 // threads to execute. 365 if (mInitiateDiscoverable) { 366 // Make the device visible to other devices. 367 mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE); 368 mInitiateDiscoverable = false; 369 } 370 return; // not break 371 372 case BluetoothAdapter.STATE_TURNING_OFF: 373 messageId = R.string.bluetooth_turning_off; 374 break; 375 376 case BluetoothAdapter.STATE_OFF: 377 setOffMessage(); 378 if (isUiRestricted()) { 379 messageId = R.string.bluetooth_empty_list_user_restricted; 380 } 381 break; 382 383 case BluetoothAdapter.STATE_TURNING_ON: 384 messageId = R.string.bluetooth_turning_on; 385 mInitialScanStarted = false; 386 break; 387 } 388 389 setDeviceListGroup(preferenceScreen); 390 removeAllDevices(); 391 if (messageId != 0) { 392 getEmptyTextView().setText(messageId); 393 } 394 if (!isUiRestricted()) { 395 getActivity().invalidateOptionsMenu(); 396 } 397 } 398 399 private void setOffMessage() { 400 final TextView emptyView = getEmptyTextView(); 401 if (emptyView == null) { 402 return; 403 } 404 final CharSequence briefText = getText(R.string.bluetooth_empty_list_bluetooth_off); 405 406 final ContentResolver resolver = getActivity().getContentResolver(); 407 final boolean bleScanningMode = Settings.Global.getInt( 408 resolver, Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE, 0) == 1; 409 410 if (!bleScanningMode) { 411 // Show only the brief text if the scanning mode has been turned off. 412 emptyView.setText(briefText, TextView.BufferType.SPANNABLE); 413 } else { 414 final StringBuilder contentBuilder = new StringBuilder(); 415 contentBuilder.append(briefText); 416 contentBuilder.append("\n\n"); 417 contentBuilder.append(getText(R.string.ble_scan_notify_text)); 418 LinkifyUtils.linkify(emptyView, contentBuilder, new LinkifyUtils.OnClickListener() { 419 @Override 420 public void onClick() { 421 final SettingsActivity activity = 422 (SettingsActivity) BluetoothSettings.this.getActivity(); 423 activity.startPreferencePanel(ScanningSettings.class.getName(), null, 424 R.string.location_scanning_screen_title, null, null, 0); 425 } 426 }); 427 } 428 getPreferenceScreen().removeAll(); 429 Spannable boldSpan = (Spannable) emptyView.getText(); 430 boldSpan.setSpan( 431 new TextAppearanceSpan(getActivity(), android.R.style.TextAppearance_Medium), 0, 432 briefText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 433 } 434 435 @Override 436 public void onBluetoothStateChanged(int bluetoothState) { 437 super.onBluetoothStateChanged(bluetoothState); 438 // If BT is turned off/on staying in the same BT Settings screen 439 // discoverability to be set again 440 if (BluetoothAdapter.STATE_ON == bluetoothState) 441 mInitiateDiscoverable = true; 442 updateContent(bluetoothState); 443 } 444 445 @Override 446 public void onScanningStateChanged(boolean started) { 447 super.onScanningStateChanged(started); 448 // Update options' enabled state 449 if (getActivity() != null) { 450 getActivity().invalidateOptionsMenu(); 451 } 452 } 453 454 @Override 455 public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { 456 setDeviceListGroup(getPreferenceScreen()); 457 removeAllDevices(); 458 updateContent(mLocalAdapter.getBluetoothState()); 459 } 460 461 private final View.OnClickListener mDeviceProfilesListener = new View.OnClickListener() { 462 @Override 463 public void onClick(View v) { 464 // User clicked on advanced options icon for a device in the list 465 if (!(v.getTag() instanceof CachedBluetoothDevice)) { 466 Log.w(TAG, "onClick() called for other View: " + v); 467 return; 468 } 469 470 final CachedBluetoothDevice device = (CachedBluetoothDevice) v.getTag(); 471 Bundle args = new Bundle(); 472 args.putString(DeviceProfilesSettings.ARG_DEVICE_ADDRESS, 473 device.getDevice().getAddress()); 474 DeviceProfilesSettings profileSettings = new DeviceProfilesSettings(); 475 profileSettings.setArguments(args); 476 profileSettings.show(getFragmentManager(), 477 DeviceProfilesSettings.class.getSimpleName()); 478 } 479 }; 480 481 /** 482 * Add a listener, which enables the advanced settings icon. 483 * @param preference the newly added preference 484 */ 485 @Override 486 void initDevicePreference(BluetoothDevicePreference preference) { 487 CachedBluetoothDevice cachedDevice = preference.getCachedDevice(); 488 if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) { 489 // Only paired device have an associated advanced settings screen 490 preference.setOnSettingsClickListener(mDeviceProfilesListener); 491 } 492 } 493 494 @Override 495 protected int getHelpResource() { 496 return R.string.help_url_bluetooth; 497 } 498 499 private static class SummaryProvider 500 implements SummaryLoader.SummaryProvider, BluetoothCallback { 501 502 private final LocalBluetoothManager mBluetoothManager; 503 private final Context mContext; 504 private final SummaryLoader mSummaryLoader; 505 506 private boolean mEnabled; 507 private boolean mConnected; 508 509 public SummaryProvider(Context context, SummaryLoader summaryLoader) { 510 mBluetoothManager = Utils.getLocalBtManager(context); 511 mContext = context; 512 mSummaryLoader = summaryLoader; 513 } 514 515 @Override 516 public void setListening(boolean listening) { 517 BluetoothAdapter defaultAdapter = BluetoothAdapter.getDefaultAdapter(); 518 if (defaultAdapter == null) return; 519 if (listening) { 520 mEnabled = defaultAdapter.isEnabled(); 521 mConnected = 522 defaultAdapter.getConnectionState() == BluetoothAdapter.STATE_CONNECTED; 523 mSummaryLoader.setSummary(this, getSummary()); 524 mBluetoothManager.getEventManager().registerCallback(this); 525 } else { 526 mBluetoothManager.getEventManager().unregisterCallback(this); 527 } 528 } 529 530 private CharSequence getSummary() { 531 return mContext.getString(!mEnabled ? R.string.bluetooth_disabled 532 : mConnected ? R.string.bluetooth_connected 533 : R.string.bluetooth_disconnected); 534 } 535 536 @Override 537 public void onBluetoothStateChanged(int bluetoothState) { 538 mEnabled = bluetoothState == BluetoothAdapter.STATE_ON; 539 mSummaryLoader.setSummary(this, getSummary()); 540 } 541 542 @Override 543 public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { 544 mConnected = state == BluetoothAdapter.STATE_CONNECTED; 545 mSummaryLoader.setSummary(this, getSummary()); 546 } 547 548 @Override 549 public void onScanningStateChanged(boolean started) { 550 551 } 552 553 @Override 554 public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { 555 556 } 557 558 @Override 559 public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { 560 561 } 562 563 @Override 564 public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { 565 566 } 567 } 568 569 public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY 570 = new SummaryLoader.SummaryProviderFactory() { 571 @Override 572 public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, 573 SummaryLoader summaryLoader) { 574 return new SummaryProvider(activity, summaryLoader); 575 } 576 }; 577 578 public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 579 new BaseSearchIndexProvider() { 580 @Override 581 public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { 582 583 final List<SearchIndexableRaw> result = new ArrayList<SearchIndexableRaw>(); 584 585 final Resources res = context.getResources(); 586 587 // Add fragment title 588 SearchIndexableRaw data = new SearchIndexableRaw(context); 589 data.title = res.getString(R.string.bluetooth_settings); 590 data.screenTitle = res.getString(R.string.bluetooth_settings); 591 result.add(data); 592 593 // Add cached paired BT devices 594 LocalBluetoothManager lbtm = Utils.getLocalBtManager(context); 595 // LocalBluetoothManager.getInstance can return null if the device does not 596 // support bluetooth (e.g. the emulator). 597 if (lbtm != null) { 598 Set<BluetoothDevice> bondedDevices = 599 lbtm.getBluetoothAdapter().getBondedDevices(); 600 601 for (BluetoothDevice device : bondedDevices) { 602 data = new SearchIndexableRaw(context); 603 data.title = device.getName(); 604 data.screenTitle = res.getString(R.string.bluetooth_settings); 605 data.enabled = enabled; 606 result.add(data); 607 } 608 } 609 return result; 610 } 611 }; 612 } 613