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 android.app.AlertDialog;
20import android.bluetooth.BluetoothDevice;
21import android.bluetooth.BluetoothProfile;
22import android.content.Context;
23import android.content.DialogInterface;
24import android.os.Bundle;
25import android.preference.CheckBoxPreference;
26import android.preference.EditTextPreference;
27import android.preference.Preference;
28import android.preference.PreferenceGroup;
29import android.preference.PreferenceScreen;
30import android.text.Html;
31import android.text.TextUtils;
32import android.util.Log;
33import android.view.View;
34import android.widget.EditText;
35import android.text.TextWatcher;
36import android.app.Dialog;
37import android.widget.Button;
38import android.text.Editable;
39import com.android.settings.R;
40import com.android.settings.SettingsPreferenceFragment;
41
42import java.util.HashMap;
43
44/**
45 * This preference fragment presents the user with all of the profiles
46 * for a particular device, and allows them to be individually connected
47 * (or disconnected).
48 */
49public final class DeviceProfilesSettings extends SettingsPreferenceFragment
50        implements CachedBluetoothDevice.Callback, Preference.OnPreferenceChangeListener {
51    private static final String TAG = "DeviceProfilesSettings";
52
53    private static final String KEY_RENAME_DEVICE = "rename_device";
54    private static final String KEY_PROFILE_CONTAINER = "profile_container";
55    private static final String KEY_UNPAIR = "unpair";
56
57    public static final String EXTRA_DEVICE = "device";
58    private RenameEditTextPreference mRenameDeviceNamePref;
59    private LocalBluetoothManager mManager;
60    private CachedBluetoothDevice mCachedDevice;
61    private LocalBluetoothProfileManager mProfileManager;
62
63    private PreferenceGroup mProfileContainer;
64    private EditTextPreference mDeviceNamePref;
65
66    private final HashMap<LocalBluetoothProfile, CheckBoxPreference> mAutoConnectPrefs
67            = new HashMap<LocalBluetoothProfile, CheckBoxPreference>();
68
69    private AlertDialog mDisconnectDialog;
70    private boolean mProfileGroupIsRemoved;
71
72    private class RenameEditTextPreference implements TextWatcher{
73        public void afterTextChanged(Editable s) {
74            Dialog d = mDeviceNamePref.getDialog();
75            if (d instanceof AlertDialog) {
76                ((AlertDialog) d).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(s.length() > 0);
77            }
78        }
79
80        // TextWatcher interface
81        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
82            // not used
83        }
84
85        // TextWatcher interface
86        public void onTextChanged(CharSequence s, int start, int before, int count) {
87            // not used
88        }
89    }
90
91    @Override
92    public void onCreate(Bundle savedInstanceState) {
93        super.onCreate(savedInstanceState);
94
95        BluetoothDevice device;
96        if (savedInstanceState != null) {
97            device = savedInstanceState.getParcelable(EXTRA_DEVICE);
98        } else {
99            Bundle args = getArguments();
100            device = args.getParcelable(EXTRA_DEVICE);
101        }
102
103        addPreferencesFromResource(R.xml.bluetooth_device_advanced);
104        getPreferenceScreen().setOrderingAsAdded(false);
105        mProfileContainer = (PreferenceGroup) findPreference(KEY_PROFILE_CONTAINER);
106        mDeviceNamePref = (EditTextPreference) findPreference(KEY_RENAME_DEVICE);
107
108        if (device == null) {
109            Log.w(TAG, "Activity started without a remote Bluetooth device");
110            finish();
111            return;  // TODO: test this failure path
112        }
113        mRenameDeviceNamePref = new RenameEditTextPreference();
114        mManager = LocalBluetoothManager.getInstance(getActivity());
115        CachedBluetoothDeviceManager deviceManager =
116                mManager.getCachedDeviceManager();
117        mProfileManager = mManager.getProfileManager();
118        mCachedDevice = deviceManager.findDevice(device);
119        if (mCachedDevice == null) {
120            Log.w(TAG, "Device not found, cannot connect to it");
121            finish();
122            return;  // TODO: test this failure path
123        }
124
125        String deviceName = mCachedDevice.getName();
126        mDeviceNamePref.setSummary(deviceName);
127        mDeviceNamePref.setText(deviceName);
128        mDeviceNamePref.setOnPreferenceChangeListener(this);
129
130        // Add a preference for each profile
131        addPreferencesForProfiles();
132    }
133
134    @Override
135    public void onDestroy() {
136        super.onDestroy();
137        if (mDisconnectDialog != null) {
138            mDisconnectDialog.dismiss();
139            mDisconnectDialog = null;
140        }
141    }
142
143    @Override
144    public void onSaveInstanceState(Bundle outState) {
145        super.onSaveInstanceState(outState);
146        outState.putParcelable(EXTRA_DEVICE, mCachedDevice.getDevice());
147    }
148
149    @Override
150    public void onResume() {
151        super.onResume();
152
153        mManager.setForegroundActivity(getActivity());
154        mCachedDevice.registerCallback(this);
155        if(mCachedDevice.getBondState() == BluetoothDevice.BOND_NONE)
156            finish();
157        refresh();
158        EditText et = mDeviceNamePref.getEditText();
159        if (et != null) {
160            et.addTextChangedListener(mRenameDeviceNamePref);
161            Dialog d = mDeviceNamePref.getDialog();
162            if (d instanceof AlertDialog) {
163                Button b = ((AlertDialog) d).getButton(AlertDialog.BUTTON_POSITIVE);
164                b.setEnabled(et.getText().length() > 0);
165            }
166        }
167    }
168
169    @Override
170    public void onPause() {
171        super.onPause();
172
173        mCachedDevice.unregisterCallback(this);
174        mManager.setForegroundActivity(null);
175    }
176
177    private void addPreferencesForProfiles() {
178        for (LocalBluetoothProfile profile : mCachedDevice.getConnectableProfiles()) {
179            Preference pref = createProfilePreference(profile);
180            mProfileContainer.addPreference(pref);
181        }
182        showOrHideProfileGroup();
183    }
184
185    private void showOrHideProfileGroup() {
186        int numProfiles = mProfileContainer.getPreferenceCount();
187        if (!mProfileGroupIsRemoved && numProfiles == 0) {
188            getPreferenceScreen().removePreference(mProfileContainer);
189            mProfileGroupIsRemoved = true;
190        } else if (mProfileGroupIsRemoved && numProfiles != 0) {
191            getPreferenceScreen().addPreference(mProfileContainer);
192            mProfileGroupIsRemoved = false;
193        }
194    }
195
196    /**
197     * Creates a checkbox preference for the particular profile. The key will be
198     * the profile's name.
199     *
200     * @param profile The profile for which the preference controls.
201     * @return A preference that allows the user to choose whether this profile
202     *         will be connected to.
203     */
204    private CheckBoxPreference createProfilePreference(LocalBluetoothProfile profile) {
205        CheckBoxPreference pref = new CheckBoxPreference(getActivity());
206        pref.setKey(profile.toString());
207        pref.setTitle(profile.getNameResource(mCachedDevice.getDevice()));
208        pref.setPersistent(false);
209        pref.setOrder(getProfilePreferenceIndex(profile.getOrdinal()));
210        pref.setOnPreferenceChangeListener(this);
211
212        int iconResource = profile.getDrawableResource(mCachedDevice.getBtClass());
213        if (iconResource != 0) {
214            pref.setIcon(getResources().getDrawable(iconResource));
215        }
216
217        /**
218         * Gray out profile while connecting and disconnecting
219         */
220        pref.setEnabled(!mCachedDevice.isBusy());
221
222        refreshProfilePreference(pref, profile);
223
224        return pref;
225    }
226
227    @Override
228    public boolean onPreferenceTreeClick(PreferenceScreen screen, Preference preference) {
229        String key = preference.getKey();
230        if (key.equals(KEY_UNPAIR)) {
231            unpairDevice();
232            finish();
233            return true;
234        }
235
236        return super.onPreferenceTreeClick(screen, preference);
237    }
238
239    public boolean onPreferenceChange(Preference preference, Object newValue) {
240        if (preference == mDeviceNamePref) {
241            mCachedDevice.setName((String) newValue);
242        } else if (preference instanceof CheckBoxPreference) {
243            LocalBluetoothProfile prof = getProfileOf(preference);
244            onProfileClicked(prof, (CheckBoxPreference) preference);
245            return false;   // checkbox will update from onDeviceAttributesChanged() callback
246        } else {
247            return false;
248        }
249
250        return true;
251    }
252
253    private void onProfileClicked(LocalBluetoothProfile profile, CheckBoxPreference profilePref) {
254        BluetoothDevice device = mCachedDevice.getDevice();
255
256        int status = profile.getConnectionStatus(device);
257        boolean isConnected =
258                status == BluetoothProfile.STATE_CONNECTED;
259
260        if (isConnected) {
261            askDisconnect(getActivity(), profile);
262        } else {
263            if (profile.isPreferred(device)) {
264                // profile is preferred but not connected: disable auto-connect
265                profile.setPreferred(device, false);
266                refreshProfilePreference(profilePref, profile);
267            } else {
268                profile.setPreferred(device, true);
269                mCachedDevice.connectProfile(profile);
270            }
271        }
272    }
273
274    private void askDisconnect(Context context,
275            final LocalBluetoothProfile profile) {
276        // local reference for callback
277        final CachedBluetoothDevice device = mCachedDevice;
278        String name = device.getName();
279        if (TextUtils.isEmpty(name)) {
280            name = context.getString(R.string.bluetooth_device);
281        }
282
283        String profileName = context.getString(profile.getNameResource(device.getDevice()));
284
285        String title = context.getString(R.string.bluetooth_disable_profile_title);
286        String message = context.getString(R.string.bluetooth_disable_profile_message,
287                profileName, name);
288
289        DialogInterface.OnClickListener disconnectListener =
290                new DialogInterface.OnClickListener() {
291            public void onClick(DialogInterface dialog, int which) {
292                device.disconnect(profile);
293                profile.setPreferred(device.getDevice(), false);
294            }
295        };
296
297        mDisconnectDialog = Utils.showDisconnectDialog(context,
298                mDisconnectDialog, disconnectListener, title, Html.fromHtml(message));
299    }
300
301    public void onDeviceAttributesChanged() {
302        refresh();
303    }
304
305    private void refresh() {
306        String deviceName = mCachedDevice.getName();
307        mDeviceNamePref.setSummary(deviceName);
308        mDeviceNamePref.setText(deviceName);
309
310        refreshProfiles();
311    }
312
313    private void refreshProfiles() {
314        for (LocalBluetoothProfile profile : mCachedDevice.getConnectableProfiles()) {
315            CheckBoxPreference profilePref = (CheckBoxPreference)findPreference(profile.toString());
316            if (profilePref == null) {
317                profilePref = createProfilePreference(profile);
318                mProfileContainer.addPreference(profilePref);
319            } else {
320                refreshProfilePreference(profilePref, profile);
321            }
322        }
323        for (LocalBluetoothProfile profile : mCachedDevice.getRemovedProfiles()) {
324            Preference profilePref = findPreference(profile.toString());
325            if (profilePref != null) {
326                Log.d(TAG, "Removing " + profile.toString() + " from profile list");
327                mProfileContainer.removePreference(profilePref);
328            }
329        }
330        showOrHideProfileGroup();
331    }
332
333    private void refreshProfilePreference(CheckBoxPreference profilePref,
334            LocalBluetoothProfile profile) {
335        BluetoothDevice device = mCachedDevice.getDevice();
336
337        /*
338         * Gray out checkbox while connecting and disconnecting
339         */
340        profilePref.setEnabled(!mCachedDevice.isBusy());
341        profilePref.setChecked(profile.isPreferred(device));
342        profilePref.setSummary(profile.getSummaryResourceForDevice(device));
343    }
344
345    private LocalBluetoothProfile getProfileOf(Preference pref) {
346        if (!(pref instanceof CheckBoxPreference)) {
347            return null;
348        }
349        String key = pref.getKey();
350        if (TextUtils.isEmpty(key)) return null;
351
352        try {
353            return mProfileManager.getProfileByName(pref.getKey());
354        } catch (IllegalArgumentException ignored) {
355            return null;
356        }
357    }
358
359    private int getProfilePreferenceIndex(int profIndex) {
360        return mProfileContainer.getOrder() + profIndex * 10;
361    }
362
363    private void unpairDevice() {
364        mCachedDevice.unpair();
365    }
366}
367