summaryrefslogtreecommitdiffstats
path: root/src/com/android/settings/bluetooth/LocalBluetoothDevice.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/settings/bluetooth/LocalBluetoothDevice.java')
-rw-r--r--src/com/android/settings/bluetooth/LocalBluetoothDevice.java576
1 files changed, 576 insertions, 0 deletions
diff --git a/src/com/android/settings/bluetooth/LocalBluetoothDevice.java b/src/com/android/settings/bluetooth/LocalBluetoothDevice.java
new file mode 100644
index 0000000..a488540
--- /dev/null
+++ b/src/com/android/settings/bluetooth/LocalBluetoothDevice.java
@@ -0,0 +1,576 @@
+/*
+ * Copyright (C) 2008 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.Profile;
+
+import android.app.AlertDialog;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.IBluetoothDeviceCallback;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * LocalBluetoothDevice represents a remote Bluetooth device. It contains
+ * attributes of the device (such as the address, name, RSSI, etc.) and
+ * functionality that can be performed on the device (connect, pair, disconnect,
+ * etc.).
+ */
+public class LocalBluetoothDevice implements Comparable<LocalBluetoothDevice> {
+ private static final String TAG = "LocalBluetoothDevice";
+
+ private static final int CONTEXT_ITEM_CONNECT = Menu.FIRST + 1;
+ private static final int CONTEXT_ITEM_DISCONNECT = Menu.FIRST + 2;
+ private static final int CONTEXT_ITEM_UNPAIR = Menu.FIRST + 3;
+ private static final int CONTEXT_ITEM_CONNECT_ADVANCED = Menu.FIRST + 4;
+
+ private final String mAddress;
+ private String mName;
+ private short mRssi;
+ private int mBtClass = BluetoothClass.ERROR;
+
+ private List<Profile> mProfiles = new ArrayList<Profile>();
+
+ private boolean mVisible;
+
+ private final LocalBluetoothManager mLocalManager;
+
+ private List<Callback> mCallbacks = new ArrayList<Callback>();
+
+ /**
+ * When we connect to multiple profiles, we only want to display a single
+ * error even if they all fail. This tracks that state.
+ */
+ private boolean mIsConnectingErrorPossible;
+
+ LocalBluetoothDevice(Context context, String address) {
+ mLocalManager = LocalBluetoothManager.getInstance(context);
+ if (mLocalManager == null) {
+ throw new IllegalStateException(
+ "Cannot use LocalBluetoothDevice without Bluetooth hardware");
+ }
+
+ mAddress = address;
+
+ fillData();
+ }
+
+ public void onClicked() {
+ int bondState = getBondState();
+
+ if (isConnected()) {
+ askDisconnect();
+ } else if (bondState == BluetoothDevice.BOND_BONDED) {
+ connect();
+ } else if (bondState == BluetoothDevice.BOND_NOT_BONDED) {
+ pair();
+ }
+ }
+
+ public void disconnect() {
+ for (Profile profile : mProfiles) {
+ disconnect(profile);
+ }
+ }
+
+ public void disconnect(Profile profile) {
+ LocalBluetoothProfileManager profileManager =
+ LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
+ int status = profileManager.getConnectionStatus(mAddress);
+ if (SettingsBtStatus.isConnectionStatusConnected(status)) {
+ profileManager.disconnect(mAddress);
+ }
+ }
+
+ public void askDisconnect() {
+ Context context = mLocalManager.getForegroundActivity();
+ if (context == null) {
+ // Cannot ask, since we need an activity context
+ disconnect();
+ return;
+ }
+
+ Resources res = context.getResources();
+
+ String name = getName();
+ if (TextUtils.isEmpty(name)) {
+ name = res.getString(R.string.bluetooth_device);
+ }
+ String message = res.getString(R.string.bluetooth_disconnect_blank, name);
+
+ DialogInterface.OnClickListener disconnectListener = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ disconnect();
+ }
+ };
+
+ AlertDialog ad = new AlertDialog.Builder(context)
+ .setTitle(getName())
+ .setMessage(message)
+ .setPositiveButton(android.R.string.ok, disconnectListener)
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();
+ }
+
+ public void connect() {
+ if (!ensurePaired()) return;
+
+ // Reset the only-show-one-error-dialog tracking variable
+ mIsConnectingErrorPossible = true;
+
+ Context context = mLocalManager.getContext();
+ boolean hasAtLeastOnePreferredProfile = false;
+ for (Profile profile : mProfiles) {
+ LocalBluetoothProfileManager profileManager =
+ LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
+ if (profileManager.isPreferred(mAddress)) {
+ hasAtLeastOnePreferredProfile = true;
+ connectInt(profile);
+ }
+ }
+
+ if (!hasAtLeastOnePreferredProfile) {
+ connectAndPreferAllProfiles();
+ }
+ }
+
+ private void connectAndPreferAllProfiles() {
+ if (!ensurePaired()) return;
+
+ // Reset the only-show-one-error-dialog tracking variable
+ mIsConnectingErrorPossible = true;
+
+ Context context = mLocalManager.getContext();
+ for (Profile profile : mProfiles) {
+ LocalBluetoothProfileManager profileManager =
+ LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
+ profileManager.setPreferred(mAddress, true);
+ connectInt(profile);
+ }
+ }
+
+ public void connect(Profile profile) {
+ // Reset the only-show-one-error-dialog tracking variable
+ mIsConnectingErrorPossible = true;
+ connectInt(profile);
+ }
+
+ public void connectInt(Profile profile) {
+ if (!ensurePaired()) return;
+
+ LocalBluetoothProfileManager profileManager =
+ LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile);
+ int status = profileManager.getConnectionStatus(mAddress);
+ if (!SettingsBtStatus.isConnectionStatusConnected(status)) {
+ if (profileManager.connect(mAddress) != BluetoothDevice.RESULT_SUCCESS) {
+ Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
+ }
+ }
+ }
+
+ public void showConnectingError() {
+ if (!mIsConnectingErrorPossible) return;
+ mIsConnectingErrorPossible = false;
+
+ mLocalManager.showError(mAddress, R.string.bluetooth_error_title,
+ R.string.bluetooth_connecting_error_message);
+ }
+
+ private boolean ensurePaired() {
+ if (getBondState() == BluetoothDevice.BOND_NOT_BONDED) {
+ pair();
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ public void pair() {
+ BluetoothDevice manager = mLocalManager.getBluetoothManager();
+
+ // Pairing is unreliable while scanning, so cancel discovery
+ if (manager.isDiscovering()) {
+ manager.cancelDiscovery();
+ }
+
+ if (!mLocalManager.getBluetoothManager().createBond(mAddress)) {
+ mLocalManager.showError(mAddress, R.string.bluetooth_error_title,
+ R.string.bluetooth_pairing_error_message);
+ }
+ }
+
+ public void unpair() {
+ BluetoothDevice manager = mLocalManager.getBluetoothManager();
+
+ switch (getBondState()) {
+ case BluetoothDevice.BOND_BONDED:
+ manager.removeBond(mAddress);
+ break;
+
+ case BluetoothDevice.BOND_BONDING:
+ manager.cancelBondProcess(mAddress);
+ break;
+ }
+ }
+
+ private void fillData() {
+ BluetoothDevice manager = mLocalManager.getBluetoothManager();
+
+ fetchName();
+ fetchBtClass();
+
+ mVisible = false;
+
+ dispatchAttributesChanged();
+ }
+
+ public String getAddress() {
+ return mAddress;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public void refreshName() {
+ fetchName();
+ dispatchAttributesChanged();
+ }
+
+ private void fetchName() {
+ mName = mLocalManager.getBluetoothManager().getRemoteName(mAddress);
+
+ if (TextUtils.isEmpty(mName)) {
+ mName = mAddress;
+ }
+ }
+
+ public void refresh() {
+ dispatchAttributesChanged();
+ }
+
+ public boolean isVisible() {
+ return mVisible;
+ }
+
+ void setVisible(boolean visible) {
+ if (mVisible != visible) {
+ mVisible = visible;
+ dispatchAttributesChanged();
+ }
+ }
+
+ public int getBondState() {
+ return mLocalManager.getBluetoothManager().getBondState(mAddress);
+ }
+
+ void setRssi(short rssi) {
+ if (mRssi != rssi) {
+ mRssi = rssi;
+ dispatchAttributesChanged();
+ }
+ }
+
+ /**
+ * Checks whether we are connected to this device (any profile counts).
+ *
+ * @return Whether it is connected.
+ */
+ public boolean isConnected() {
+ for (Profile profile : mProfiles) {
+ int status = LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile)
+ .getConnectionStatus(mAddress);
+ if (SettingsBtStatus.isConnectionStatusConnected(status)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public boolean isBusy() {
+ for (Profile profile : mProfiles) {
+ int status = LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile)
+ .getConnectionStatus(mAddress);
+ if (SettingsBtStatus.isConnectionStatusBusy(status)) {
+ return true;
+ }
+ }
+
+ if (getBondState() == BluetoothDevice.BOND_BONDING) {
+ return true;
+ }
+
+ return false;
+ }
+
+ public int getBtClassDrawable() {
+
+ // First try looking at profiles
+ if (mProfiles.contains(Profile.A2DP)) {
+ return R.drawable.ic_bt_headphones_a2dp;
+ } else if (mProfiles.contains(Profile.HEADSET)) {
+ return R.drawable.ic_bt_headset_hfp;
+ }
+
+ // Fallback on class
+ switch (BluetoothClass.Device.Major.getDeviceMajor(mBtClass)) {
+ case BluetoothClass.Device.Major.COMPUTER:
+ return R.drawable.ic_bt_laptop;
+
+ case BluetoothClass.Device.Major.PHONE:
+ return R.drawable.ic_bt_cellphone;
+
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Fetches a new value for the cached BT class.
+ */
+ private void fetchBtClass() {
+ mBtClass = mLocalManager.getBluetoothManager().getRemoteClass(mAddress);
+ mProfiles.clear();
+ LocalBluetoothProfileManager.fill(mBtClass, mProfiles);
+ }
+
+ /**
+ * Refreshes the UI for the BT class, including fetching the latest value
+ * for the class.
+ */
+ public void refreshBtClass() {
+ fetchBtClass();
+ dispatchAttributesChanged();
+ }
+
+ public int getSummary() {
+ // TODO: clean up
+ int oneOffSummary = getOneOffSummary();
+ if (oneOffSummary != 0) {
+ return oneOffSummary;
+ }
+
+ for (Profile profile : mProfiles) {
+ LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
+ .getProfileManager(mLocalManager, profile);
+ int connectionStatus = profileManager.getConnectionStatus(mAddress);
+
+ if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus) ||
+ connectionStatus == SettingsBtStatus.CONNECTION_STATUS_CONNECTING ||
+ connectionStatus == SettingsBtStatus.CONNECTION_STATUS_DISCONNECTING) {
+ return SettingsBtStatus.getConnectionStatusSummary(connectionStatus);
+ }
+ }
+
+ return SettingsBtStatus.getPairingStatusSummary(getBondState());
+ }
+
+ /**
+ * We have special summaries when particular profiles are connected. This
+ * checks for those states and returns an applicable summary.
+ *
+ * @return A one-off summary that is applicable for the current state, or 0.
+ */
+ private int getOneOffSummary() {
+ boolean isA2dpConnected = false, isHeadsetConnected = false, isConnecting = false;
+
+ if (mProfiles.contains(Profile.A2DP)) {
+ LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
+ .getProfileManager(mLocalManager, Profile.A2DP);
+ isConnecting = profileManager.getConnectionStatus(mAddress) ==
+ SettingsBtStatus.CONNECTION_STATUS_CONNECTING;
+ isA2dpConnected = profileManager.isConnected(mAddress);
+ }
+
+ if (mProfiles.contains(Profile.HEADSET)) {
+ LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager
+ .getProfileManager(mLocalManager, Profile.HEADSET);
+ isConnecting |= profileManager.getConnectionStatus(mAddress) ==
+ SettingsBtStatus.CONNECTION_STATUS_CONNECTING;
+ isHeadsetConnected = profileManager.isConnected(mAddress);
+ }
+
+ if (isConnecting) {
+ // If any of these important profiles is connecting, prefer that
+ return SettingsBtStatus.getConnectionStatusSummary(
+ SettingsBtStatus.CONNECTION_STATUS_CONNECTING);
+ } else if (isA2dpConnected && isHeadsetConnected) {
+ return R.string.bluetooth_summary_connected_to_a2dp_headset;
+ } else if (isA2dpConnected) {
+ return R.string.bluetooth_summary_connected_to_a2dp;
+ } else if (isHeadsetConnected) {
+ return R.string.bluetooth_summary_connected_to_headset;
+ } else {
+ return 0;
+ }
+ }
+
+ public List<Profile> getProfiles() {
+ return new ArrayList<Profile>(mProfiles);
+ }
+
+ public void onCreateContextMenu(ContextMenu menu) {
+ // No context menu if it is busy (none of these items are applicable if busy)
+ if (isBusy()) return;
+
+ int bondState = getBondState();
+ boolean isConnected = isConnected();
+ boolean hasProfiles = mProfiles.size() > 0;
+
+ menu.setHeaderTitle(getName());
+
+ if (isConnected) {
+ menu.add(0, CONTEXT_ITEM_DISCONNECT, 0, R.string.bluetooth_device_context_disconnect);
+ } else if (hasProfiles) {
+ // For connection action, show either "Connect" or "Pair & connect"
+ int connectString = (bondState == BluetoothDevice.BOND_NOT_BONDED)
+ ? R.string.bluetooth_device_context_pair_connect
+ : R.string.bluetooth_device_context_connect;
+ menu.add(0, CONTEXT_ITEM_CONNECT, 0, connectString);
+ }
+
+ if (bondState == BluetoothDevice.BOND_BONDED) {
+ // For unpair action, show either "Unpair" or "Disconnect & unpair"
+ int unpairString = isConnected
+ ? R.string.bluetooth_device_context_disconnect_unpair
+ : R.string.bluetooth_device_context_unpair;
+ menu.add(0, CONTEXT_ITEM_UNPAIR, 0, unpairString);
+
+ // Show the connection options item
+ menu.add(0, CONTEXT_ITEM_CONNECT_ADVANCED, 0,
+ R.string.bluetooth_device_context_connect_advanced);
+ }
+ }
+
+ /**
+ * Called when a context menu item is clicked.
+ *
+ * @param item The item that was clicked.
+ */
+ public void onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case CONTEXT_ITEM_DISCONNECT:
+ disconnect();
+ break;
+
+ case CONTEXT_ITEM_CONNECT:
+ connect();
+ break;
+
+ case CONTEXT_ITEM_UNPAIR:
+ mLocalManager.getBluetoothManager().disconnectRemoteDeviceAcl(mAddress);
+ unpair();
+ break;
+
+ case CONTEXT_ITEM_CONNECT_ADVANCED:
+ Intent intent = new Intent();
+ // Need an activity context to open this in our task
+ Context context = mLocalManager.getForegroundActivity();
+ if (context == null) {
+ // Fallback on application context, and open in a new task
+ context = mLocalManager.getContext();
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+ intent.setClass(context, ConnectSpecificProfilesActivity.class);
+ intent.putExtra(ConnectSpecificProfilesActivity.EXTRA_ADDRESS, mAddress);
+ context.startActivity(intent);
+ break;
+ }
+ }
+
+ public void registerCallback(Callback callback) {
+ synchronized (mCallbacks) {
+ mCallbacks.add(callback);
+ }
+ }
+
+ public void unregisterCallback(Callback callback) {
+ synchronized (mCallbacks) {
+ mCallbacks.remove(callback);
+ }
+ }
+
+ private void dispatchAttributesChanged() {
+ synchronized (mCallbacks) {
+ for (Callback callback : mCallbacks) {
+ callback.onDeviceAttributesChanged(this);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return mAddress;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if ((o == null) || !(o instanceof LocalBluetoothDevice)) {
+ throw new ClassCastException();
+ }
+
+ return mAddress.equals(((LocalBluetoothDevice) o).mAddress);
+ }
+
+ @Override
+ public int hashCode() {
+ return mAddress.hashCode();
+ }
+
+ public int compareTo(LocalBluetoothDevice another) {
+ int comparison;
+
+ // Connected above not connected
+ comparison = (another.isConnected() ? 1 : 0) - (isConnected() ? 1 : 0);
+ if (comparison != 0) return comparison;
+
+ // Paired above not paired
+ comparison = (another.getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0) -
+ (getBondState() == BluetoothDevice.BOND_BONDED ? 1 : 0);
+ if (comparison != 0) return comparison;
+
+ // Visible above not visible
+ comparison = (another.mVisible ? 1 : 0) - (mVisible ? 1 : 0);
+ if (comparison != 0) return comparison;
+
+ // Stronger signal above weaker signal
+ comparison = another.mRssi - mRssi;
+ if (comparison != 0) return comparison;
+
+ // Fallback on name
+ return getName().compareTo(another.getName());
+ }
+
+ public interface Callback {
+ void onDeviceAttributesChanged(LocalBluetoothDevice device);
+ }
+}