Major refactoring of Bluetooth settings classes. - Moved all functionality from LocalBluetoothManager into new LocalBluetoothAdapter and LocalBluetoothPreferences, and into existing classes. - Refactored functionality from BluetoothEventRedirector into new BluetoothEventManager class, deleting the original version. New version uses a HashMap from action Strings to implementers of the BluetoothEventManager.Handler interface. - Created new BluetoothDiscoveryReceiver to update shared preferences timestamp for Bluetooth discovery start/finish. This is the only event handling we need to do when the settings app is not visible, so it has its own receiver entry in AndroidManifest.xml. Edits are written using QueuedWork.singleThreadExecutor(), which BroadcastReceiver knows about and will wait for completion, eliminating the need for PendingResult. - Miscellaneous cleanups to code style and logic for readability. - Pulled some large switch statement code blocks into new methods. - Changed all Bluetooth state references to the new BluetoothProfile constants. - Changed use of deprecated Notification constructor in BluetoothPairingRequest to use Notification.Builder. - Moved Utf8ByteLengthFilter helper function from BluetoothNamePreference into its own class, and moved test cases into the same package. - Moved all LocalBluetoothProfileManager functionality related to specific profiles into new top-level classes (A2dpProfile, etc.), all implementing the LocalBluetoothProfile interface. - Moved all UI-related methods from CachedBluetoothDevice into the class that uses the method, or into the static Utils class for shared methods. Change-Id: I6d49b7f4ae0c7d7dcf62551ee40b51ecb5fe4f47
860 lines
33 KiB
Java
860 lines
33 KiB
Java
/*
|
|
* 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 com.android.settings.R;
|
|
import com.android.settings.bluetooth.LocalBluetoothProfileManager.ServiceListener;
|
|
|
|
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.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.util.Log;
|
|
import android.view.LayoutInflater;
|
|
import android.view.View;
|
|
import android.view.WindowManager;
|
|
import android.widget.CheckBox;
|
|
import android.widget.CompoundButton;
|
|
|
|
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;
|
|
|
|
@Override
|
|
public void onCreate() {
|
|
if (DEBUG) Log.d(TAG, "onCreate");
|
|
|
|
LocalBluetoothManager manager = LocalBluetoothManager.getInstance(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:
|
|
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);
|
|
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 && !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;
|
|
}
|
|
}
|
|
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.getName()));
|
|
}
|
|
|
|
if (device == null) {
|
|
Log.w(TAG, "device is null");
|
|
return null;
|
|
}
|
|
|
|
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_CAR:
|
|
if (DockEventReceiver.ACTION_DOCK_SHOW_UI.equals(intent.getAction())) {
|
|
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:
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
startForeground(0, new Notification());
|
|
|
|
// Device in a new dock.
|
|
boolean firstTime = !LocalBluetoothPreferences.hasDockAutoConnectSetting(this, device.getAddress());
|
|
|
|
CharSequence[] items = initBtSettings(device, state, firstTime);
|
|
|
|
final AlertDialog.Builder ab = new AlertDialog.Builder(this);
|
|
ab.setTitle(getString(R.string.bluetooth_dock_settings_title));
|
|
|
|
// Profiles
|
|
ab.setMultiChoiceItems(items, mCheckedItems, mMultiClickListener);
|
|
|
|
// Remember this settings
|
|
LayoutInflater inflater = (LayoutInflater)
|
|
getSystemService(LAYOUT_INFLATER_SERVICE);
|
|
float pixelScaleFactor = getResources().getDisplayMetrics().density;
|
|
View 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);
|
|
int viewSpacingLeft = (int) (14 * pixelScaleFactor);
|
|
int viewSpacingRight = (int) (14 * pixelScaleFactor);
|
|
ab.setView(view, viewSpacingLeft, 0 /* top */, viewSpacingRight, 0 /* bottom */);
|
|
if (DEBUG) {
|
|
Log.d(TAG, "Auto connect = "
|
|
+ LocalBluetoothPreferences.getDockAutoConnectSetting(this, device.getAddress()));
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
// 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
|
|
&& mDevice != null) {
|
|
if (!LocalBluetoothPreferences
|
|
.hasDockAutoConnectSetting(
|
|
DockService.this,
|
|
mDevice.getAddress())) {
|
|
LocalBluetoothPreferences
|
|
.saveDockAutoConnectSetting(
|
|
DockService.this,
|
|
mDevice.getAddress(), true);
|
|
}
|
|
|
|
applyBtSettings(mDevice, mStartIdAssociatedWithDialog);
|
|
}
|
|
}
|
|
};
|
|
|
|
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:
|
|
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:
|
|
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 (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;
|
|
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?
|
|
}
|
|
}
|