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