• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.settings.bluetooth;
18 
19 import com.android.settings.R;
20 
21 import android.app.Activity;
22 import android.app.AlertDialog;
23 import android.bluetooth.BluetoothA2dp;
24 import android.bluetooth.BluetoothAdapter;
25 import android.bluetooth.BluetoothDevice;
26 import android.content.Context;
27 import android.content.SharedPreferences;
28 import android.util.Config;
29 import android.util.Log;
30 import android.widget.Toast;
31 
32 import java.util.ArrayList;
33 import java.util.List;
34 import 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  */
41 public 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 
getInstance(Context context)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 
init(Context context)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 
getBluetoothAdapter()121     public BluetoothAdapter getBluetoothAdapter() {
122         return mAdapter;
123     }
124 
getContext()125     public Context getContext() {
126         return mContext;
127     }
128 
getForegroundActivity()129     public Activity getForegroundActivity() {
130         return mForegroundActivity;
131     }
132 
setForegroundActivity(Activity activity)133     public void setForegroundActivity(Activity activity) {
134         if (mErrorDialog != null) {
135             mErrorDialog.dismiss();
136             mErrorDialog = null;
137         }
138         mForegroundActivity = activity;
139     }
140 
getSharedPreferences()141     public SharedPreferences getSharedPreferences() {
142         return mContext.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
143     }
144 
getCachedDeviceManager()145     public CachedBluetoothDeviceManager getCachedDeviceManager() {
146         return mCachedDeviceManager;
147     }
148 
getCallbacks()149     List<Callback> getCallbacks() {
150         return mCallbacks;
151     }
152 
registerCallback(Callback callback)153     public void registerCallback(Callback callback) {
154         synchronized (mCallbacks) {
155             mCallbacks.add(callback);
156         }
157     }
158 
unregisterCallback(Callback callback)159     public void unregisterCallback(Callback callback) {
160         synchronized (mCallbacks) {
161             mCallbacks.remove(callback);
162         }
163     }
164 
startScanning(boolean force)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 
getBluetoothState()199     public int getBluetoothState() {
200 
201         if (mState == BluetoothAdapter.ERROR) {
202             syncBluetoothState();
203         }
204 
205         return mState;
206     }
207 
setBluetoothStateInt(int state)208     void setBluetoothStateInt(int state) {
209         mState = state;
210         if (state == BluetoothAdapter.STATE_ON ||
211             state == BluetoothAdapter.STATE_OFF) {
212             mCachedDeviceManager.onBluetoothStateChanged(state ==
213                     BluetoothAdapter.STATE_ON);
214         }
215     }
216 
syncBluetoothState()217     private void syncBluetoothState() {
218         int bluetoothState;
219 
220         if (mAdapter != null) {
221             bluetoothState = mAdapter.isEnabled()
222                     ? BluetoothAdapter.STATE_ON
223                     : BluetoothAdapter.STATE_OFF;
224         } else {
225             bluetoothState = BluetoothAdapter.ERROR;
226         }
227 
228         setBluetoothStateInt(bluetoothState);
229     }
230 
setBluetoothEnabled(boolean enabled)231     public void setBluetoothEnabled(boolean enabled) {
232         boolean wasSetStateSuccessful = enabled
233                 ? mAdapter.enable()
234                 : mAdapter.disable();
235 
236         if (wasSetStateSuccessful) {
237             setBluetoothStateInt(enabled
238                 ? BluetoothAdapter.STATE_TURNING_ON
239                 : BluetoothAdapter.STATE_TURNING_OFF);
240         } else {
241             if (V) {
242                 Log.v(TAG,
243                         "setBluetoothEnabled call, manager didn't return success for enabled: "
244                                 + enabled);
245             }
246 
247             syncBluetoothState();
248         }
249     }
250 
251     /**
252      * @param started True if scanning started, false if scanning finished.
253      */
onScanningStateChanged(boolean started)254     void onScanningStateChanged(boolean started) {
255         // TODO: have it be a callback (once we switch bluetooth state changed to callback)
256         mCachedDeviceManager.onScanningStateChanged(started);
257         dispatchScanningStateChanged(started);
258     }
259 
dispatchScanningStateChanged(boolean started)260     private void dispatchScanningStateChanged(boolean started) {
261         synchronized (mCallbacks) {
262             for (Callback callback : mCallbacks) {
263                 callback.onScanningStateChanged(started);
264             }
265         }
266     }
267 
showError(BluetoothDevice device, int titleResId, int messageResId)268     public void showError(BluetoothDevice device, int titleResId, int messageResId) {
269         CachedBluetoothDevice cachedDevice = mCachedDeviceManager.findDevice(device);
270         String name = null;
271         if (cachedDevice == null) {
272             if (device != null) name = device.getName();
273 
274             if (name == null) {
275                 name = mContext.getString(R.string.bluetooth_remote_device);
276             }
277         } else {
278             name = cachedDevice.getName();
279         }
280         String message = mContext.getString(messageResId, name);
281 
282         if (mForegroundActivity != null) {
283             // Need an activity context to show a dialog
284             mErrorDialog = new AlertDialog.Builder(mForegroundActivity)
285                 .setIcon(android.R.drawable.ic_dialog_alert)
286                 .setTitle(titleResId)
287                 .setMessage(message)
288                 .setPositiveButton(android.R.string.ok, null)
289                 .show();
290         } else {
291             // Fallback on a toast
292             Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
293         }
294     }
295 
296     public interface Callback {
onScanningStateChanged(boolean started)297         void onScanningStateChanged(boolean started);
onDeviceAdded(CachedBluetoothDevice cachedDevice)298         void onDeviceAdded(CachedBluetoothDevice cachedDevice);
onDeviceDeleted(CachedBluetoothDevice cachedDevice)299         void onDeviceDeleted(CachedBluetoothDevice cachedDevice);
300     }
301 
shouldShowDialogInForeground(String deviceAddress)302     public boolean shouldShowDialogInForeground(String deviceAddress) {
303         // If Bluetooth Settings is visible
304         if (mForegroundActivity != null) return true;
305 
306         long currentTimeMillis = System.currentTimeMillis();
307         SharedPreferences sharedPreferences = getSharedPreferences();
308 
309         // If the device was in discoverable mode recently
310         long lastDiscoverableEndTime = sharedPreferences.getLong(
311                 BluetoothDiscoverableEnabler.SHARED_PREFERENCES_KEY_DISCOVERABLE_END_TIMESTAMP, 0);
312         if ((lastDiscoverableEndTime + GRACE_PERIOD_TO_SHOW_DIALOGS_IN_FOREGROUND)
313                 > currentTimeMillis) {
314             return true;
315         }
316 
317         // If the device was picked in the device picker recently
318         if (deviceAddress != null) {
319             String lastSelectedDevice = sharedPreferences.getString(
320                     SHARED_PREFERENCES_KEY_LAST_SELECTED_DEVICE, null);
321 
322             if (deviceAddress.equals(lastSelectedDevice)) {
323                 long lastDeviceSelectedTime = sharedPreferences.getLong(
324                         SHARED_PREFERENCES_KEY_LAST_SELECTED_DEVICE_TIME, 0);
325                 if ((lastDeviceSelectedTime + GRACE_PERIOD_TO_SHOW_DIALOGS_IN_FOREGROUND)
326                         > currentTimeMillis) {
327                     return true;
328                 }
329             }
330         }
331         return false;
332     }
333 
persistSelectedDeviceInPicker(String deviceAddress)334     void persistSelectedDeviceInPicker(String deviceAddress) {
335         SharedPreferences.Editor editor = getSharedPreferences().edit();
336         editor.putString(LocalBluetoothManager.SHARED_PREFERENCES_KEY_LAST_SELECTED_DEVICE,
337                 deviceAddress);
338         editor.putLong(LocalBluetoothManager.SHARED_PREFERENCES_KEY_LAST_SELECTED_DEVICE_TIME,
339                 System.currentTimeMillis());
340         editor.commit();
341     }
342 }
343