• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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