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