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