/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.bluetooth; import android.app.AlertDialog; import android.app.Notification; import android.app.Service; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.provider.Settings; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; import android.widget.CheckBox; import android.widget.CompoundButton; import com.android.settings.R; import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.LocalBluetoothAdapter; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfile; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager.ServiceListener; import java.util.Collection; import java.util.List; import java.util.Set; public final class DockService extends Service implements ServiceListener { private static final String TAG = "DockService"; static final boolean DEBUG = false; // Time allowed for the device to be undocked and redocked without severing // the bluetooth connection private static final long UNDOCKED_GRACE_PERIOD = 1000; // Time allowed for the device to be undocked and redocked without turning // off Bluetooth private static final long DISABLE_BT_GRACE_PERIOD = 2000; // Msg for user wanting the UI to setup the dock private static final int MSG_TYPE_SHOW_UI = 111; // Msg for device docked event private static final int MSG_TYPE_DOCKED = 222; // Msg for device undocked event private static final int MSG_TYPE_UNDOCKED_TEMPORARY = 333; // Msg for undocked command to be process after UNDOCKED_GRACE_PERIOD millis // since MSG_TYPE_UNDOCKED_TEMPORARY private static final int MSG_TYPE_UNDOCKED_PERMANENT = 444; // Msg for disabling bt after DISABLE_BT_GRACE_PERIOD millis since // MSG_TYPE_UNDOCKED_PERMANENT private static final int MSG_TYPE_DISABLE_BT = 555; private static final String SHARED_PREFERENCES_NAME = "dock_settings"; private static final String KEY_DISABLE_BT_WHEN_UNDOCKED = "disable_bt_when_undock"; private static final String KEY_DISABLE_BT = "disable_bt"; private static final String KEY_CONNECT_RETRY_COUNT = "connect_retry_count"; /* * If disconnected unexpectedly, reconnect up to 6 times. Each profile counts * as one time so it's only 3 times for both profiles on the car dock. */ private static final int MAX_CONNECT_RETRY = 6; private static final int INVALID_STARTID = -100; // Created in OnCreate() private volatile Looper mServiceLooper; private volatile ServiceHandler mServiceHandler; private Runnable mRunnable; private LocalBluetoothAdapter mLocalAdapter; private CachedBluetoothDeviceManager mDeviceManager; private LocalBluetoothProfileManager mProfileManager; // Normally set after getting a docked event and unset when the connection // is severed. One exception is that mDevice could be null if the service // was started after the docked event. private BluetoothDevice mDevice; // Created and used for the duration of the dialog private AlertDialog mDialog; private LocalBluetoothProfile[] mProfiles; private boolean[] mCheckedItems; private int mStartIdAssociatedWithDialog; // Set while BT is being enabled. private BluetoothDevice mPendingDevice; private int mPendingStartId; private int mPendingTurnOnStartId = INVALID_STARTID; private int mPendingTurnOffStartId = INVALID_STARTID; private CheckBox mAudioMediaCheckbox; @Override public void onCreate() { if (DEBUG) Log.d(TAG, "onCreate"); LocalBluetoothManager manager = Utils.getLocalBtManager(this); if (manager == null) { Log.e(TAG, "Can't get LocalBluetoothManager: exiting"); return; } mLocalAdapter = manager.getBluetoothAdapter(); mDeviceManager = manager.getCachedDeviceManager(); mProfileManager = manager.getProfileManager(); if (mProfileManager == null) { Log.e(TAG, "Can't get LocalBluetoothProfileManager: exiting"); return; } HandlerThread thread = new HandlerThread("DockService"); thread.start(); mServiceLooper = thread.getLooper(); mServiceHandler = new ServiceHandler(mServiceLooper); } @Override public void onDestroy() { if (DEBUG) Log.d(TAG, "onDestroy"); mRunnable = null; if (mDialog != null) { mDialog.dismiss(); mDialog = null; } if (mProfileManager != null) { mProfileManager.removeServiceListener(this); } if (mServiceLooper != null) { mServiceLooper.quit(); } mLocalAdapter = null; mDeviceManager = null; mProfileManager = null; mServiceLooper = null; mServiceHandler = null; } @Override public IBinder onBind(Intent intent) { // not supported return null; } private SharedPreferences getPrefs() { return getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (DEBUG) Log.d(TAG, "onStartCommand startId: " + startId + " flags: " + flags); if (intent == null) { // Nothing to process, stop. if (DEBUG) Log.d(TAG, "START_NOT_STICKY - intent is null."); // NOTE: We MUST not call stopSelf() directly, since we need to // make sure the wake lock acquired by the Receiver is released. DockEventReceiver.finishStartingService(this, startId); return START_NOT_STICKY; } if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) { handleBtStateChange(intent, startId); return START_NOT_STICKY; } /* * This assumes that the intent sender has checked that this is a dock * and that the intent is for a disconnect */ final SharedPreferences prefs = getPrefs(); if (BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { BluetoothDevice disconnectedDevice = intent .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); int retryCount = prefs.getInt(KEY_CONNECT_RETRY_COUNT, 0); if (retryCount < MAX_CONNECT_RETRY) { prefs.edit().putInt(KEY_CONNECT_RETRY_COUNT, retryCount + 1).apply(); handleUnexpectedDisconnect(disconnectedDevice, mProfileManager.getHeadsetProfile(), startId); } return START_NOT_STICKY; } else if (BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { BluetoothDevice disconnectedDevice = intent .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); int retryCount = prefs.getInt(KEY_CONNECT_RETRY_COUNT, 0); if (retryCount < MAX_CONNECT_RETRY) { prefs.edit().putInt(KEY_CONNECT_RETRY_COUNT, retryCount + 1).apply(); handleUnexpectedDisconnect(disconnectedDevice, mProfileManager.getA2dpProfile(), startId); } return START_NOT_STICKY; } Message msg = parseIntent(intent); if (msg == null) { // Bad intent if (DEBUG) Log.d(TAG, "START_NOT_STICKY - Bad intent."); DockEventReceiver.finishStartingService(this, startId); return START_NOT_STICKY; } if (msg.what == MSG_TYPE_DOCKED) { prefs.edit().remove(KEY_CONNECT_RETRY_COUNT).apply(); } msg.arg2 = startId; processMessage(msg); return START_NOT_STICKY; } private final class ServiceHandler extends Handler { private ServiceHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { processMessage(msg); } } // This method gets messages from both onStartCommand and mServiceHandler/mServiceLooper private synchronized void processMessage(Message msg) { int msgType = msg.what; final int state = msg.arg1; final int startId = msg.arg2; BluetoothDevice device = null; if (msg.obj != null) { device = (BluetoothDevice) msg.obj; } if(DEBUG) Log.d(TAG, "processMessage: " + msgType + " state: " + state + " device = " + (device == null ? "null" : device.toString())); boolean deferFinishCall = false; switch (msgType) { case MSG_TYPE_SHOW_UI: if (device != null) { createDialog(device, state, startId); } break; case MSG_TYPE_DOCKED: deferFinishCall = msgTypeDocked(device, state, startId); break; case MSG_TYPE_UNDOCKED_PERMANENT: deferFinishCall = msgTypeUndockedPermanent(device, startId); break; case MSG_TYPE_UNDOCKED_TEMPORARY: msgTypeUndockedTemporary(device, state, startId); break; case MSG_TYPE_DISABLE_BT: deferFinishCall = msgTypeDisableBluetooth(startId); break; } if (mDialog == null && mPendingDevice == null && msgType != MSG_TYPE_UNDOCKED_TEMPORARY && !deferFinishCall) { // NOTE: We MUST not call stopSelf() directly, since we need to // make sure the wake lock acquired by the Receiver is released. DockEventReceiver.finishStartingService(this, startId); } } private boolean msgTypeDisableBluetooth(int startId) { if (DEBUG) { Log.d(TAG, "BT DISABLE"); } final SharedPreferences prefs = getPrefs(); if (mLocalAdapter.disable()) { prefs.edit().remove(KEY_DISABLE_BT_WHEN_UNDOCKED).apply(); return false; } else { // disable() returned an error. Persist a flag to disable BT later prefs.edit().putBoolean(KEY_DISABLE_BT, true).apply(); mPendingTurnOffStartId = startId; if(DEBUG) { Log.d(TAG, "disable failed. try again later " + startId); } return true; } } private void msgTypeUndockedTemporary(BluetoothDevice device, int state, int startId) { // Undocked event received. Queue a delayed msg to sever connection Message newMsg = mServiceHandler.obtainMessage(MSG_TYPE_UNDOCKED_PERMANENT, state, startId, device); mServiceHandler.sendMessageDelayed(newMsg, UNDOCKED_GRACE_PERIOD); } private boolean msgTypeUndockedPermanent(BluetoothDevice device, int startId) { // Grace period passed. Disconnect. handleUndocked(device); if (device != null) { final SharedPreferences prefs = getPrefs(); if (DEBUG) { Log.d(TAG, "DISABLE_BT_WHEN_UNDOCKED = " + prefs.getBoolean(KEY_DISABLE_BT_WHEN_UNDOCKED, false)); } if (prefs.getBoolean(KEY_DISABLE_BT_WHEN_UNDOCKED, false)) { if (hasOtherConnectedDevices(device)) { // Don't disable BT if something is connected prefs.edit().remove(KEY_DISABLE_BT_WHEN_UNDOCKED).apply(); } else { // BT was disabled when we first docked if (DEBUG) { Log.d(TAG, "QUEUED BT DISABLE"); } // Queue a delayed msg to disable BT Message newMsg = mServiceHandler.obtainMessage( MSG_TYPE_DISABLE_BT, 0, startId, null); mServiceHandler.sendMessageDelayed(newMsg, DISABLE_BT_GRACE_PERIOD); return true; } } } return false; } private boolean msgTypeDocked(BluetoothDevice device, final int state, final int startId) { if (DEBUG) { // TODO figure out why hasMsg always returns false if device // is supplied Log.d(TAG, "1 Has undock perm msg = " + mServiceHandler.hasMessages(MSG_TYPE_UNDOCKED_PERMANENT, mDevice)); Log.d(TAG, "2 Has undock perm msg = " + mServiceHandler.hasMessages(MSG_TYPE_UNDOCKED_PERMANENT, device)); } mServiceHandler.removeMessages(MSG_TYPE_UNDOCKED_PERMANENT); mServiceHandler.removeMessages(MSG_TYPE_DISABLE_BT); getPrefs().edit().remove(KEY_DISABLE_BT).apply(); if (device != null) { if (!device.equals(mDevice)) { if (mDevice != null) { // Not expected. Cleanup/undock existing handleUndocked(mDevice); } mDevice = device; // Register first in case LocalBluetoothProfileManager // becomes ready after isManagerReady is called and it // would be too late to register a service listener. mProfileManager.addServiceListener(this); if (mProfileManager.isManagerReady()) { handleDocked(device, state, startId); // Not needed after all mProfileManager.removeServiceListener(this); } else { final BluetoothDevice d = device; mRunnable = new Runnable() { public void run() { handleDocked(d, state, startId); // FIXME: WTF runnable here? } }; return true; } } } else { // display dialog to enable dock for media audio only in the case of low end docks and // if not already selected by user int dockAudioMediaEnabled = Settings.Global.getInt(getContentResolver(), Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, -1); if (dockAudioMediaEnabled == -1 && state == Intent.EXTRA_DOCK_STATE_LE_DESK) { handleDocked(null, state, startId); return true; } } return false; } synchronized boolean hasOtherConnectedDevices(BluetoothDevice dock) { Collection<CachedBluetoothDevice> cachedDevices = mDeviceManager.getCachedDevicesCopy(); Set<BluetoothDevice> btDevices = mLocalAdapter.getBondedDevices(); if (btDevices == null || cachedDevices == null || btDevices.isEmpty()) { return false; } if(DEBUG) { Log.d(TAG, "btDevices = " + btDevices.size()); Log.d(TAG, "cachedDeviceUIs = " + cachedDevices.size()); } for (CachedBluetoothDevice deviceUI : cachedDevices) { BluetoothDevice btDevice = deviceUI.getDevice(); if (!btDevice.equals(dock) && btDevices.contains(btDevice) && deviceUI .isConnected()) { if(DEBUG) Log.d(TAG, "connected deviceUI = " + deviceUI.getName()); return true; } } return false; } private Message parseIntent(Intent intent) { BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); int state = intent.getIntExtra(Intent.EXTRA_DOCK_STATE, -1234); if (DEBUG) { Log.d(TAG, "Action: " + intent.getAction() + " State:" + state + " Device: " + (device == null ? "null" : device.getAliasName())); } int msgType; switch (state) { case Intent.EXTRA_DOCK_STATE_UNDOCKED: msgType = MSG_TYPE_UNDOCKED_TEMPORARY; break; case Intent.EXTRA_DOCK_STATE_DESK: case Intent.EXTRA_DOCK_STATE_HE_DESK: case Intent.EXTRA_DOCK_STATE_CAR: if (device == null) { Log.w(TAG, "device is null"); return null; } /// Fall Through /// case Intent.EXTRA_DOCK_STATE_LE_DESK: if (DockEventReceiver.ACTION_DOCK_SHOW_UI.equals(intent.getAction())) { if (device == null) { Log.w(TAG, "device is null"); return null; } msgType = MSG_TYPE_SHOW_UI; } else { msgType = MSG_TYPE_DOCKED; } break; default: return null; } return mServiceHandler.obtainMessage(msgType, state, 0, device); } private void createDialog(BluetoothDevice device, int state, int startId) { if (mDialog != null) { // Shouldn't normally happen mDialog.dismiss(); mDialog = null; } mDevice = device; switch (state) { case Intent.EXTRA_DOCK_STATE_CAR: case Intent.EXTRA_DOCK_STATE_DESK: case Intent.EXTRA_DOCK_STATE_LE_DESK: case Intent.EXTRA_DOCK_STATE_HE_DESK: break; default: return; } startForeground(0, new Notification()); final AlertDialog.Builder ab = new AlertDialog.Builder(this); View view; LayoutInflater inflater = (LayoutInflater)getSystemService(LAYOUT_INFLATER_SERVICE); mAudioMediaCheckbox = null; if (device != null) { // Device in a new dock. boolean firstTime = !LocalBluetoothPreferences.hasDockAutoConnectSetting(this, device.getAddress()); CharSequence[] items = initBtSettings(device, state, firstTime); ab.setTitle(getString(R.string.bluetooth_dock_settings_title)); // Profiles ab.setMultiChoiceItems(items, mCheckedItems, mMultiClickListener); // Remember this settings view = inflater.inflate(R.layout.remember_dock_setting, null); CheckBox rememberCheckbox = (CheckBox) view.findViewById(R.id.remember); // check "Remember setting" by default if no value was saved boolean checked = firstTime || LocalBluetoothPreferences.getDockAutoConnectSetting(this, device.getAddress()); rememberCheckbox.setChecked(checked); rememberCheckbox.setOnCheckedChangeListener(mCheckedChangeListener); if (DEBUG) { Log.d(TAG, "Auto connect = " + LocalBluetoothPreferences.getDockAutoConnectSetting(this, device.getAddress())); } } else { ab.setTitle(getString(R.string.bluetooth_dock_settings_title)); view = inflater.inflate(R.layout.dock_audio_media_enable_dialog, null); mAudioMediaCheckbox = (CheckBox) view.findViewById(R.id.dock_audio_media_enable_cb); boolean checked = Settings.Global.getInt(getContentResolver(), Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, 0) == 1; mAudioMediaCheckbox.setChecked(checked); mAudioMediaCheckbox.setOnCheckedChangeListener(mCheckedChangeListener); } float pixelScaleFactor = getResources().getDisplayMetrics().density; int viewSpacingLeft = (int) (14 * pixelScaleFactor); int viewSpacingRight = (int) (14 * pixelScaleFactor); ab.setView(view, viewSpacingLeft, 0 /* top */, viewSpacingRight, 0 /* bottom */); // Ok Button ab.setPositiveButton(getString(android.R.string.ok), mClickListener); mStartIdAssociatedWithDialog = startId; mDialog = ab.create(); mDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); mDialog.setOnDismissListener(mDismissListener); mDialog.show(); } // Called when the individual bt profiles are clicked. private final DialogInterface.OnMultiChoiceClickListener mMultiClickListener = new DialogInterface.OnMultiChoiceClickListener() { public void onClick(DialogInterface dialog, int which, boolean isChecked) { if (DEBUG) { Log.d(TAG, "Item " + which + " changed to " + isChecked); } mCheckedItems[which] = isChecked; } }; // Called when the "Remember" Checkbox is clicked private final CompoundButton.OnCheckedChangeListener mCheckedChangeListener = new CompoundButton.OnCheckedChangeListener() { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (DEBUG) { Log.d(TAG, "onCheckedChanged: Remember Settings = " + isChecked); } if (mDevice != null) { LocalBluetoothPreferences.saveDockAutoConnectSetting( DockService.this, mDevice.getAddress(), isChecked); } else { Settings.Global.putInt(getContentResolver(), Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, isChecked ? 1 : 0); } } }; // Called when the dialog is dismissed private final DialogInterface.OnDismissListener mDismissListener = new DialogInterface.OnDismissListener() { public void onDismiss(DialogInterface dialog) { // NOTE: We MUST not call stopSelf() directly, since we need to // make sure the wake lock acquired by the Receiver is released. if (mPendingDevice == null) { DockEventReceiver.finishStartingService( DockService.this, mStartIdAssociatedWithDialog); } stopForeground(true); } }; // Called when clicked on the OK button private final DialogInterface.OnClickListener mClickListener = new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { if (which == DialogInterface.BUTTON_POSITIVE) { if (mDevice != null) { if (!LocalBluetoothPreferences .hasDockAutoConnectSetting( DockService.this, mDevice.getAddress())) { LocalBluetoothPreferences .saveDockAutoConnectSetting( DockService.this, mDevice.getAddress(), true); } applyBtSettings(mDevice, mStartIdAssociatedWithDialog); } else if (mAudioMediaCheckbox != null) { Settings.Global.putInt(getContentResolver(), Settings.Global.DOCK_AUDIO_MEDIA_ENABLED, mAudioMediaCheckbox.isChecked() ? 1 : 0); } } } }; private CharSequence[] initBtSettings(BluetoothDevice device, int state, boolean firstTime) { // TODO Avoid hardcoding dock and profiles. Read from system properties int numOfProfiles; switch (state) { case Intent.EXTRA_DOCK_STATE_DESK: case Intent.EXTRA_DOCK_STATE_LE_DESK: case Intent.EXTRA_DOCK_STATE_HE_DESK: numOfProfiles = 1; break; case Intent.EXTRA_DOCK_STATE_CAR: numOfProfiles = 2; break; default: return null; } mProfiles = new LocalBluetoothProfile[numOfProfiles]; mCheckedItems = new boolean[numOfProfiles]; CharSequence[] items = new CharSequence[numOfProfiles]; // FIXME: convert switch to something else switch (state) { case Intent.EXTRA_DOCK_STATE_CAR: items[0] = getString(R.string.bluetooth_dock_settings_headset); items[1] = getString(R.string.bluetooth_dock_settings_a2dp); mProfiles[0] = mProfileManager.getHeadsetProfile(); mProfiles[1] = mProfileManager.getA2dpProfile(); if (firstTime) { // Enable by default for car dock mCheckedItems[0] = true; mCheckedItems[1] = true; } else { mCheckedItems[0] = mProfiles[0].isPreferred(device); mCheckedItems[1] = mProfiles[1].isPreferred(device); } break; case Intent.EXTRA_DOCK_STATE_DESK: case Intent.EXTRA_DOCK_STATE_LE_DESK: case Intent.EXTRA_DOCK_STATE_HE_DESK: items[0] = getString(R.string.bluetooth_dock_settings_a2dp); mProfiles[0] = mProfileManager.getA2dpProfile(); if (firstTime) { // Disable by default for desk dock mCheckedItems[0] = false; } else { mCheckedItems[0] = mProfiles[0].isPreferred(device); } break; } return items; } // TODO: move to background thread to fix strict mode warnings private void handleBtStateChange(Intent intent, int startId) { int btState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); synchronized (this) { if(DEBUG) Log.d(TAG, "BtState = " + btState + " mPendingDevice = " + mPendingDevice); if (btState == BluetoothAdapter.STATE_ON) { handleBluetoothStateOn(startId); } else if (btState == BluetoothAdapter.STATE_TURNING_OFF) { // Remove the flag to disable BT if someone is turning off bt. // The rational is that: // a) if BT is off at undock time, no work needs to be done // b) if BT is on at undock time, the user wants it on. getPrefs().edit().remove(KEY_DISABLE_BT_WHEN_UNDOCKED).apply(); DockEventReceiver.finishStartingService(this, startId); } else if (btState == BluetoothAdapter.STATE_OFF) { // Bluetooth was turning off as we were trying to turn it on. // Let's try again if(DEBUG) Log.d(TAG, "Bluetooth = OFF mPendingDevice = " + mPendingDevice); if (mPendingTurnOffStartId != INVALID_STARTID) { DockEventReceiver.finishStartingService(this, mPendingTurnOffStartId); getPrefs().edit().remove(KEY_DISABLE_BT).apply(); mPendingTurnOffStartId = INVALID_STARTID; } if (mPendingDevice != null) { mLocalAdapter.enable(); mPendingTurnOnStartId = startId; } else { DockEventReceiver.finishStartingService(this, startId); } } } } private void handleBluetoothStateOn(int startId) { if (mPendingDevice != null) { if (mPendingDevice.equals(mDevice)) { if(DEBUG) { Log.d(TAG, "applying settings"); } applyBtSettings(mPendingDevice, mPendingStartId); } else if(DEBUG) { Log.d(TAG, "mPendingDevice (" + mPendingDevice + ") != mDevice (" + mDevice + ')'); } mPendingDevice = null; DockEventReceiver.finishStartingService(this, mPendingStartId); } else { final SharedPreferences prefs = getPrefs(); if (DEBUG) { Log.d(TAG, "A DISABLE_BT_WHEN_UNDOCKED = " + prefs.getBoolean(KEY_DISABLE_BT_WHEN_UNDOCKED, false)); } // Reconnect if docked and bluetooth was enabled by user. Intent i = registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT)); if (i != null) { int state = i.getIntExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED); if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) { BluetoothDevice device = i .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (device != null) { connectIfEnabled(device); } } else if (prefs.getBoolean(KEY_DISABLE_BT, false) && mLocalAdapter.disable()) { mPendingTurnOffStartId = startId; prefs.edit().remove(KEY_DISABLE_BT).apply(); return; } } } if (mPendingTurnOnStartId != INVALID_STARTID) { DockEventReceiver.finishStartingService(this, mPendingTurnOnStartId); mPendingTurnOnStartId = INVALID_STARTID; } DockEventReceiver.finishStartingService(this, startId); } private synchronized void handleUnexpectedDisconnect(BluetoothDevice disconnectedDevice, LocalBluetoothProfile profile, int startId) { if (DEBUG) { Log.d(TAG, "handling failed connect for " + disconnectedDevice); } // Reconnect if docked. if (disconnectedDevice != null) { // registerReceiver can't be called from a BroadcastReceiver Intent intent = registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT)); if (intent != null) { int state = intent.getIntExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED); if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) { BluetoothDevice dockedDevice = intent .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (dockedDevice != null && dockedDevice.equals(disconnectedDevice)) { CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice( dockedDevice); cachedDevice.connectProfile(profile); } } } } DockEventReceiver.finishStartingService(this, startId); } private synchronized void connectIfEnabled(BluetoothDevice device) { CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice( device); List<LocalBluetoothProfile> profiles = cachedDevice.getConnectableProfiles(); for (LocalBluetoothProfile profile : profiles) { if (profile.getPreferred(device) == BluetoothProfile.PRIORITY_AUTO_CONNECT) { cachedDevice.connect(false); return; } } } private synchronized void applyBtSettings(BluetoothDevice device, int startId) { if (device == null || mProfiles == null || mCheckedItems == null || mLocalAdapter == null) { return; } // Turn on BT if something is enabled for (boolean enable : mCheckedItems) { if (enable) { int btState = mLocalAdapter.getBluetoothState(); if (DEBUG) { Log.d(TAG, "BtState = " + btState); } // May have race condition as the phone comes in and out and in the dock. // Always turn on BT mLocalAdapter.enable(); // if adapter was previously OFF, TURNING_OFF, or TURNING_ON if (btState != BluetoothAdapter.STATE_ON) { if (mPendingDevice != null && mPendingDevice.equals(mDevice)) { return; } mPendingDevice = device; mPendingStartId = startId; if (btState != BluetoothAdapter.STATE_TURNING_ON) { getPrefs().edit().putBoolean( KEY_DISABLE_BT_WHEN_UNDOCKED, true).apply(); } return; } } } mPendingDevice = null; boolean callConnect = false; CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice( device); for (int i = 0; i < mProfiles.length; i++) { LocalBluetoothProfile profile = mProfiles[i]; if (DEBUG) Log.d(TAG, profile.toString() + " = " + mCheckedItems[i]); if (mCheckedItems[i]) { // Checked but not connected callConnect = true; } else if (!mCheckedItems[i]) { // Unchecked, may or may not be connected. int status = profile.getConnectionStatus(cachedDevice.getDevice()); if (status == BluetoothProfile.STATE_CONNECTED) { if (DEBUG) Log.d(TAG, "applyBtSettings - Disconnecting"); cachedDevice.disconnect(mProfiles[i]); } } profile.setPreferred(device, mCheckedItems[i]); if (DEBUG) { if (mCheckedItems[i] != profile.isPreferred(device)) { Log.e(TAG, "Can't save preferred value"); } } } if (callConnect) { if (DEBUG) Log.d(TAG, "applyBtSettings - Connecting"); cachedDevice.connect(false); } } private synchronized void handleDocked(BluetoothDevice device, int state, int startId) { if (device != null && LocalBluetoothPreferences.getDockAutoConnectSetting(this, device.getAddress())) { // Setting == auto connect initBtSettings(device, state, false); applyBtSettings(mDevice, startId); } else { createDialog(device, state, startId); } } private synchronized void handleUndocked(BluetoothDevice device) { mRunnable = null; mProfileManager.removeServiceListener(this); if (mDialog != null) { mDialog.dismiss(); mDialog = null; } mDevice = null; mPendingDevice = null; if (device != null) { CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice(device); cachedDevice.disconnect(); } } private CachedBluetoothDevice getCachedBluetoothDevice(BluetoothDevice device) { CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device); if (cachedDevice == null) { cachedDevice = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, device); } return cachedDevice; } public synchronized void onServiceConnected() { if (mRunnable != null) { mRunnable.run(); mRunnable = null; mProfileManager.removeServiceListener(this); } } public void onServiceDisconnected() { // FIXME: shouldn't I do something on service disconnected too? } public static class DockBluetoothCallback implements BluetoothCallback { private final Context mContext; public DockBluetoothCallback(Context context) { mContext = context; } public void onBluetoothStateChanged(int bluetoothState) { } public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { } public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { } public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { } @Override public void onScanningStateChanged(boolean started) { // TODO: Find a more unified place for a persistent BluetoothCallback to live // as this is not exactly dock related. LocalBluetoothPreferences.persistDiscoveringTimestamp(mContext); } @Override public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { BluetoothDevice device = cachedDevice.getDevice(); if (bondState == BluetoothDevice.BOND_NONE) { if (device.isBluetoothDock()) { // After a dock is unpaired, we will forget the settings LocalBluetoothPreferences .removeDockAutoConnectSetting(mContext, device.getAddress()); // if the device is undocked, remove it from the list as well if (!device.getAddress().equals(getDockedDeviceAddress(mContext))) { cachedDevice.setVisible(false); } } } } // This can't be called from a broadcast receiver where the filter is set in the Manifest. private static String getDockedDeviceAddress(Context context) { // This works only because these broadcast intents are "sticky" Intent i = context.registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT)); if (i != null) { int state = i.getIntExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED); if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) { BluetoothDevice device = i.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (device != null) { return device.getAddress(); } } } return null; } } }