LocalBluetoothManager.java revision 4fcf94c47b5526e1ee64854d7e25bfcac171885b
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 com.android.settings.R;
20
21import android.app.Activity;
22import android.app.AlertDialog;
23import android.bluetooth.BluetoothA2dp;
24import android.bluetooth.BluetoothAdapter;
25import android.bluetooth.BluetoothDevice;
26import android.content.Context;
27import android.content.SharedPreferences;
28import android.util.Config;
29import android.util.Log;
30import android.widget.Toast;
31
32import java.util.ArrayList;
33import java.util.List;
34import java.util.Set;
35
36// TODO: have some notion of shutting down.  Maybe a minute after they leave BT settings?
37/**
38 * LocalBluetoothManager provides a simplified interface on top of a subset of
39 * the Bluetooth API.
40 */
41public class LocalBluetoothManager {
42    private static final String TAG = "LocalBluetoothManager";
43    static final boolean V = Config.LOGV;
44    static final boolean D = Config.LOGD;
45
46    private static final String SHARED_PREFERENCES_NAME = "bluetooth_settings";
47
48    private static LocalBluetoothManager INSTANCE;
49    /** Used when obtaining a reference to the singleton instance. */
50    private static Object INSTANCE_LOCK = new Object();
51    private boolean mInitialized;
52
53    private Context mContext;
54    /** If a BT-related activity is in the foreground, this will be it. */
55    private Activity mForegroundActivity;
56    private AlertDialog mErrorDialog = null;
57
58    private BluetoothAdapter mAdapter;
59
60    private CachedBluetoothDeviceManager mCachedDeviceManager;
61    private BluetoothEventRedirector mEventRedirector;
62    private BluetoothA2dp mBluetoothA2dp;
63
64    private int mState = BluetoothAdapter.ERROR;
65
66    private List<Callback> mCallbacks = new ArrayList<Callback>();
67
68    private static final int SCAN_EXPIRATION_MS = 5 * 60 * 1000; // 5 mins
69
70    // If a device was picked from the device picker or was in discoverable mode
71    // in the last 60 seconds, show the pairing dialogs in foreground instead
72    // of raising notifications
73    private static long GRACE_PERIOD_TO_SHOW_DIALOGS_IN_FOREGROUND = 60 * 1000;
74
75    private static final String SHARED_PREFERENCES_KEY_LAST_SELECTED_DEVICE =
76        "last_selected_device";
77
78    private static final String SHARED_PREFERENCES_KEY_LAST_SELECTED_DEVICE_TIME =
79        "last_selected_device_time";
80
81    private long mLastScan;
82
83    public static LocalBluetoothManager getInstance(Context context) {
84        synchronized (INSTANCE_LOCK) {
85            if (INSTANCE == null) {
86                INSTANCE = new LocalBluetoothManager();
87            }
88
89            if (!INSTANCE.init(context)) {
90                return null;
91            }
92
93            LocalBluetoothProfileManager.init(INSTANCE);
94
95            return INSTANCE;
96        }
97    }
98
99    private boolean init(Context context) {
100        if (mInitialized) return true;
101        mInitialized = true;
102
103        // This will be around as long as this process is
104        mContext = context.getApplicationContext();
105
106        mAdapter = BluetoothAdapter.getDefaultAdapter();
107        if (mAdapter == null) {
108            return false;
109        }
110
111        mCachedDeviceManager = new CachedBluetoothDeviceManager(this);
112
113        mEventRedirector = new BluetoothEventRedirector(this);
114        mEventRedirector.start();
115
116        mBluetoothA2dp = new BluetoothA2dp(context);
117
118        return true;
119    }
120
121    public BluetoothAdapter getBluetoothAdapter() {
122        return mAdapter;
123    }
124
125    public Context getContext() {
126        return mContext;
127    }
128
129    public Activity getForegroundActivity() {
130        return mForegroundActivity;
131    }
132
133    public void setForegroundActivity(Activity activity) {
134        if (mErrorDialog != null) {
135            mErrorDialog.dismiss();
136            mErrorDialog = null;
137        }
138        mForegroundActivity = activity;
139    }
140
141    public SharedPreferences getSharedPreferences() {
142        return mContext.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
143    }
144
145    public CachedBluetoothDeviceManager getCachedDeviceManager() {
146        return mCachedDeviceManager;
147    }
148
149    List<Callback> getCallbacks() {
150        return mCallbacks;
151    }
152
153    public void registerCallback(Callback callback) {
154        synchronized (mCallbacks) {
155            mCallbacks.add(callback);
156        }
157    }
158
159    public void unregisterCallback(Callback callback) {
160        synchronized (mCallbacks) {
161            mCallbacks.remove(callback);
162        }
163    }
164
165    public void startScanning(boolean force) {
166        if (mAdapter.isDiscovering()) {
167            /*
168             * Already discovering, but give the callback that information.
169             * Note: we only call the callbacks, not the same path as if the
170             * scanning state had really changed (in that case the device
171             * manager would clear its list of unpaired scanned devices).
172             */
173            dispatchScanningStateChanged(true);
174        } else {
175            if (!force) {
176                // Don't scan more than frequently than SCAN_EXPIRATION_MS,
177                // unless forced
178                if (mLastScan + SCAN_EXPIRATION_MS > System.currentTimeMillis()) {
179                    return;
180                }
181
182                // If we are playing music, don't scan unless forced.
183                Set<BluetoothDevice> sinks = mBluetoothA2dp.getConnectedSinks();
184                if (sinks != null) {
185                    for (BluetoothDevice sink : sinks) {
186                        if (mBluetoothA2dp.getSinkState(sink) == BluetoothA2dp.STATE_PLAYING) {
187                            return;
188                        }
189                    }
190                }
191            }
192
193            if (mAdapter.startDiscovery()) {
194                mLastScan = System.currentTimeMillis();
195            }
196        }
197    }
198
199    public void stopScanning() {
200        if (mAdapter.isDiscovering()) {
201            mAdapter.cancelDiscovery();
202        }
203    }
204
205    public int getBluetoothState() {
206
207        if (mState == BluetoothAdapter.ERROR) {
208            syncBluetoothState();
209        }
210
211        return mState;
212    }
213
214    void setBluetoothStateInt(int state) {
215        mState = state;
216        if (state == BluetoothAdapter.STATE_ON ||
217            state == BluetoothAdapter.STATE_OFF) {
218            mCachedDeviceManager.onBluetoothStateChanged(state ==
219                    BluetoothAdapter.STATE_ON);
220        }
221    }
222
223    private void syncBluetoothState() {
224        int bluetoothState;
225
226        if (mAdapter != null) {
227            bluetoothState = mAdapter.isEnabled()
228                    ? BluetoothAdapter.STATE_ON
229                    : BluetoothAdapter.STATE_OFF;
230        } else {
231            bluetoothState = BluetoothAdapter.ERROR;
232        }
233
234        setBluetoothStateInt(bluetoothState);
235    }
236
237    public void setBluetoothEnabled(boolean enabled) {
238        boolean wasSetStateSuccessful = enabled
239                ? mAdapter.enable()
240                : mAdapter.disable();
241
242        if (wasSetStateSuccessful) {
243            setBluetoothStateInt(enabled
244                ? BluetoothAdapter.STATE_TURNING_ON
245                : BluetoothAdapter.STATE_TURNING_OFF);
246        } else {
247            if (V) {
248                Log.v(TAG,
249                        "setBluetoothEnabled call, manager didn't return success for enabled: "
250                                + enabled);
251            }
252
253            syncBluetoothState();
254        }
255    }
256
257    /**
258     * @param started True if scanning started, false if scanning finished.
259     */
260    void onScanningStateChanged(boolean started) {
261        // TODO: have it be a callback (once we switch bluetooth state changed to callback)
262        mCachedDeviceManager.onScanningStateChanged(started);
263        dispatchScanningStateChanged(started);
264    }
265
266    private void dispatchScanningStateChanged(boolean started) {
267        synchronized (mCallbacks) {
268            for (Callback callback : mCallbacks) {
269                callback.onScanningStateChanged(started);
270            }
271        }
272    }
273
274    public void showError(BluetoothDevice device, int titleResId, int messageResId) {
275        CachedBluetoothDevice cachedDevice = mCachedDeviceManager.findDevice(device);
276        String name = null;
277        if (cachedDevice == null) {
278            if (device != null) name = device.getName();
279
280            if (name == null) {
281                name = mContext.getString(R.string.bluetooth_remote_device);
282            }
283        } else {
284            name = cachedDevice.getName();
285        }
286        String message = mContext.getString(messageResId, name);
287
288        if (mForegroundActivity != null) {
289            // Need an activity context to show a dialog
290            mErrorDialog = new AlertDialog.Builder(mForegroundActivity)
291                .setIcon(android.R.drawable.ic_dialog_alert)
292                .setTitle(titleResId)
293                .setMessage(message)
294                .setPositiveButton(android.R.string.ok, null)
295                .show();
296        } else {
297            // Fallback on a toast
298            Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
299        }
300    }
301
302    public interface Callback {
303        void onScanningStateChanged(boolean started);
304        void onDeviceAdded(CachedBluetoothDevice cachedDevice);
305        void onDeviceDeleted(CachedBluetoothDevice cachedDevice);
306    }
307
308    public boolean shouldShowDialogInForeground(String deviceAddress) {
309        // If Bluetooth Settings is visible
310        if (mForegroundActivity != null) return true;
311
312        long currentTimeMillis = System.currentTimeMillis();
313        SharedPreferences sharedPreferences = getSharedPreferences();
314
315        // If the device was in discoverable mode recently
316        long lastDiscoverableEndTime = sharedPreferences.getLong(
317                BluetoothDiscoverableEnabler.SHARED_PREFERENCES_KEY_DISCOVERABLE_END_TIMESTAMP, 0);
318        if ((lastDiscoverableEndTime + GRACE_PERIOD_TO_SHOW_DIALOGS_IN_FOREGROUND)
319                > currentTimeMillis) {
320            return true;
321        }
322
323        // If the device was picked in the device picker recently
324        if (deviceAddress != null) {
325            String lastSelectedDevice = sharedPreferences.getString(
326                    SHARED_PREFERENCES_KEY_LAST_SELECTED_DEVICE, null);
327
328            if (deviceAddress.equals(lastSelectedDevice)) {
329                long lastDeviceSelectedTime = sharedPreferences.getLong(
330                        SHARED_PREFERENCES_KEY_LAST_SELECTED_DEVICE_TIME, 0);
331                if ((lastDeviceSelectedTime + GRACE_PERIOD_TO_SHOW_DIALOGS_IN_FOREGROUND)
332                        > currentTimeMillis) {
333                    return true;
334                }
335            }
336        }
337        return false;
338    }
339
340    void persistSelectedDeviceInPicker(String deviceAddress) {
341        SharedPreferences.Editor editor = getSharedPreferences().edit();
342        editor.putString(LocalBluetoothManager.SHARED_PREFERENCES_KEY_LAST_SELECTED_DEVICE,
343                deviceAddress);
344        editor.putLong(LocalBluetoothManager.SHARED_PREFERENCES_KEY_LAST_SELECTED_DEVICE_TIME,
345                System.currentTimeMillis());
346        editor.commit();
347    }
348}
349