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 com.android.settings.R;
20
21import android.bluetooth.BluetoothAdapter;
22import android.bluetooth.BluetoothClass;
23import android.bluetooth.BluetoothDevice;
24import android.content.BroadcastReceiver;
25import android.content.Context;
26import android.content.Intent;
27import android.content.IntentFilter;
28import android.util.Log;
29
30import java.util.ArrayList;
31import java.util.Collection;
32import java.util.HashMap;
33import java.util.Map;
34import java.util.Set;
35
36/**
37 * BluetoothEventManager receives broadcasts and callbacks from the Bluetooth
38 * API and dispatches the event on the UI thread to the right class in the
39 * Settings.
40 */
41final class BluetoothEventManager {
42    private static final String TAG = "BluetoothEventManager";
43
44    private final LocalBluetoothAdapter mLocalAdapter;
45    private final CachedBluetoothDeviceManager mDeviceManager;
46    private LocalBluetoothProfileManager mProfileManager;
47    private final IntentFilter mAdapterIntentFilter, mProfileIntentFilter;
48    private final Map<String, Handler> mHandlerMap;
49    private Context mContext;
50
51    private final Collection<BluetoothCallback> mCallbacks =
52            new ArrayList<BluetoothCallback>();
53
54    interface Handler {
55        void onReceive(Context context, Intent intent, BluetoothDevice device);
56    }
57
58    void addHandler(String action, Handler handler) {
59        mHandlerMap.put(action, handler);
60        mAdapterIntentFilter.addAction(action);
61    }
62
63    void addProfileHandler(String action, Handler handler) {
64        mHandlerMap.put(action, handler);
65        mProfileIntentFilter.addAction(action);
66    }
67
68    // Set profile manager after construction due to circular dependency
69    void setProfileManager(LocalBluetoothProfileManager manager) {
70        mProfileManager = manager;
71    }
72
73    BluetoothEventManager(LocalBluetoothAdapter adapter,
74            CachedBluetoothDeviceManager deviceManager, Context context) {
75        mLocalAdapter = adapter;
76        mDeviceManager = deviceManager;
77        mAdapterIntentFilter = new IntentFilter();
78        mProfileIntentFilter = new IntentFilter();
79        mHandlerMap = new HashMap<String, Handler>();
80        mContext = context;
81
82        // Bluetooth on/off broadcasts
83        addHandler(BluetoothAdapter.ACTION_STATE_CHANGED, new AdapterStateChangedHandler());
84
85        // Discovery broadcasts
86        addHandler(BluetoothAdapter.ACTION_DISCOVERY_STARTED, new ScanningStateChangedHandler(true));
87        addHandler(BluetoothAdapter.ACTION_DISCOVERY_FINISHED, new ScanningStateChangedHandler(false));
88        addHandler(BluetoothDevice.ACTION_FOUND, new DeviceFoundHandler());
89        addHandler(BluetoothDevice.ACTION_DISAPPEARED, new DeviceDisappearedHandler());
90        addHandler(BluetoothDevice.ACTION_NAME_CHANGED, new NameChangedHandler());
91
92        // Pairing broadcasts
93        addHandler(BluetoothDevice.ACTION_BOND_STATE_CHANGED, new BondStateChangedHandler());
94        addHandler(BluetoothDevice.ACTION_PAIRING_CANCEL, new PairingCancelHandler());
95
96        // Fine-grained state broadcasts
97        addHandler(BluetoothDevice.ACTION_CLASS_CHANGED, new ClassChangedHandler());
98        addHandler(BluetoothDevice.ACTION_UUID, new UuidChangedHandler());
99
100        // Dock event broadcasts
101        addHandler(Intent.ACTION_DOCK_EVENT, new DockEventHandler());
102        mContext.registerReceiver(mBroadcastReceiver, mAdapterIntentFilter);
103    }
104
105    void registerProfileIntentReceiver() {
106        mContext.registerReceiver(mBroadcastReceiver, mProfileIntentFilter);
107    }
108
109    /** Register to start receiving callbacks for Bluetooth events. */
110    void registerCallback(BluetoothCallback callback) {
111        synchronized (mCallbacks) {
112            mCallbacks.add(callback);
113        }
114    }
115
116    /** Unregister to stop receiving callbacks for Bluetooth events. */
117    void unregisterCallback(BluetoothCallback callback) {
118        synchronized (mCallbacks) {
119            mCallbacks.remove(callback);
120        }
121    }
122
123    // This can't be called from a broadcast receiver where the filter is set in the Manifest.
124    private static String getDockedDeviceAddress(Context context) {
125        // This works only because these broadcast intents are "sticky"
126        Intent i = context.registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT));
127        if (i != null) {
128            int state = i.getIntExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED);
129            if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) {
130                BluetoothDevice device = i.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
131                if (device != null) {
132                    return device.getAddress();
133                }
134            }
135        }
136        return null;
137    }
138
139    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
140        @Override
141        public void onReceive(Context context, Intent intent) {
142            Log.v(TAG, "Received " + intent.getAction());
143
144            String action = intent.getAction();
145            BluetoothDevice device = intent
146                    .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
147
148            Handler handler = mHandlerMap.get(action);
149            if (handler != null) {
150                handler.onReceive(context, intent, device);
151            }
152        }
153    };
154
155    private class AdapterStateChangedHandler implements Handler {
156        public void onReceive(Context context, Intent intent,
157                BluetoothDevice device) {
158            int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
159                                    BluetoothAdapter.ERROR);
160            // update local profiles and get paired devices
161            mLocalAdapter.setBluetoothStateInt(state);
162            // send callback to update UI and possibly start scanning
163            synchronized (mCallbacks) {
164                for (BluetoothCallback callback : mCallbacks) {
165                    callback.onBluetoothStateChanged(state);
166                }
167            }
168        }
169    }
170
171    private class ScanningStateChangedHandler implements Handler {
172        private final boolean mStarted;
173
174        ScanningStateChangedHandler(boolean started) {
175            mStarted = started;
176        }
177        public void onReceive(Context context, Intent intent,
178                BluetoothDevice device) {
179            synchronized (mCallbacks) {
180                for (BluetoothCallback callback : mCallbacks) {
181                    callback.onScanningStateChanged(mStarted);
182                }
183            }
184            mDeviceManager.onScanningStateChanged(mStarted);
185            LocalBluetoothPreferences.persistDiscoveringTimestamp(context);
186        }
187    }
188
189    private class DeviceFoundHandler implements Handler {
190        public void onReceive(Context context, Intent intent,
191                BluetoothDevice device) {
192            short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);
193            BluetoothClass btClass = intent.getParcelableExtra(BluetoothDevice.EXTRA_CLASS);
194            String name = intent.getStringExtra(BluetoothDevice.EXTRA_NAME);
195            // TODO Pick up UUID. They should be available for 2.1 devices.
196            // Skip for now, there's a bluez problem and we are not getting uuids even for 2.1.
197            CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
198            if (cachedDevice == null) {
199                cachedDevice = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, device);
200                Log.d(TAG, "DeviceFoundHandler created new CachedBluetoothDevice: "
201                        + cachedDevice);
202                // callback to UI to create Preference for new device
203                dispatchDeviceAdded(cachedDevice);
204            }
205            cachedDevice.setRssi(rssi);
206            cachedDevice.setBtClass(btClass);
207            cachedDevice.setName(name);
208            cachedDevice.setVisible(true);
209        }
210    }
211
212    private void dispatchDeviceAdded(CachedBluetoothDevice cachedDevice) {
213        synchronized (mCallbacks) {
214            for (BluetoothCallback callback : mCallbacks) {
215                callback.onDeviceAdded(cachedDevice);
216            }
217        }
218    }
219
220    private class DeviceDisappearedHandler implements Handler {
221        public void onReceive(Context context, Intent intent,
222                BluetoothDevice device) {
223            CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
224            if (cachedDevice == null) {
225                Log.w(TAG, "received ACTION_DISAPPEARED for an unknown device: " + device);
226                return;
227            }
228            if (CachedBluetoothDeviceManager.onDeviceDisappeared(cachedDevice)) {
229                synchronized (mCallbacks) {
230                    for (BluetoothCallback callback : mCallbacks) {
231                        callback.onDeviceDeleted(cachedDevice);
232                    }
233                }
234            }
235        }
236    }
237
238    private class NameChangedHandler implements Handler {
239        public void onReceive(Context context, Intent intent,
240                BluetoothDevice device) {
241            mDeviceManager.onDeviceNameUpdated(device);
242        }
243    }
244
245    private class BondStateChangedHandler implements Handler {
246        public void onReceive(Context context, Intent intent,
247                BluetoothDevice device) {
248            if (device == null) {
249                Log.e(TAG, "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE");
250                return;
251            }
252            int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
253                                               BluetoothDevice.ERROR);
254            CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
255            if (cachedDevice == null) {
256                Log.w(TAG, "CachedBluetoothDevice for device " + device +
257                        " not found, calling readPairedDevices().");
258                if (!readPairedDevices()) {
259                    Log.e(TAG, "Got bonding state changed for " + device +
260                            ", but we have no record of that device.");
261                    return;
262                }
263                cachedDevice = mDeviceManager.findDevice(device);
264                if (cachedDevice == null) {
265                    Log.e(TAG, "Got bonding state changed for " + device +
266                            ", but device not added in cache.");
267                    return;
268                }
269            }
270
271            synchronized (mCallbacks) {
272                for (BluetoothCallback callback : mCallbacks) {
273                    callback.onDeviceBondStateChanged(cachedDevice, bondState);
274                }
275            }
276            cachedDevice.onBondingStateChanged(bondState);
277
278            if (bondState == BluetoothDevice.BOND_NONE) {
279                if (device.isBluetoothDock()) {
280                    // After a dock is unpaired, we will forget the settings
281                    LocalBluetoothPreferences
282                            .removeDockAutoConnectSetting(context, device.getAddress());
283
284                    // if the device is undocked, remove it from the list as well
285                    if (!device.getAddress().equals(getDockedDeviceAddress(context))) {
286                        cachedDevice.setVisible(false);
287                    }
288                }
289                int reason = intent.getIntExtra(BluetoothDevice.EXTRA_REASON,
290                        BluetoothDevice.ERROR);
291
292                showUnbondMessage(context, cachedDevice.getName(), reason);
293            }
294        }
295
296        /**
297         * Called when we have reached the unbonded state.
298         *
299         * @param reason one of the error reasons from
300         *            BluetoothDevice.UNBOND_REASON_*
301         */
302        private void showUnbondMessage(Context context, String name, int reason) {
303            int errorMsg;
304
305            switch(reason) {
306            case BluetoothDevice.UNBOND_REASON_AUTH_FAILED:
307                errorMsg = R.string.bluetooth_pairing_pin_error_message;
308                break;
309            case BluetoothDevice.UNBOND_REASON_AUTH_REJECTED:
310                errorMsg = R.string.bluetooth_pairing_rejected_error_message;
311                break;
312            case BluetoothDevice.UNBOND_REASON_REMOTE_DEVICE_DOWN:
313                errorMsg = R.string.bluetooth_pairing_device_down_error_message;
314                break;
315            case BluetoothDevice.UNBOND_REASON_DISCOVERY_IN_PROGRESS:
316            case BluetoothDevice.UNBOND_REASON_AUTH_TIMEOUT:
317            case BluetoothDevice.UNBOND_REASON_REPEATED_ATTEMPTS:
318            case BluetoothDevice.UNBOND_REASON_REMOTE_AUTH_CANCELED:
319                errorMsg = R.string.bluetooth_pairing_error_message;
320                break;
321            default:
322                Log.w(TAG, "showUnbondMessage: Not displaying any message for reason: " + reason);
323                return;
324            }
325            Utils.showError(context, name, errorMsg);
326        }
327    }
328
329    private class ClassChangedHandler implements Handler {
330        public void onReceive(Context context, Intent intent,
331                BluetoothDevice device) {
332            mDeviceManager.onBtClassChanged(device);
333        }
334    }
335
336    private class UuidChangedHandler implements Handler {
337        public void onReceive(Context context, Intent intent,
338                BluetoothDevice device) {
339            mDeviceManager.onUuidChanged(device);
340        }
341    }
342
343    private class PairingCancelHandler implements Handler {
344        public void onReceive(Context context, Intent intent, BluetoothDevice device) {
345            if (device == null) {
346                Log.e(TAG, "ACTION_PAIRING_CANCEL with no EXTRA_DEVICE");
347                return;
348            }
349            int errorMsg = R.string.bluetooth_pairing_error_message;
350            CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
351            Utils.showError(context, cachedDevice.getName(), errorMsg);
352        }
353    }
354
355    private class DockEventHandler implements Handler {
356        public void onReceive(Context context, Intent intent, BluetoothDevice device) {
357            // Remove if unpair device upon undocking
358            int anythingButUnDocked = Intent.EXTRA_DOCK_STATE_UNDOCKED + 1;
359            int state = intent.getIntExtra(Intent.EXTRA_DOCK_STATE, anythingButUnDocked);
360            if (state == Intent.EXTRA_DOCK_STATE_UNDOCKED) {
361                if (device != null && device.getBondState() == BluetoothDevice.BOND_NONE) {
362                    CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
363                    if (cachedDevice != null) {
364                        cachedDevice.setVisible(false);
365                    }
366                }
367            }
368        }
369    }
370
371    boolean readPairedDevices() {
372        Set<BluetoothDevice> bondedDevices = mLocalAdapter.getBondedDevices();
373        if (bondedDevices == null) {
374            return false;
375        }
376
377        boolean deviceAdded = false;
378        for (BluetoothDevice device : bondedDevices) {
379            CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device);
380            if (cachedDevice == null) {
381                cachedDevice = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, device);
382                dispatchDeviceAdded(cachedDevice);
383                deviceAdded = true;
384            }
385        }
386
387        return deviceAdded;
388    }
389}
390