Home | History | Annotate | Download | only in bluetooth
      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 
     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 
    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 
    125     public BluetoothAdapter getBluetoothAdapter() {
    126         return mAdapter;
    127     }
    128 
    129     public Context getContext() {
    130         return mContext;
    131     }
    132 
    133     public Activity getForegroundActivity() {
    134         return mForegroundActivity;
    135     }
    136 
    137     public void setForegroundActivity(Activity activity) {
    138         if (mErrorDialog != null) {
    139             mErrorDialog.dismiss();
    140             mErrorDialog = null;
    141         }
    142         mForegroundActivity = activity;
    143     }
    144 
    145     public SharedPreferences getSharedPreferences() {
    146         return mContext.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
    147     }
    148 
    149     public CachedBluetoothDeviceManager getCachedDeviceManager() {
    150         return mCachedDeviceManager;
    151     }
    152 
    153     List<Callback> getCallbacks() {
    154         return mCallbacks;
    155     }
    156 
    157     public void registerCallback(Callback callback) {
    158         synchronized (mCallbacks) {
    159             mCallbacks.add(callback);
    160         }
    161     }
    162 
    163     public void unregisterCallback(Callback callback) {
    164         synchronized (mCallbacks) {
    165             mCallbacks.remove(callback);
    166         }
    167     }
    168 
    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 
    203     public void stopScanning() {
    204         if (mAdapter.isDiscovering()) {
    205             mAdapter.cancelDiscovery();
    206         }
    207     }
    208 
    209     public int getBluetoothState() {
    210 
    211         if (mState == BluetoothAdapter.ERROR) {
    212             syncBluetoothState();
    213         }
    214 
    215         return mState;
    216     }
    217 
    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 
    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 
    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      */
    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 
    270     private void dispatchScanningStateChanged(boolean started) {
    271         synchronized (mCallbacks) {
    272             for (Callback callback : mCallbacks) {
    273                 callback.onScanningStateChanged(started);
    274             }
    275         }
    276     }
    277 
    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 {
    307         void onScanningStateChanged(boolean started);
    308         void onDeviceAdded(CachedBluetoothDevice cachedDevice);
    309         void onDeviceDeleted(CachedBluetoothDevice cachedDevice);
    310     }
    311 
    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 
    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 
    361     public boolean hasDockAutoConnectSetting(String addr) {
    362         return getSharedPreferences().contains(SHARED_PREFERENCES_KEY_DOCK_AUTO_CONNECT + addr);
    363     }
    364 
    365     public boolean getDockAutoConnectSetting(String addr) {
    366         return getSharedPreferences().getBoolean(SHARED_PREFERENCES_KEY_DOCK_AUTO_CONNECT + addr,
    367                 false);
    368     }
    369 
    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 
    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