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