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