1/*
2 * Copyright (C) 2008 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.BluetoothClass;
21import android.bluetooth.BluetoothDevice;
22import android.bluetooth.BluetoothProfile;
23import android.content.Context;
24import android.content.DialogInterface;
25import android.graphics.drawable.Drawable;
26import android.preference.Preference;
27import android.text.Html;
28import android.text.TextUtils;
29import android.util.Log;
30import android.util.TypedValue;
31import android.view.LayoutInflater;
32import android.view.View;
33import android.view.View.OnClickListener;
34import android.view.ViewGroup;
35import android.widget.ImageView;
36
37import com.android.settings.R;
38
39import java.util.List;
40
41/**
42 * BluetoothDevicePreference is the preference type used to display each remote
43 * Bluetooth device in the Bluetooth Settings screen.
44 */
45public final class BluetoothDevicePreference extends Preference implements
46        CachedBluetoothDevice.Callback, OnClickListener {
47    private static final String TAG = "BluetoothDevicePreference";
48
49    private static int sDimAlpha = Integer.MIN_VALUE;
50
51    private final CachedBluetoothDevice mCachedDevice;
52
53    private OnClickListener mOnSettingsClickListener;
54
55    private AlertDialog mDisconnectDialog;
56
57    public BluetoothDevicePreference(Context context, CachedBluetoothDevice cachedDevice) {
58        super(context);
59
60        if (sDimAlpha == Integer.MIN_VALUE) {
61            TypedValue outValue = new TypedValue();
62            context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true);
63            sDimAlpha = (int) (outValue.getFloat() * 255);
64        }
65
66        mCachedDevice = cachedDevice;
67
68        if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) {
69            setWidgetLayoutResource(R.layout.preference_bluetooth);
70        }
71
72        mCachedDevice.registerCallback(this);
73
74        onDeviceAttributesChanged();
75    }
76
77    CachedBluetoothDevice getCachedDevice() {
78        return mCachedDevice;
79    }
80
81    public void setOnSettingsClickListener(OnClickListener listener) {
82        mOnSettingsClickListener = listener;
83    }
84
85    @Override
86    protected void onPrepareForRemoval() {
87        super.onPrepareForRemoval();
88        mCachedDevice.unregisterCallback(this);
89        if (mDisconnectDialog != null) {
90            mDisconnectDialog.dismiss();
91            mDisconnectDialog = null;
92        }
93    }
94
95    public void onDeviceAttributesChanged() {
96        /*
97         * The preference framework takes care of making sure the value has
98         * changed before proceeding. It will also call notifyChanged() if
99         * any preference info has changed from the previous value.
100         */
101        setTitle(mCachedDevice.getName());
102
103        int summaryResId = getConnectionSummary();
104        if (summaryResId != 0) {
105            setSummary(summaryResId);
106        } else {
107            setSummary(null);   // empty summary for unpaired devices
108        }
109
110        int iconResId = getBtClassDrawable();
111        if (iconResId != 0) {
112            setIcon(iconResId);
113        }
114
115        // Used to gray out the item
116        setEnabled(!mCachedDevice.isBusy());
117
118        // This could affect ordering, so notify that
119        notifyHierarchyChanged();
120    }
121
122    @Override
123    protected void onBindView(View view) {
124        // Disable this view if the bluetooth enable/disable preference view is off
125        if (null != findPreferenceInHierarchy("bt_checkbox")) {
126            setDependency("bt_checkbox");
127        }
128
129        if (mCachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) {
130            ImageView deviceDetails = (ImageView) view.findViewById(R.id.deviceDetails);
131            if (deviceDetails != null) {
132                deviceDetails.setOnClickListener(this);
133                deviceDetails.setTag(mCachedDevice);
134                deviceDetails.setAlpha(isEnabled() ? 255 : sDimAlpha);
135            }
136        }
137
138        super.onBindView(view);
139    }
140
141    public void onClick(View v) {
142        // Should never be null by construction
143        if (mOnSettingsClickListener != null) {
144            mOnSettingsClickListener.onClick(v);
145        }
146    }
147
148    @Override
149    public boolean equals(Object o) {
150        if ((o == null) || !(o instanceof BluetoothDevicePreference)) {
151            return false;
152        }
153        return mCachedDevice.equals(
154                ((BluetoothDevicePreference) o).mCachedDevice);
155    }
156
157    @Override
158    public int hashCode() {
159        return mCachedDevice.hashCode();
160    }
161
162    @Override
163    public int compareTo(Preference another) {
164        if (!(another instanceof BluetoothDevicePreference)) {
165            // Rely on default sort
166            return super.compareTo(another);
167        }
168
169        return mCachedDevice
170                .compareTo(((BluetoothDevicePreference) another).mCachedDevice);
171    }
172
173    void onClicked() {
174        int bondState = mCachedDevice.getBondState();
175
176        if (mCachedDevice.isConnected()) {
177            askDisconnect();
178        } else if (bondState == BluetoothDevice.BOND_BONDED) {
179            mCachedDevice.connect(true);
180        } else if (bondState == BluetoothDevice.BOND_NONE) {
181            pair();
182        }
183    }
184
185    // Show disconnect confirmation dialog for a device.
186    private void askDisconnect() {
187        Context context = getContext();
188        String name = mCachedDevice.getName();
189        if (TextUtils.isEmpty(name)) {
190            name = context.getString(R.string.bluetooth_device);
191        }
192        String message = context.getString(R.string.bluetooth_disconnect_all_profiles, name);
193        String title = context.getString(R.string.bluetooth_disconnect_title);
194
195        DialogInterface.OnClickListener disconnectListener = new DialogInterface.OnClickListener() {
196            public void onClick(DialogInterface dialog, int which) {
197                mCachedDevice.disconnect();
198            }
199        };
200
201        mDisconnectDialog = Utils.showDisconnectDialog(context,
202                mDisconnectDialog, disconnectListener, title, Html.fromHtml(message));
203    }
204
205    private void pair() {
206        if (!mCachedDevice.startPairing()) {
207            Utils.showError(getContext(), mCachedDevice.getName(),
208                    R.string.bluetooth_pairing_error_message);
209        }
210    }
211
212    private int getConnectionSummary() {
213        final CachedBluetoothDevice cachedDevice = mCachedDevice;
214
215        boolean profileConnected = false;       // at least one profile is connected
216        boolean a2dpNotConnected = false;       // A2DP is preferred but not connected
217        boolean headsetNotConnected = false;    // Headset is preferred but not connected
218
219        for (LocalBluetoothProfile profile : cachedDevice.getProfiles()) {
220            int connectionStatus = cachedDevice.getProfileConnectionState(profile);
221
222            switch (connectionStatus) {
223                case BluetoothProfile.STATE_CONNECTING:
224                case BluetoothProfile.STATE_DISCONNECTING:
225                    return Utils.getConnectionStateSummary(connectionStatus);
226
227                case BluetoothProfile.STATE_CONNECTED:
228                    profileConnected = true;
229                    break;
230
231                case BluetoothProfile.STATE_DISCONNECTED:
232                    if (profile.isProfileReady() && profile.isPreferred(cachedDevice.getDevice())) {
233                        if (profile instanceof A2dpProfile) {
234                            a2dpNotConnected = true;
235                        } else if (profile instanceof HeadsetProfile) {
236                            headsetNotConnected = true;
237                        }
238                    }
239                    break;
240            }
241        }
242
243        if (profileConnected) {
244            if (a2dpNotConnected && headsetNotConnected) {
245                return R.string.bluetooth_connected_no_headset_no_a2dp;
246            } else if (a2dpNotConnected) {
247                return R.string.bluetooth_connected_no_a2dp;
248            } else if (headsetNotConnected) {
249                return R.string.bluetooth_connected_no_headset;
250            } else {
251                return R.string.bluetooth_connected;
252            }
253        }
254
255        switch (cachedDevice.getBondState()) {
256            case BluetoothDevice.BOND_BONDING:
257                return R.string.bluetooth_pairing;
258
259            case BluetoothDevice.BOND_BONDED:
260            case BluetoothDevice.BOND_NONE:
261            default:
262                return 0;
263        }
264    }
265
266    private int getBtClassDrawable() {
267        BluetoothClass btClass = mCachedDevice.getBtClass();
268        if (btClass != null) {
269            switch (btClass.getMajorDeviceClass()) {
270                case BluetoothClass.Device.Major.COMPUTER:
271                    return R.drawable.ic_bt_laptop;
272
273                case BluetoothClass.Device.Major.PHONE:
274                    return R.drawable.ic_bt_cellphone;
275
276                case BluetoothClass.Device.Major.PERIPHERAL:
277                    return HidProfile.getHidClassDrawable(btClass);
278
279                case BluetoothClass.Device.Major.IMAGING:
280                    return R.drawable.ic_bt_imaging;
281
282                default:
283                    // unrecognized device class; continue
284            }
285        } else {
286            Log.w(TAG, "mBtClass is null");
287        }
288
289        List<LocalBluetoothProfile> profiles = mCachedDevice.getProfiles();
290        for (LocalBluetoothProfile profile : profiles) {
291            int resId = profile.getDrawableResource(btClass);
292            if (resId != 0) {
293                return resId;
294            }
295        }
296        if (btClass != null) {
297            if (btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
298                return R.drawable.ic_bt_headphones_a2dp;
299
300            }
301            if (btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
302                return R.drawable.ic_bt_headset_hfp;
303            }
304        }
305        return 0;
306    }
307}
308