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
17package com.android.settings.bluetooth;
18
19import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
20
21import android.app.Activity;
22import android.app.AlertDialog;
23import android.bluetooth.BluetoothAdapter;
24import android.bluetooth.BluetoothDevice;
25import android.content.BroadcastReceiver;
26import android.content.Context;
27import android.content.DialogInterface;
28import android.content.Intent;
29import android.content.IntentFilter;
30import android.content.res.Resources;
31import android.content.SharedPreferences;
32import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
33import android.os.Bundle;
34import android.preference.Preference;
35import android.preference.PreferenceCategory;
36import android.preference.PreferenceFragment;
37import android.preference.PreferenceGroup;
38import android.preference.PreferenceScreen;
39import android.util.Log;
40import android.view.LayoutInflater;
41import android.view.Menu;
42import android.view.MenuInflater;
43import android.view.MenuItem;
44import android.view.View;
45import android.view.ViewGroup;
46import android.view.WindowManager;
47import android.view.inputmethod.InputMethodManager;
48import android.widget.EditText;
49import android.widget.TextView;
50
51import com.android.settings.R;
52import com.android.settings.SettingsActivity;
53import com.android.settings.search.BaseSearchIndexProvider;
54import com.android.settings.search.Index;
55import com.android.settings.search.Indexable;
56import com.android.settings.search.SearchIndexableRaw;
57import com.android.settings.widget.SwitchBar;
58
59import java.util.ArrayList;
60import java.util.List;
61import java.util.Set;
62
63/**
64 * BluetoothSettings is the Settings screen for Bluetooth configuration and
65 * connection management.
66 */
67public 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