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