diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-12-17 18:06:01 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-12-17 18:06:01 -0800 |
commit | abc48f80d8747b4fc051b7dd364355ee667a9bac (patch) | |
tree | 31ae577fe29d75963b071e738703e4db83ad6580 /src/com/android/settings/bluetooth | |
parent | de2d9f5f109265873196f1615e1f3546b114aaa7 (diff) | |
download | packages_apps_settings-abc48f80d8747b4fc051b7dd364355ee667a9bac.zip packages_apps_settings-abc48f80d8747b4fc051b7dd364355ee667a9bac.tar.gz packages_apps_settings-abc48f80d8747b4fc051b7dd364355ee667a9bac.tar.bz2 |
Code drop from //branches/cupcake/...@124589
Diffstat (limited to 'src/com/android/settings/bluetooth')
14 files changed, 2888 insertions, 0 deletions
diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java new file mode 100644 index 0000000..f0a8189 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java @@ -0,0 +1,122 @@ +/* + * 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 android.content.Context; +import android.preference.Preference; +import android.util.TypedValue; +import android.view.View; +import android.widget.ImageView; + +/** + * BluetoothDevicePreference is the preference type used to display each remote + * Bluetooth device in the Bluetooth Settings screen. + */ +public class BluetoothDevicePreference extends Preference implements LocalBluetoothDevice.Callback { + private static final String TAG = "BluetoothDevicePreference"; + + private static int sDimAlpha = Integer.MIN_VALUE; + + private LocalBluetoothDevice mLocalDevice; + + /** + * Cached local copy of whether the device is busy. This is only updated + * from {@link #onDeviceAttributesChanged(LocalBluetoothDevice)}. + */ + private boolean mIsBusy; + + public BluetoothDevicePreference(Context context, LocalBluetoothDevice localDevice) { + super(context); + + if (sDimAlpha == Integer.MIN_VALUE) { + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true); + sDimAlpha = (int) (outValue.getFloat() * 255); + } + + mLocalDevice = localDevice; + + setLayoutResource(R.layout.preference_bluetooth); + + localDevice.registerCallback(this); + + onDeviceAttributesChanged(localDevice); + } + + public LocalBluetoothDevice getDevice() { + return mLocalDevice; + } + + @Override + protected void onPrepareForRemoval() { + super.onPrepareForRemoval(); + mLocalDevice.unregisterCallback(this); + } + + public void onDeviceAttributesChanged(LocalBluetoothDevice device) { + + /* + * The preference framework takes care of making sure the value has + * changed before proceeding. + */ + + setTitle(mLocalDevice.getName()); + + /* + * TODO: Showed "Paired" even though it was "Connected". This may be + * related to BluetoothHeadset not bound to the actual + * BluetoothHeadsetService when we got here. + */ + setSummary(mLocalDevice.getSummary()); + + // Used to gray out the item + mIsBusy = mLocalDevice.isBusy(); + + // Data has changed + notifyChanged(); + + // This could affect ordering, so notify that also + notifyHierarchyChanged(); + } + + @Override + public boolean isEnabled() { + return super.isEnabled() && !mIsBusy; + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + + ImageView btClass = (ImageView) view.findViewById(R.id.btClass); + btClass.setImageResource(mLocalDevice.getBtClassDrawable()); + btClass.setAlpha(isEnabled() ? 255 : sDimAlpha); + } + + @Override + public int compareTo(Preference another) { + if (!(another instanceof BluetoothDevicePreference)) { + // Put other preference types above us + return 1; + } + + return mLocalDevice.compareTo(((BluetoothDevicePreference) another).mLocalDevice); + } + +} diff --git a/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java b/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java new file mode 100644 index 0000000..f895696 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDiscoverableEnabler.java @@ -0,0 +1,192 @@ +/* + * 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 android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.SystemProperties; +import android.preference.Preference; +import android.preference.CheckBoxPreference; +import android.util.Log; + +/** + * BluetoothDiscoverableEnabler is a helper to manage the "Discoverable" + * checkbox. It sets/unsets discoverability and keeps track of how much time + * until the the discoverability is automatically turned off. + */ +public class BluetoothDiscoverableEnabler implements Preference.OnPreferenceChangeListener { + private static final String TAG = "BluetoothDiscoverableEnabler"; + private static final boolean V = LocalBluetoothManager.V; + + private static final String SYSTEM_PROPERTY_DISCOVERABLE_TIMEOUT = + "debug.bt.discoverable_time"; + private static final int DISCOVERABLE_TIMEOUT = 120; + + private static final String SHARED_PREFERENCES_KEY_DISCOVERABLE_END_TIMESTAMP = + "discoverable_end_timestamp"; + + private final Context mContext; + private final Handler mUiHandler; + private final CheckBoxPreference mCheckBoxPreference; + + private final LocalBluetoothManager mLocalManager; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleModeChanged(intent.getIntExtra(BluetoothIntent.MODE, + BluetoothDevice.MODE_UNKNOWN)); + } + }; + + private final Runnable mUpdateCountdownSummaryRunnable = new Runnable() { + public void run() { + updateCountdownSummary(); + } + }; + + public BluetoothDiscoverableEnabler(Context context, CheckBoxPreference checkBoxPreference) { + mContext = context; + mUiHandler = new Handler(); + mCheckBoxPreference = checkBoxPreference; + + checkBoxPreference.setPersistent(false); + + mLocalManager = LocalBluetoothManager.getInstance(context); + if (mLocalManager == null) { + // Bluetooth not supported + checkBoxPreference.setEnabled(false); + } + } + + public void resume() { + if (mLocalManager == null) { + return; + } + + mContext.registerReceiver(mReceiver, + new IntentFilter(BluetoothIntent.MODE_CHANGED_ACTION)); + mCheckBoxPreference.setOnPreferenceChangeListener(this); + + handleModeChanged(mLocalManager.getBluetoothManager().getMode()); + } + + public void pause() { + if (mLocalManager == null) { + return; + } + + mUiHandler.removeCallbacks(mUpdateCountdownSummaryRunnable); + mCheckBoxPreference.setOnPreferenceChangeListener(null); + mContext.unregisterReceiver(mReceiver); + } + + public boolean onPreferenceChange(Preference preference, Object value) { + if (V) { + Log.v(TAG, "Preference changed to " + value); + } + + // Turn on/off BT discoverability + setEnabled((Boolean) value); + + return true; + } + + private void setEnabled(final boolean enable) { + BluetoothDevice manager = mLocalManager.getBluetoothManager(); + + if (enable) { + + int timeout = getDiscoverableTimeout(); + manager.setDiscoverableTimeout(timeout); + + long endTimestamp = System.currentTimeMillis() + timeout * 1000; + persistDiscoverableEndTimestamp(endTimestamp); + + manager.setMode(BluetoothDevice.MODE_DISCOVERABLE); + handleModeChanged(BluetoothDevice.MODE_DISCOVERABLE); + + } else { + manager.setMode(BluetoothDevice.MODE_CONNECTABLE); + } + } + + private int getDiscoverableTimeout() { + int timeout = SystemProperties.getInt(SYSTEM_PROPERTY_DISCOVERABLE_TIMEOUT, -1); + if (timeout <= 0) { + timeout = DISCOVERABLE_TIMEOUT; + } + + return timeout; + } + + private void persistDiscoverableEndTimestamp(long endTimestamp) { + SharedPreferences.Editor editor = mLocalManager.getSharedPreferences().edit(); + editor.putLong(SHARED_PREFERENCES_KEY_DISCOVERABLE_END_TIMESTAMP, endTimestamp); + editor.commit(); + } + + private void handleModeChanged(int mode) { + if (V) { + Log.v(TAG, "Got mode changed: " + mode); + } + + if (mode == BluetoothDevice.MODE_DISCOVERABLE) { + mCheckBoxPreference.setChecked(true); + updateCountdownSummary(); + + } else { + mCheckBoxPreference.setChecked(false); + } + } + + private void updateCountdownSummary() { + int mode = mLocalManager.getBluetoothManager().getMode(); + if (mode != BluetoothDevice.MODE_DISCOVERABLE) return; + + long currentTimestamp = System.currentTimeMillis(); + long endTimestamp = mLocalManager.getSharedPreferences().getLong( + SHARED_PREFERENCES_KEY_DISCOVERABLE_END_TIMESTAMP, 0); + + if (currentTimestamp > endTimestamp) { + // We're still in discoverable mode, but maybe there isn't a timeout. + mCheckBoxPreference.setSummaryOn(null); + return; + } + + String formattedTimeLeft = String.valueOf((endTimestamp - currentTimestamp) / 1000); + + mCheckBoxPreference.setSummaryOn( + mContext.getResources().getString(R.string.bluetooth_is_discoverable, + formattedTimeLeft)); + + synchronized (this) { + mUiHandler.removeCallbacks(mUpdateCountdownSummaryRunnable); + mUiHandler.postDelayed(mUpdateCountdownSummaryRunnable, 1000); + } + } + + +} diff --git a/src/com/android/settings/bluetooth/BluetoothEnabler.java b/src/com/android/settings/bluetooth/BluetoothEnabler.java new file mode 100644 index 0000000..661700f --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothEnabler.java @@ -0,0 +1,149 @@ +/* + * 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.LocalBluetoothManager.ExtendedBluetoothState; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.preference.Preference; +import android.preference.CheckBoxPreference; +import android.text.TextUtils; +import android.util.Config; + +/** + * BluetoothEnabler is a helper to manage the Bluetooth on/off checkbox + * preference. It is turns on/off Bluetooth and ensures the summary of the + * preference reflects the current state. + */ +public class BluetoothEnabler implements Preference.OnPreferenceChangeListener { + + private static final boolean LOCAL_LOGD = Config.LOGD || false; + private static final String TAG = "BluetoothEnabler"; + + private final Context mContext; + private final CheckBoxPreference mCheckBoxPreference; + private final CharSequence mOriginalSummary; + + private final LocalBluetoothManager mLocalManager; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleStateChanged(mLocalManager.getBluetoothState()); + } + }; + + public BluetoothEnabler(Context context, CheckBoxPreference checkBoxPreference) { + mContext = context; + mCheckBoxPreference = checkBoxPreference; + + mOriginalSummary = checkBoxPreference.getSummary(); + checkBoxPreference.setPersistent(false); + + mLocalManager = LocalBluetoothManager.getInstance(context); + if (mLocalManager == null) { + // Bluetooth not supported + checkBoxPreference.setEnabled(false); + } + } + + public void resume() { + if (mLocalManager == null) { + return; + } + + ExtendedBluetoothState state = mLocalManager.getBluetoothState(); + // This is the widget enabled state, not the preference toggled state + mCheckBoxPreference.setEnabled(state == ExtendedBluetoothState.ENABLED || + state == ExtendedBluetoothState.DISABLED); + // BT state is not a sticky broadcast, so set it manually + handleStateChanged(state); + + mContext.registerReceiver(mReceiver, + new IntentFilter(LocalBluetoothManager.EXTENDED_BLUETOOTH_STATE_CHANGED_ACTION)); + mCheckBoxPreference.setOnPreferenceChangeListener(this); + } + + public void pause() { + if (mLocalManager == null) { + return; + } + + mContext.unregisterReceiver(mReceiver); + mCheckBoxPreference.setOnPreferenceChangeListener(null); + } + + public boolean onPreferenceChange(Preference preference, Object value) { + // Turn on/off BT + setEnabled((Boolean) value); + + // Don't update UI to opposite state until we're sure + return false; + } + + private void setEnabled(final boolean enable) { + // Disable preference + mCheckBoxPreference.setEnabled(false); + + mLocalManager.setBluetoothEnabled(enable); + } + + private void handleStateChanged(ExtendedBluetoothState state) { + + if (state == ExtendedBluetoothState.DISABLED || state == ExtendedBluetoothState.ENABLED) { + mCheckBoxPreference.setChecked(state == ExtendedBluetoothState.ENABLED); + mCheckBoxPreference + .setSummary(state == ExtendedBluetoothState.DISABLED ? mOriginalSummary : null); + + mCheckBoxPreference.setEnabled(isEnabledByDependency()); + + } else if (state == ExtendedBluetoothState.ENABLING || + state == ExtendedBluetoothState.DISABLING) { + mCheckBoxPreference.setSummary(state == ExtendedBluetoothState.ENABLING + ? R.string.wifi_starting + : R.string.wifi_stopping); + + } else if (state == ExtendedBluetoothState.UNKNOWN) { + mCheckBoxPreference.setChecked(false); + mCheckBoxPreference.setSummary(R.string.wifi_error); + mCheckBoxPreference.setEnabled(true); + } + } + + private boolean isEnabledByDependency() { + Preference dep = getDependencyPreference(); + if (dep == null) { + return true; + } + + return !dep.shouldDisableDependents(); + } + + private Preference getDependencyPreference() { + String depKey = mCheckBoxPreference.getDependency(); + if (TextUtils.isEmpty(depKey)) { + return null; + } + + return mCheckBoxPreference.getPreferenceManager().findPreference(depKey); + } + +} diff --git a/src/com/android/settings/bluetooth/BluetoothEventRedirector.java b/src/com/android/settings/bluetooth/BluetoothEventRedirector.java new file mode 100644 index 0000000..bcad206 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothEventRedirector.java @@ -0,0 +1,159 @@ +/* + * 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.bluetooth.LocalBluetoothManager.ExtendedBluetoothState; + +import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadset; +import android.bluetooth.BluetoothIntent; +import android.bluetooth.IBluetoothDeviceCallback; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.util.Log; + +/** + * BluetoothEventRedirector receives broadcasts and callbacks from the Bluetooth + * API and dispatches the event on the UI thread to the right class in the + * Settings. + */ +public class BluetoothEventRedirector { + private static final String TAG = "BluetoothEventRedirector"; + private static final boolean V = LocalBluetoothManager.V; + + private LocalBluetoothManager mManager; + private Handler mUiHandler = new Handler(); + + private IBluetoothDeviceCallback mBtDevCallback = new IBluetoothDeviceCallback.Stub() { + public void onCreateBondingResult(final String address, final int result) { + if (V) { + Log.v(TAG, "onCreateBondingResult(" + address + ", " + result + ")"); + } + + mUiHandler.post(new Runnable() { + public void run() { + boolean wasSuccess = result == BluetoothDevice.RESULT_SUCCESS; + LocalBluetoothDeviceManager deviceManager = mManager.getLocalDeviceManager(); + deviceManager.onBondingStateChanged(address, wasSuccess); + if (!wasSuccess) { + deviceManager.onBondingError(address); + } + } + }); + } + + public void onEnableResult(int result) { } + public void onGetRemoteServiceChannelResult(String address, int channel) { } + }; + + private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (V) { + Log.v(TAG, "Received " + intent.getAction()); + } + + String action = intent.getAction(); + String address = intent.getStringExtra(BluetoothIntent.ADDRESS); + + if (action.equals(BluetoothIntent.ENABLED_ACTION)) { + mManager.setBluetoothStateInt(ExtendedBluetoothState.ENABLED); + } else if (action.equals(BluetoothIntent.DISABLED_ACTION)) { + mManager.setBluetoothStateInt(ExtendedBluetoothState.DISABLED); + + } else if (action.equals(BluetoothIntent.DISCOVERY_STARTED_ACTION)) { + mManager.onScanningStateChanged(true); + } else if (action.equals(BluetoothIntent.DISCOVERY_COMPLETED_ACTION)) { + mManager.onScanningStateChanged(false); + + } else if (action.equals(BluetoothIntent.REMOTE_DEVICE_FOUND_ACTION)) { + short rssi = intent.getShortExtra(BluetoothIntent.RSSI, Short.MIN_VALUE); + mManager.getLocalDeviceManager().onDeviceAppeared(address, rssi); + } else if (action.equals(BluetoothIntent.REMOTE_DEVICE_DISAPPEARED_ACTION)) { + mManager.getLocalDeviceManager().onDeviceDisappeared(address); + } else if (action.equals(BluetoothIntent.REMOTE_NAME_UPDATED_ACTION)) { + mManager.getLocalDeviceManager().onDeviceNameUpdated(address); + + } else if (action.equals(BluetoothIntent.BONDING_CREATED_ACTION)) { + mManager.getLocalDeviceManager().onBondingStateChanged(address, true); + } else if (action.equals(BluetoothIntent.BONDING_REMOVED_ACTION)) { + mManager.getLocalDeviceManager().onBondingStateChanged(address, false); + + } else if (action.equals(BluetoothIntent.HEADSET_STATE_CHANGED_ACTION)) { + mManager.getLocalDeviceManager().onProfileStateChanged(address); + + int newState = intent.getIntExtra(BluetoothIntent.HEADSET_STATE, 0); + int oldState = intent.getIntExtra(BluetoothIntent.HEADSET_PREVIOUS_STATE, 0); + if (newState == BluetoothHeadset.STATE_DISCONNECTED && + oldState == BluetoothHeadset.STATE_CONNECTING) { + mManager.getLocalDeviceManager().onConnectingError(address); + } + + } else if (action.equals(BluetoothA2dp.SINK_STATE_CHANGED_ACTION)) { + mManager.getLocalDeviceManager().onProfileStateChanged(address); + + int newState = intent.getIntExtra(BluetoothA2dp.SINK_STATE, 0); + int oldState = intent.getIntExtra(BluetoothA2dp.SINK_PREVIOUS_STATE, 0); + if (newState == BluetoothA2dp.STATE_DISCONNECTED && + oldState == BluetoothA2dp.STATE_CONNECTING) { + mManager.getLocalDeviceManager().onConnectingError(address); + } + } + } + }; + + public BluetoothEventRedirector(LocalBluetoothManager localBluetoothManager) { + mManager = localBluetoothManager; + } + + public void start() { + IntentFilter filter = new IntentFilter(); + + // Bluetooth on/off broadcasts + filter.addAction(BluetoothIntent.ENABLED_ACTION); + filter.addAction(BluetoothIntent.DISABLED_ACTION); + + // Discovery broadcasts + filter.addAction(BluetoothIntent.DISCOVERY_STARTED_ACTION); + filter.addAction(BluetoothIntent.DISCOVERY_COMPLETED_ACTION); + filter.addAction(BluetoothIntent.REMOTE_DEVICE_DISAPPEARED_ACTION); + filter.addAction(BluetoothIntent.REMOTE_DEVICE_FOUND_ACTION); + filter.addAction(BluetoothIntent.REMOTE_NAME_UPDATED_ACTION); + + // Pairing broadcasts + filter.addAction(BluetoothIntent.BONDING_CREATED_ACTION); + filter.addAction(BluetoothIntent.BONDING_REMOVED_ACTION); + + // Fine-grained state broadcasts + filter.addAction(BluetoothA2dp.SINK_STATE_CHANGED_ACTION); + filter.addAction(BluetoothIntent.HEADSET_STATE_CHANGED_ACTION); + + mManager.getContext().registerReceiver(mBroadcastReceiver, filter); + } + + public void stop() { + mManager.getContext().unregisterReceiver(mBroadcastReceiver); + } + + public IBluetoothDeviceCallback getBluetoothDeviceCallback() { + return mBtDevCallback; + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothNamePreference.java b/src/com/android/settings/bluetooth/BluetoothNamePreference.java new file mode 100644 index 0000000..3065b26 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothNamePreference.java @@ -0,0 +1,79 @@ +/* + * 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 android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.preference.EditTextPreference; +import android.preference.PreferenceManager; +import android.util.AttributeSet; + +/** + * BluetoothNamePreference is the preference type for editing the device's + * Bluetooth name. It asks the user for a name, and persists it via the + * Bluetooth API. + */ +public class BluetoothNamePreference extends EditTextPreference { + private static final String TAG = "BluetoothNamePreference"; + + private LocalBluetoothManager mLocalManager; + + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + setSummaryToName(); + } + }; + + public BluetoothNamePreference(Context context, AttributeSet attrs) { + super(context, attrs); + + mLocalManager = LocalBluetoothManager.getInstance(context); + + setSummaryToName(); + } + + public void resume() { + IntentFilter filter = new IntentFilter(); + filter.addAction(BluetoothIntent.ENABLED_ACTION); + filter.addAction(BluetoothIntent.NAME_CHANGED_ACTION); + getContext().registerReceiver(mReceiver, filter); + } + + public void pause() { + getContext().unregisterReceiver(mReceiver); + } + + private void setSummaryToName() { + BluetoothDevice manager = mLocalManager.getBluetoothManager(); + if (manager.isEnabled()) { + setSummary(manager.getName()); + } + } + + @Override + protected boolean persistString(String value) { + BluetoothDevice manager = mLocalManager.getBluetoothManager(); + manager.setName(value); + return true; + } + +} diff --git a/src/com/android/settings/bluetooth/BluetoothPinDialog.java b/src/com/android/settings/bluetooth/BluetoothPinDialog.java new file mode 100644 index 0000000..291d0c1 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothPinDialog.java @@ -0,0 +1,112 @@ +/* + * 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 android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothIntent; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.text.InputFilter; +import android.text.method.DigitsKeyListener; +import android.util.Log; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; + +import com.android.internal.app.AlertActivity; +import com.android.internal.app.AlertController; +import com.android.settings.R; + +/** + * BluetoothPinDialog asks the user to enter a PIN for pairing with a remote + * Bluetooth device. It is an activity that appears as a dialog. + */ +public class BluetoothPinDialog extends AlertActivity implements DialogInterface.OnClickListener { + private static final String TAG = "BluetoothPinDialog"; + + private LocalBluetoothManager mLocalManager; + private String mAddress; + private EditText mPinView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + if (!intent.getAction().equals(BluetoothIntent.PAIRING_REQUEST_ACTION)) + { + Log.e(TAG, + "Error: this activity may be started only with intent " + + BluetoothIntent.PAIRING_REQUEST_ACTION); + finish(); + } + + mLocalManager = LocalBluetoothManager.getInstance(this); + mAddress = intent.getStringExtra(BluetoothIntent.ADDRESS); + + // Set up the "dialog" + final AlertController.AlertParams p = mAlertParams; + p.mIconId = android.R.drawable.ic_dialog_info; + p.mTitle = getString(R.string.bluetooth_pin_entry); + p.mView = createView(); + p.mPositiveButtonText = getString(android.R.string.ok); + p.mPositiveButtonListener = this; + p.mNegativeButtonText = getString(android.R.string.cancel); + p.mNegativeButtonListener = this; + setupAlert(); + } + + private View createView() { + View view = getLayoutInflater().inflate(R.layout.bluetooth_pin_entry, null); + + String name = mLocalManager.getLocalDeviceManager().getName(mAddress); + TextView messageView = (TextView) view.findViewById(R.id.message); + messageView.setText(getString(R.string.bluetooth_enter_pin_msg, name)); + + mPinView = (EditText) view.findViewById(R.id.text); + + return view; + } + + private void onPair(String pin) { + byte[] pinBytes = BluetoothDevice.convertPinToBytes(pin); + + if (pinBytes == null) { + return; + } + + mLocalManager.getBluetoothManager().setPin(mAddress, pinBytes); + } + + private void onCancel() { + mLocalManager.getBluetoothManager().cancelPin(mAddress); + } + + public void onClick(DialogInterface dialog, int which) { + switch (which) { + case DialogInterface.BUTTON_POSITIVE: + onPair(mPinView.getText().toString()); + break; + + case DialogInterface.BUTTON_NEGATIVE: + onCancel(); + break; + } + } + +} diff --git a/src/com/android/settings/bluetooth/BluetoothPinRequest.java b/src/com/android/settings/bluetooth/BluetoothPinRequest.java new file mode 100644 index 0000000..619052d --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothPinRequest.java @@ -0,0 +1,96 @@ +/* + * 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 android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.text.TextUtils; + +/** + * BluetoothPinRequest is a receiver for any Bluetooth pairing PIN request. It + * checks if the Bluetooth Settings is currently visible and brings up the PIN + * entry dialog. Otherwise it puts a Notification in the status bar, which can + * be clicked to bring up the PIN entry dialog. + */ +public class BluetoothPinRequest extends BroadcastReceiver { + + public static final int NOTIFICATION_ID = android.R.drawable.stat_sys_data_bluetooth; + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(BluetoothIntent.PAIRING_REQUEST_ACTION)) { + + LocalBluetoothManager localManager = LocalBluetoothManager.getInstance(context); + + String address = intent.getStringExtra(BluetoothIntent.ADDRESS); + Intent pinIntent = new Intent(); + pinIntent.setClass(context, BluetoothPinDialog.class); + pinIntent.putExtra(BluetoothIntent.ADDRESS, address); + pinIntent.setAction(BluetoothIntent.PAIRING_REQUEST_ACTION); + pinIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + if (localManager.getForegroundActivity() != null) { + // Since the BT-related activity is in the foreground, just open the dialog + context.startActivity(pinIntent); + + } else { + + // Put up a notification that leads to the dialog + Resources res = context.getResources(); + Notification notification = new Notification( + android.R.drawable.stat_sys_data_bluetooth, + res.getString(R.string.bluetooth_notif_ticker), + System.currentTimeMillis()); + + PendingIntent pending = PendingIntent.getActivity(context, 0, + pinIntent, PendingIntent.FLAG_ONE_SHOT); + + String name = intent.getStringExtra(BluetoothIntent.NAME); + if (TextUtils.isEmpty(name)) { + name = localManager.getLocalDeviceManager().getName(address); + } + + notification.setLatestEventInfo(context, + res.getString(R.string.bluetooth_notif_title), + res.getString(R.string.bluetooth_notif_message) + name, + pending); + notification.flags |= Notification.FLAG_AUTO_CANCEL; + + NotificationManager manager = (NotificationManager) + context.getSystemService(Context.NOTIFICATION_SERVICE); + manager.notify(NOTIFICATION_ID, notification); + } + + } else if (action.equals(BluetoothIntent.PAIRING_CANCEL_ACTION)) { + + // Remove the notification + NotificationManager manager = (NotificationManager) context + .getSystemService(Context.NOTIFICATION_SERVICE); + manager.cancel(NOTIFICATION_ID); + } + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothSettings.java b/src/com/android/settings/bluetooth/BluetoothSettings.java new file mode 100644 index 0000000..316e831 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothSettings.java @@ -0,0 +1,258 @@ +/* + * 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.ProgressCategory; +import com.android.settings.R; +import com.android.settings.bluetooth.LocalBluetoothManager.ExtendedBluetoothState; + +import java.util.List; +import java.util.WeakHashMap; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceScreen; +import android.view.ContextMenu; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ContextMenu.ContextMenuInfo; +import android.widget.AdapterView.AdapterContextMenuInfo; + +/** + * BluetoothSettings is the Settings screen for Bluetooth configuration and + * connection management. + */ +public class BluetoothSettings extends PreferenceActivity + implements LocalBluetoothManager.Callback { + + private static final String TAG = "BluetoothSettings"; + + private static final int MENU_SCAN = Menu.FIRST; + + private static final String KEY_BT_CHECKBOX = "bt_checkbox"; + private static final String KEY_BT_DISCOVERABLE = "bt_discoverable"; + private static final String KEY_BT_DEVICE_LIST = "bt_device_list"; + private static final String KEY_BT_NAME = "bt_name"; + private static final String KEY_BT_SCAN = "bt_scan"; + + private LocalBluetoothManager mLocalManager; + + private BluetoothEnabler mEnabler; + private BluetoothDiscoverableEnabler mDiscoverableEnabler; + + private BluetoothNamePreference mNamePreference; + + private ProgressCategory mDeviceList; + + private WeakHashMap<LocalBluetoothDevice, BluetoothDevicePreference> mDevicePreferenceMap = + new WeakHashMap<LocalBluetoothDevice, BluetoothDevicePreference>(); + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // TODO: put this in callback instead of receiving + onBluetoothStateChanged(mLocalManager.getBluetoothState()); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mLocalManager = LocalBluetoothManager.getInstance(this); + if (mLocalManager == null) finish(); + + addPreferencesFromResource(R.xml.bluetooth_settings); + + mEnabler = new BluetoothEnabler( + this, + (CheckBoxPreference) findPreference(KEY_BT_CHECKBOX)); + + mDiscoverableEnabler = new BluetoothDiscoverableEnabler( + this, + (CheckBoxPreference) findPreference(KEY_BT_DISCOVERABLE)); + + mNamePreference = (BluetoothNamePreference) findPreference(KEY_BT_NAME); + + mDeviceList = (ProgressCategory) findPreference(KEY_BT_DEVICE_LIST); + + registerForContextMenu(getListView()); + } + + @Override + protected void onResume() { + super.onResume(); + + // Repopulate (which isn't too bad since it's cached in the settings + // bluetooth manager + mDevicePreferenceMap.clear(); + mDeviceList.removeAll(); + addDevices(); + + mEnabler.resume(); + mDiscoverableEnabler.resume(); + mNamePreference.resume(); + mLocalManager.registerCallback(this); + + mLocalManager.startScanning(false); + + registerReceiver(mReceiver, + new IntentFilter(LocalBluetoothManager.EXTENDED_BLUETOOTH_STATE_CHANGED_ACTION)); + + mLocalManager.setForegroundActivity(this); + } + + @Override + protected void onPause() { + super.onPause(); + + mLocalManager.setForegroundActivity(null); + + unregisterReceiver(mReceiver); + + mLocalManager.unregisterCallback(this); + mNamePreference.pause(); + mDiscoverableEnabler.pause(); + mEnabler.pause(); + } + + private void addDevices() { + List<LocalBluetoothDevice> devices = mLocalManager.getLocalDeviceManager().getDevicesCopy(); + for (LocalBluetoothDevice device : devices) { + onDeviceAdded(device); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + menu.add(0, MENU_SCAN, 0, R.string.bluetooth_scan_for_devices) + .setIcon(R.drawable.ic_menu_refresh) + .setAlphabeticShortcut('r'); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + menu.findItem(MENU_SCAN).setEnabled(mLocalManager.getBluetoothManager().isEnabled()); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + + case MENU_SCAN: + mLocalManager.startScanning(true); + return true; + + default: + return false; + } + } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, + Preference preference) { + + if (KEY_BT_SCAN.equals(preference.getKey())) { + mLocalManager.startScanning(true); + return true; + } + + if (preference instanceof BluetoothDevicePreference) { + BluetoothDevicePreference btPreference = (BluetoothDevicePreference) preference; + btPreference.getDevice().onClicked(); + return true; + } + + return super.onPreferenceTreeClick(preferenceScreen, preference); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + LocalBluetoothDevice device = getDeviceFromMenuInfo(menuInfo); + if (device == null) return; + + device.onCreateContextMenu(menu); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + LocalBluetoothDevice device = getDeviceFromMenuInfo(item.getMenuInfo()); + if (device == null) return false; + + device.onContextItemSelected(item); + return true; + } + + private LocalBluetoothDevice getDeviceFromMenuInfo(ContextMenuInfo menuInfo) { + if ((menuInfo == null) || !(menuInfo instanceof AdapterContextMenuInfo)) { + return null; + } + + AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) menuInfo; + Preference pref = (Preference) getPreferenceScreen().getRootAdapter().getItem( + adapterMenuInfo.position); + if (pref == null || !(pref instanceof BluetoothDevicePreference)) { + return null; + } + + return ((BluetoothDevicePreference) pref).getDevice(); + } + + public void onDeviceAdded(LocalBluetoothDevice device) { + + if (mDevicePreferenceMap.get(device) != null) { + throw new IllegalStateException("Got onDeviceAdded, but device already exists"); + } + + createDevicePreference(device); + } + + private void createDevicePreference(LocalBluetoothDevice device) { + BluetoothDevicePreference preference = new BluetoothDevicePreference(this, device); + mDeviceList.addPreference(preference); + mDevicePreferenceMap.put(device, preference); + } + + public void onDeviceDeleted(LocalBluetoothDevice device) { + BluetoothDevicePreference preference = mDevicePreferenceMap.remove(device); + if (preference != null) { + mDeviceList.removePreference(preference); + } + } + + public void onScanningStateChanged(boolean started) { + mDeviceList.setProgress(started); + } + + private void onBluetoothStateChanged(ExtendedBluetoothState bluetoothState) { + // When bluetooth is enabled (and we are in the activity, which we are), + // we should start a scan + if (bluetoothState == ExtendedBluetoothState.ENABLED) { + mLocalManager.startScanning(false); + } + } +} diff --git a/src/com/android/settings/bluetooth/ConnectSpecificProfilesActivity.java b/src/com/android/settings/bluetooth/ConnectSpecificProfilesActivity.java new file mode 100644 index 0000000..f29ec79 --- /dev/null +++ b/src/com/android/settings/bluetooth/ConnectSpecificProfilesActivity.java @@ -0,0 +1,297 @@ +/* + * 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.content.Intent; +import android.os.Bundle; +import android.preference.CheckBoxPreference; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceGroup; +import android.preference.PreferenceScreen; +import android.text.TextUtils; +import android.widget.ImageView; +import android.widget.TextView; +import android.util.Log; + +/** + * ConnectSpecificProfilesActivity presents the user with all of the profiles + * for a particular device, and allows him to choose which should be connected + * (or disconnected). + */ +public class ConnectSpecificProfilesActivity extends PreferenceActivity + implements LocalBluetoothDevice.Callback, Preference.OnPreferenceChangeListener { + private static final String TAG = "ConnectSpecificProfilesActivity"; + + private static final String KEY_ONLINE_MODE = "online_mode"; + private static final String KEY_TITLE = "title"; + private static final String KEY_PROFILE_CONTAINER = "profile_container"; + + public static final String EXTRA_ADDRESS = "address"; + + private LocalBluetoothManager mManager; + private LocalBluetoothDevice mDevice; + + private PreferenceGroup mProfileContainer; + private CheckBoxPreference mOnlineModePreference; + + /** + * The current mode of this activity and its checkboxes (either online mode + * or offline mode). In online mode, user interactions with the profile + * checkboxes will also toggle the profile's connectivity. In offline mode, + * they will not, and only the preferred state will be saved for the + * profile. + */ + private boolean mOnlineMode; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String address; + if (savedInstanceState != null) { + address = savedInstanceState.getString(EXTRA_ADDRESS); + } else { + Intent intent = getIntent(); + address = intent.getStringExtra(EXTRA_ADDRESS); + } + + if (TextUtils.isEmpty(address)) { + Log.w(TAG, "Activity started without address"); + finish(); + } + + mManager = LocalBluetoothManager.getInstance(this); + mDevice = mManager.getLocalDeviceManager().findDevice(address); + if (mDevice == null) { + Log.w(TAG, "Device not found, cannot connect to it"); + finish(); + } + + addPreferencesFromResource(R.xml.bluetooth_device_advanced); + mProfileContainer = (PreferenceGroup) findPreference(KEY_PROFILE_CONTAINER); + + // Set the title of the screen + findPreference(KEY_TITLE).setTitle( + getString(R.string.bluetooth_device_advanced_title, mDevice.getName())); + + // Listen for check/uncheck of the online mode checkbox + mOnlineModePreference = (CheckBoxPreference) findPreference(KEY_ONLINE_MODE); + mOnlineModePreference.setOnPreferenceChangeListener(this); + + // Add a preference for each profile + addPreferencesForProfiles(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + outState.putString(EXTRA_ADDRESS, mDevice.getAddress()); + } + + @Override + protected void onResume() { + super.onResume(); + + mManager.setForegroundActivity(this); + mDevice.registerCallback(this); + + refresh(true); + } + + @Override + protected void onPause() { + super.onPause(); + + mDevice.unregisterCallback(this); + mManager.setForegroundActivity(null); + } + + private void addPreferencesForProfiles() { + for (Profile profile : mDevice.getProfiles()) { + Preference pref = createProfilePreference(profile); + mProfileContainer.addPreference(pref); + } + } + + /** + * Creates a checkbox preference for the particular profile. The key will be + * the profile's name. + * + * @param profile The profile for which the preference controls. + * @return A preference that allows the user to choose whether this profile + * will be connected to. + */ + private CheckBoxPreference createProfilePreference(Profile profile) { + CheckBoxPreference pref = new CheckBoxPreference(this); + pref.setKey(profile.toString()); + pref.setTitle(profile.localizedString); + pref.setPersistent(false); + pref.setOnPreferenceChangeListener(this); + + refreshProfilePreference(pref, profile); + + return pref; + } + + public boolean onPreferenceChange(Preference preference, Object newValue) { + String key = preference.getKey(); + if (TextUtils.isEmpty(key) || newValue == null) return true; + + if (key.equals(KEY_ONLINE_MODE)) { + onOnlineModeCheckedStateChanged((Boolean) newValue); + + } else { + Profile profile = getProfileOf(preference); + if (profile == null) return false; + onProfileCheckedStateChanged(profile, (Boolean) newValue); + } + + return true; + } + + private void onOnlineModeCheckedStateChanged(boolean checked) { + switchModes(checked, false); + } + + private void onProfileCheckedStateChanged(Profile profile, boolean checked) { + if (mOnlineMode) { + if (checked) { + mDevice.connect(profile); + } else { + mDevice.disconnect(profile); + } + } + + LocalBluetoothProfileManager.setPreferredProfile(this, mDevice.getAddress(), profile, + checked); + } + + public void onDeviceAttributesChanged(LocalBluetoothDevice device) { + refresh(false); + } + + private void refresh(boolean forceRefresh) { + // The online mode could have changed + updateOnlineMode(forceRefresh); + refreshProfiles(); + refreshOnlineModePreference(); + } + + private void updateOnlineMode(boolean force) { + // Connected or Connecting (and Disconnecting, which is fine) + boolean onlineMode = mDevice.isConnected() || mDevice.isBusy(); + switchModes(onlineMode, force); + } + + /** + * Switches between online/offline mode. + * + * @param onlineMode Whether to be in online mode, or offline mode. + */ + private void switchModes(boolean onlineMode, boolean force) { + if (mOnlineMode != onlineMode || force) { + mOnlineMode = onlineMode; + + if (onlineMode) { + mDevice.connect(); + } else { + mDevice.disconnect(); + } + + refreshOnlineModePreference(); + } + } + + private void refreshOnlineModePreference() { + mOnlineModePreference.setChecked(mOnlineMode); + + /** + * If the device is online, show status. Otherwise, show a summary that + * describes what the checkbox does. + */ + mOnlineModePreference.setSummary(mOnlineMode ? mDevice.getSummary() + : R.string.bluetooth_device_advanced_online_mode_summary); + } + + private void refreshProfiles() { + for (Profile profile : mDevice.getProfiles()) { + CheckBoxPreference profilePref = + (CheckBoxPreference) findPreference(profile.toString()); + if (profilePref == null) continue; + + refreshProfilePreference(profilePref, profile); + } + } + + private void refreshProfilePreference(CheckBoxPreference profilePref, Profile profile) { + String address = mDevice.getAddress(); + LocalBluetoothProfileManager profileManager = LocalBluetoothProfileManager + .getProfileManager(mManager, profile); + + int connectionStatus = profileManager.getConnectionStatus(address); + + profilePref.setSummary(getProfileSummary(profileManager, profile, address, + connectionStatus, mOnlineMode)); + + profilePref.setChecked( + LocalBluetoothProfileManager.isPreferredProfile(this, address, profile)); + } + + private Profile getProfileOf(Preference pref) { + if (!(pref instanceof CheckBoxPreference)) return null; + String key = pref.getKey(); + if (TextUtils.isEmpty(key)) return null; + + try { + return Profile.valueOf(pref.getKey()); + } catch (IllegalArgumentException e) { + return null; + } + } + + private static int getProfileSummary(LocalBluetoothProfileManager profileManager, + Profile profile, String address, int connectionStatus, boolean onlineMode) { + if (!onlineMode || connectionStatus == SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED) { + return getProfileSummaryForSettingPreference(profile); + } else { + return profileManager.getSummary(address); + } + } + + /** + * Gets the summary that describes when checked, it will become a preferred profile. + * + * @param profile The profile to get the summary for. + * @return The summary. + */ + private static final int getProfileSummaryForSettingPreference(Profile profile) { + switch (profile) { + case A2DP: + return R.string.bluetooth_a2dp_profile_summary_use_for; + case HEADSET: + return R.string.bluetooth_headset_profile_summary_use_for; + default: + return 0; + } + } + +} diff --git a/src/com/android/settings/bluetooth/LocalBluetoothDevice.java b/src/com/android/settings/bluetooth/LocalBluetoothDevice.java new file mode 100644 index 0000000..a8f79ff --- /dev/null +++ b/src/com/android/settings/bluetooth/LocalBluetoothDevice.java @@ -0,0 +1,558 @@ +/* + * 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.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 int mPairingStatus; + + 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 pairingStatus = getPairingStatus(); + + if (isConnected()) { + askDisconnect(); + } else if (pairingStatus == SettingsBtStatus.PAIRING_STATUS_PAIRED) { + connect(); + } else if (pairingStatus == SettingsBtStatus.PAIRING_STATUS_UNPAIRED) { + 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; + + Context context = mLocalManager.getContext(); + boolean hasAtLeastOnePreferredProfile = false; + for (Profile profile : mProfiles) { + if (LocalBluetoothProfileManager.isPreferredProfile(context, mAddress, profile)) { + hasAtLeastOnePreferredProfile = true; + connect(profile); + } + } + + if (!hasAtLeastOnePreferredProfile) { + connectAndPreferAllProfiles(); + } + } + + private void connectAndPreferAllProfiles() { + if (!ensurePaired()) return; + + Context context = mLocalManager.getContext(); + for (Profile profile : mProfiles) { + LocalBluetoothProfileManager.setPreferredProfile(context, mAddress, profile, true); + connect(profile); + } + } + + public void connect(Profile profile) { + if (!ensurePaired()) return; + + LocalBluetoothProfileManager profileManager = + LocalBluetoothProfileManager.getProfileManager(mLocalManager, profile); + int status = profileManager.getConnectionStatus(mAddress); + if (!SettingsBtStatus.isConnectionStatusConnected(status)) { + mIsConnectingErrorPossible = true; + if (profileManager.connect(mAddress) != BluetoothDevice.RESULT_SUCCESS) { + showConnectingError(); + } + } + } + + 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 (getPairingStatus() == SettingsBtStatus.PAIRING_STATUS_UNPAIRED) { + pair(); + return false; + } else { + return true; + } + } + + public void pair() { + BluetoothDevice manager = mLocalManager.getBluetoothManager(); + + // Pairing doesn't work if scanning, so cancel + if (manager.isDiscovering()) { + manager.cancelDiscovery(); + } + + if (mLocalManager.createBonding(mAddress)) { + setPairingStatus(SettingsBtStatus.PAIRING_STATUS_PAIRING); + } + } + + public void unpair() { + BluetoothDevice manager = mLocalManager.getBluetoothManager(); + + switch (getPairingStatus()) { + case SettingsBtStatus.PAIRING_STATUS_PAIRED: + manager.removeBonding(mAddress); + break; + + case SettingsBtStatus.PAIRING_STATUS_PAIRING: + manager.cancelBondingProcess(mAddress); + break; + } + } + + private void fillData() { + BluetoothDevice manager = mLocalManager.getBluetoothManager(); + + fetchName(); + mBtClass = manager.getRemoteClass(mAddress); + + LocalBluetoothProfileManager.fill(mBtClass, mProfiles); + + mPairingStatus = manager.hasBonding(mAddress) + ? SettingsBtStatus.PAIRING_STATUS_PAIRED + : SettingsBtStatus.PAIRING_STATUS_UNPAIRED; + + 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 getPairingStatus() { + return mPairingStatus; + } + + void setPairingStatus(int pairingStatus) { + if (mPairingStatus != pairingStatus) { + mPairingStatus = pairingStatus; + dispatchAttributesChanged(); + } + } + + 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 (getPairingStatus() == SettingsBtStatus.PAIRING_STATUS_PAIRING) { + 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; + } + } + + 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); + } + } + + int pairingStatus = getPairingStatus(); + return SettingsBtStatus.getPairingStatusSummary(pairingStatus); + } + + /** + * 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; + + // No context menu if there are no profiles + if (mProfiles.size() == 0) return; + + int pairingStatus = getPairingStatus(); + boolean isConnected = isConnected(); + + menu.setHeaderTitle(getName()); + + if (isConnected) { + menu.add(0, CONTEXT_ITEM_DISCONNECT, 0, R.string.bluetooth_device_context_disconnect); + } else { + // For connection action, show either "Connect" or "Pair & connect" + int connectString = pairingStatus == SettingsBtStatus.PAIRING_STATUS_UNPAIRED + ? R.string.bluetooth_device_context_pair_connect + : R.string.bluetooth_device_context_connect; + menu.add(0, CONTEXT_ITEM_CONNECT, 0, connectString); + } + + if (pairingStatus == SettingsBtStatus.PAIRING_STATUS_PAIRED) { + // 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: + 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.mPairingStatus == SettingsBtStatus.PAIRING_STATUS_PAIRED ? 1 : 0) - + (mPairingStatus == SettingsBtStatus.PAIRING_STATUS_PAIRED ? 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); + } +} diff --git a/src/com/android/settings/bluetooth/LocalBluetoothDeviceManager.java b/src/com/android/settings/bluetooth/LocalBluetoothDeviceManager.java new file mode 100644 index 0000000..48a41f1 --- /dev/null +++ b/src/com/android/settings/bluetooth/LocalBluetoothDeviceManager.java @@ -0,0 +1,209 @@ +/* + * 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 android.app.AlertDialog; +import android.bluetooth.BluetoothDevice; +import android.util.Log; +import android.widget.Toast; +import android.content.Context; + +import com.android.settings.R; +import com.android.settings.bluetooth.LocalBluetoothManager.Callback; + +import java.util.ArrayList; +import java.util.List; + +/** + * LocalBluetoothDeviceManager manages the set of remote Bluetooth devices. + */ +public class LocalBluetoothDeviceManager { + private static final String TAG = "LocalBluetoothDeviceManager"; + + final LocalBluetoothManager mLocalManager; + final List<Callback> mCallbacks; + + final List<LocalBluetoothDevice> mDevices = new ArrayList<LocalBluetoothDevice>(); + + public LocalBluetoothDeviceManager(LocalBluetoothManager localManager) { + mLocalManager = localManager; + mCallbacks = localManager.getCallbacks(); + readPairedDevices(); + } + + private synchronized void readPairedDevices() { + BluetoothDevice manager = mLocalManager.getBluetoothManager(); + String[] bondedAddresses = manager.listBondings(); + if (bondedAddresses == null) return; + + for (String address : bondedAddresses) { + LocalBluetoothDevice device = findDevice(address); + if (device == null) { + device = new LocalBluetoothDevice(mLocalManager.getContext(), address); + mDevices.add(device); + dispatchDeviceAdded(device); + } + } + } + + public synchronized List<LocalBluetoothDevice> getDevicesCopy() { + return new ArrayList<LocalBluetoothDevice>(mDevices); + } + + void onBluetoothStateChanged(boolean enabled) { + if (enabled) { + readPairedDevices(); + } + } + + public synchronized void onDeviceAppeared(String address, short rssi) { + boolean deviceAdded = false; + + LocalBluetoothDevice device = findDevice(address); + if (device == null) { + device = new LocalBluetoothDevice(mLocalManager.getContext(), address); + mDevices.add(device); + deviceAdded = true; + } + + device.setRssi(rssi); + device.setVisible(true); + + if (deviceAdded) { + dispatchDeviceAdded(device); + } + } + + public synchronized void onDeviceDisappeared(String address) { + LocalBluetoothDevice device = findDevice(address); + if (device == null) return; + + device.setVisible(false); + checkForDeviceRemoval(device); + } + + private void checkForDeviceRemoval(LocalBluetoothDevice device) { + if (device.getPairingStatus() == SettingsBtStatus.PAIRING_STATUS_UNPAIRED && + !device.isVisible()) { + // If device isn't paired, remove it altogether + mDevices.remove(device); + dispatchDeviceDeleted(device); + } + } + + public synchronized void onDeviceNameUpdated(String address) { + LocalBluetoothDevice device = findDevice(address); + if (device != null) { + device.refreshName(); + } + } + + public synchronized LocalBluetoothDevice findDevice(String address) { + + for (int i = mDevices.size() - 1; i >= 0; i--) { + LocalBluetoothDevice device = mDevices.get(i); + + if (device.getAddress().equals(address)) { + return device; + } + } + + return null; + } + + /** + * Attempts to get the name of a remote device, otherwise returns the address. + * + * @param address The address. + * @return The name, or if unavailable, the address. + */ + public String getName(String address) { + LocalBluetoothDevice device = findDevice(address); + return device != null ? device.getName() : address; + } + + private void dispatchDeviceAdded(LocalBluetoothDevice device) { + synchronized (mCallbacks) { + for (Callback callback : mCallbacks) { + callback.onDeviceAdded(device); + } + } + + // TODO: divider between prev paired/connected and scanned + } + + private void dispatchDeviceDeleted(LocalBluetoothDevice device) { + synchronized (mCallbacks) { + for (Callback callback : mCallbacks) { + callback.onDeviceDeleted(device); + } + } + } + + public synchronized void onBondingStateChanged(String address, boolean created) { + LocalBluetoothDevice device = findDevice(address); + if (device == null) { + Log.e(TAG, "Got bonding state changed for " + address + + ", but we have no record of that device."); + return; + } + + device.setPairingStatus(created ? SettingsBtStatus.PAIRING_STATUS_PAIRED + : SettingsBtStatus.PAIRING_STATUS_UNPAIRED); + checkForDeviceRemoval(device); + + if (created) { + // Auto-connect after pairing + device.connect(); + } + } + + public synchronized void onBondingError(String address) { + mLocalManager.showError(address, R.string.bluetooth_error_title, + R.string.bluetooth_pairing_error_message); + } + + public synchronized void onProfileStateChanged(String address) { + LocalBluetoothDevice device = findDevice(address); + if (device == null) return; + + device.refresh(); + } + + public synchronized void onConnectingError(String address) { + LocalBluetoothDevice device = findDevice(address); + if (device == null) return; + + /* + * Go through the device's delegate so we don't spam the user with + * errors connecting to different profiles, and instead make sure the + * user sees a single error for his single 'connect' action. + */ + device.showConnectingError(); + } + + public synchronized void onScanningStateChanged(boolean started) { + if (!started) return; + + // If starting a new scan, clear old visibility + for (int i = mDevices.size() - 1; i >= 0; i--) { + LocalBluetoothDevice device = mDevices.get(i); + device.setVisible(false); + checkForDeviceRemoval(device); + } + } +} diff --git a/src/com/android/settings/bluetooth/LocalBluetoothManager.java b/src/com/android/settings/bluetooth/LocalBluetoothManager.java new file mode 100644 index 0000000..9db9e77 --- /dev/null +++ b/src/com/android/settings/bluetooth/LocalBluetoothManager.java @@ -0,0 +1,260 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; + +import android.app.Activity; +import android.app.AlertDialog; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; +import android.widget.Toast; + +// TODO: have some notion of shutting down. Maybe a minute after they leave BT settings? +/** + * LocalBluetoothManager provides a simplified interface on top of a subset of + * the Bluetooth API. + */ +public class LocalBluetoothManager { + private static final String TAG = "LocalBluetoothManager"; + static final boolean V = true; + + public static final String EXTENDED_BLUETOOTH_STATE_CHANGED_ACTION = + "com.android.settings.bluetooth.intent.action.EXTENDED_BLUETOOTH_STATE_CHANGED"; + private static final String SHARED_PREFERENCES_NAME = "bluetooth_settings"; + + private static LocalBluetoothManager INSTANCE; + /** Used when obtaining a reference to the singleton instance. */ + private static Object INSTANCE_LOCK = new Object(); + private boolean mInitialized; + + private Context mContext; + /** If a BT-related activity is in the foreground, this will be it. */ + private Activity mForegroundActivity; + + private BluetoothDevice mManager; + + private LocalBluetoothDeviceManager mLocalDeviceManager; + private BluetoothEventRedirector mEventRedirector; + + public static enum ExtendedBluetoothState { ENABLED, ENABLING, DISABLED, DISABLING, UNKNOWN } + private ExtendedBluetoothState mState = ExtendedBluetoothState.UNKNOWN; + + private List<Callback> mCallbacks = new ArrayList<Callback>(); + + private static final int SCAN_EXPIRATION_MS = 5 * 60 * 1000; // 5 mins + private long mLastScan; + + public static LocalBluetoothManager getInstance(Context context) { + synchronized (INSTANCE_LOCK) { + if (INSTANCE == null) { + INSTANCE = new LocalBluetoothManager(); + } + + if (!INSTANCE.init(context)) { + return null; + } + + return INSTANCE; + } + } + + private boolean init(Context context) { + if (mInitialized) return true; + mInitialized = true; + + // This will be around as long as this process is + mContext = context.getApplicationContext(); + + mManager = (BluetoothDevice) context.getSystemService(Context.BLUETOOTH_SERVICE); + if (mManager == null) { + return false; + } + + mLocalDeviceManager = new LocalBluetoothDeviceManager(this); + + mEventRedirector = new BluetoothEventRedirector(this); + mEventRedirector.start(); + + return true; + } + + public BluetoothDevice getBluetoothManager() { + return mManager; + } + + public Context getContext() { + return mContext; + } + + public Activity getForegroundActivity() { + return mForegroundActivity; + } + + public void setForegroundActivity(Activity activity) { + mForegroundActivity = activity; + } + + public SharedPreferences getSharedPreferences() { + return mContext.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); + } + + public LocalBluetoothDeviceManager getLocalDeviceManager() { + return mLocalDeviceManager; + } + + List<Callback> getCallbacks() { + return mCallbacks; + } + + public void registerCallback(Callback callback) { + synchronized (mCallbacks) { + mCallbacks.add(callback); + } + } + + public void unregisterCallback(Callback callback) { + synchronized (mCallbacks) { + mCallbacks.remove(callback); + } + } + + public void startScanning(boolean force) { + if (mManager.isDiscovering()) { + /* + * Already discovering, but give the callback that information. + * Note: we only call the callbacks, not the same path as if the + * scanning state had really changed (in that case the device + * manager would clear its list of unpaired scanned devices). + */ + dispatchScanningStateChanged(true); + } else { + + // Don't scan more than frequently than SCAN_EXPIRATION_MS, unless forced + if (!force && mLastScan + SCAN_EXPIRATION_MS > System.currentTimeMillis()) return; + + if (mManager.startDiscovery(true)) { + mLastScan = System.currentTimeMillis(); + } + } + } + + public ExtendedBluetoothState getBluetoothState() { + + if (mState == ExtendedBluetoothState.UNKNOWN) { + syncBluetoothState(); + } + + return mState; + } + + void setBluetoothStateInt(ExtendedBluetoothState state) { + mState = state; + + /* + * TODO: change to callback method. originally it was broadcast to + * parallel the framework's method, but it just complicates things here. + */ + // If this were a real API, I'd add as an extra + mContext.sendBroadcast(new Intent(EXTENDED_BLUETOOTH_STATE_CHANGED_ACTION)); + + if (state == ExtendedBluetoothState.ENABLED || state == ExtendedBluetoothState.DISABLED) { + mLocalDeviceManager.onBluetoothStateChanged(state == ExtendedBluetoothState.ENABLED); + } + } + + private void syncBluetoothState() { + setBluetoothStateInt(mManager.isEnabled() + ? ExtendedBluetoothState.ENABLED + : ExtendedBluetoothState.DISABLED); + } + + public void setBluetoothEnabled(boolean enabled) { + boolean wasSetStateSuccessful = enabled + ? mManager.enable() + : mManager.disable(); + + if (wasSetStateSuccessful) { + setBluetoothStateInt(enabled + ? ExtendedBluetoothState.ENABLING + : ExtendedBluetoothState.DISABLING); + } else { + if (V) { + Log.v(TAG, + "setBluetoothEnabled call, manager didn't return success for enabled: " + + enabled); + } + + syncBluetoothState(); + } + } + + /** + * @param started True if scanning started, false if scanning finished. + */ + void onScanningStateChanged(boolean started) { + // TODO: have it be a callback (once we switch bluetooth state changed to callback) + mLocalDeviceManager.onScanningStateChanged(started); + dispatchScanningStateChanged(started); + } + + private void dispatchScanningStateChanged(boolean started) { + synchronized (mCallbacks) { + for (Callback callback : mCallbacks) { + callback.onScanningStateChanged(started); + } + } + } + + public boolean createBonding(String address) { + return mManager.createBonding(address, mEventRedirector.getBluetoothDeviceCallback()); + } + + public void showError(String address, int titleResId, int messageResId) { + LocalBluetoothDevice device = mLocalDeviceManager.findDevice(address); + if (device == null) return; + + String name = device.getName(); + String message = mContext.getString(messageResId, name); + + if (mForegroundActivity != null) { + // Need an activity context to show a dialog + AlertDialog ad = new AlertDialog.Builder(mForegroundActivity) + .setIcon(android.R.drawable.ic_dialog_alert) + .setTitle(titleResId) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show(); + } else { + // Fallback on a toast + Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show(); + } + } + + public interface Callback { + void onScanningStateChanged(boolean started); + void onDeviceAdded(LocalBluetoothDevice device); + void onDeviceDeleted(LocalBluetoothDevice device); + } + +} diff --git a/src/com/android/settings/bluetooth/LocalBluetoothProfileManager.java b/src/com/android/settings/bluetooth/LocalBluetoothProfileManager.java new file mode 100644 index 0000000..b614712 --- /dev/null +++ b/src/com/android/settings/bluetooth/LocalBluetoothProfileManager.java @@ -0,0 +1,312 @@ +/* + * 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 android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothError; +import android.bluetooth.BluetoothHeadset; +import android.bluetooth.BluetoothClass; +import android.content.Context; +import android.content.SharedPreferences; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * LocalBluetoothProfileManager is an abstract class defining the basic + * functionality related to a profile. + */ +public abstract class LocalBluetoothProfileManager { + + // TODO: close profiles when we're shutting down + private static Map<Profile, LocalBluetoothProfileManager> sProfileMap = + new HashMap<Profile, LocalBluetoothProfileManager>(); + + protected LocalBluetoothManager mLocalManager; + + public static LocalBluetoothProfileManager getProfileManager(LocalBluetoothManager localManager, + Profile profile) { + + LocalBluetoothProfileManager profileManager; + + synchronized (sProfileMap) { + profileManager = sProfileMap.get(profile); + + if (profileManager == null) { + switch (profile) { + case A2DP: + profileManager = new A2dpProfileManager(localManager); + break; + + case HEADSET: + profileManager = new HeadsetProfileManager(localManager); + break; + } + + sProfileMap.put(profile, profileManager); + } + } + + return profileManager; + } + + // TODO: remove once the framework has this API + public static boolean isPreferredProfile(Context context, String address, Profile profile) { + return getPreferredProfileSharedPreferences(context).getBoolean( + getPreferredProfileKey(address, profile), true); + } + + public static void setPreferredProfile(Context context, String address, Profile profile, + boolean preferred) { + getPreferredProfileSharedPreferences(context).edit().putBoolean( + getPreferredProfileKey(address, profile), preferred).commit(); + } + + private static SharedPreferences getPreferredProfileSharedPreferences(Context context) { + return context.getSharedPreferences("bluetooth_preferred_profiles", Context.MODE_PRIVATE); + } + + private static String getPreferredProfileKey(String address, Profile profile) { + return address + "_" + profile.toString(); + } + + /** + * Temporary method to fill profiles based on a device's class. + * + * @param btClass The class + * @param profiles The list of profiles to fill + */ + public static void fill(int btClass, List<Profile> profiles) { + profiles.clear(); + + if (A2dpProfileManager.doesClassMatch(btClass)) { + profiles.add(Profile.A2DP); + } + + if (HeadsetProfileManager.doesClassMatch(btClass)) { + profiles.add(Profile.HEADSET); + } + } + + protected LocalBluetoothProfileManager(LocalBluetoothManager localManager) { + mLocalManager = localManager; + } + + public abstract int connect(String address); + + public abstract int disconnect(String address); + + public abstract int getConnectionStatus(String address); + + public abstract int getSummary(String address); + + public boolean isConnected(String address) { + return SettingsBtStatus.isConnectionStatusConnected(getConnectionStatus(address)); + } + + // TODO: int instead of enum + public enum Profile { + HEADSET(R.string.bluetooth_profile_headset), + A2DP(R.string.bluetooth_profile_a2dp); + + public final int localizedString; + + private Profile(int localizedString) { + this.localizedString = localizedString; + } + } + + /** + * A2dpProfileManager is an abstraction for the {@link BluetoothA2dp} service. + */ + private static class A2dpProfileManager extends LocalBluetoothProfileManager { + private BluetoothA2dp mService; + + public A2dpProfileManager(LocalBluetoothManager localManager) { + super(localManager); + + mService = new BluetoothA2dp(localManager.getContext()); + // TODO: block until connection? + } + + @Override + public int connect(String address) { + return mService.connectSink(address); + } + + @Override + public int disconnect(String address) { + return mService.disconnectSink(address); + } + + static boolean doesClassMatch(int btClass) { + if (BluetoothClass.Service.hasService(btClass, BluetoothClass.Service.RENDER)) { + return true; + } + + // By the specification A2DP sinks must indicate the RENDER service + // class, but some do not (Chordette). So match on a few more to be + // safe + switch (BluetoothClass.Device.getDevice(btClass)) { + case BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO: + case BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES: + case BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER: + case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO: + return true; + + default: + return false; + } + } + + @Override + public int getConnectionStatus(String address) { + return convertState(mService.getSinkState(address)); + } + + @Override + public int getSummary(String address) { + int connectionStatus = getConnectionStatus(address); + + if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus)) { + return R.string.bluetooth_a2dp_profile_summary_connected; + } else { + return SettingsBtStatus.getConnectionStatusSummary(connectionStatus); + } + } + + private static int convertState(int a2dpState) { + switch (a2dpState) { + case BluetoothA2dp.STATE_CONNECTED: + return SettingsBtStatus.CONNECTION_STATUS_CONNECTED; + case BluetoothA2dp.STATE_CONNECTING: + return SettingsBtStatus.CONNECTION_STATUS_CONNECTING; + case BluetoothA2dp.STATE_DISCONNECTED: + return SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED; + case BluetoothA2dp.STATE_DISCONNECTING: + return SettingsBtStatus.CONNECTION_STATUS_DISCONNECTING; + case BluetoothA2dp.STATE_PLAYING: + return SettingsBtStatus.CONNECTION_STATUS_ACTIVE; + default: + return SettingsBtStatus.CONNECTION_STATUS_UNKNOWN; + } + } + } + + /** + * HeadsetProfileManager is an abstraction for the {@link BluetoothHeadset} service. + */ + private static class HeadsetProfileManager extends LocalBluetoothProfileManager { + private BluetoothHeadset mService; + + public HeadsetProfileManager(LocalBluetoothManager localManager) { + super(localManager); + +// final boolean[] isServiceConnected = new boolean[1]; +// BluetoothHeadset.ServiceListener l = new BluetoothHeadset.ServiceListener() { +// public void onServiceConnected() { +// synchronized (this) { +// isServiceConnected[0] = true; +// notifyAll(); +// } +// } +// public void onServiceDisconnected() { +// mService = null; +// } +// }; + + // TODO: block, but can't on UI thread + mService = new BluetoothHeadset(localManager.getContext(), null); + +// synchronized (l) { +// while (!isServiceConnected[0]) { +// try { +// l.wait(100); +// } catch (InterruptedException e) { +// throw new IllegalStateException(e); +// } +// } +// } + } + + @Override + public int connect(String address) { + // Since connectHeadset fails if already connected to a headset, we + // disconnect from any headset first + mService.disconnectHeadset(); + return mService.connectHeadset(address, null) + ? BluetoothError.SUCCESS : BluetoothError.ERROR; + } + + @Override + public int disconnect(String address) { + if (mService.getHeadsetAddress().equals(address)) { + return mService.disconnectHeadset() ? BluetoothError.SUCCESS : BluetoothError.ERROR; + } else { + return BluetoothError.SUCCESS; + } + } + + static boolean doesClassMatch(int btClass) { + switch (BluetoothClass.Device.getDevice(btClass)) { + case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE: + case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET: + case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO: + return true; + + default: + return false; + } + } + + @Override + public int getConnectionStatus(String address) { + String headsetAddress = mService.getHeadsetAddress(); + return headsetAddress != null && headsetAddress.equals(address) + ? convertState(mService.getState()) + : SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED; + } + + @Override + public int getSummary(String address) { + int connectionStatus = getConnectionStatus(address); + + if (SettingsBtStatus.isConnectionStatusConnected(connectionStatus)) { + return R.string.bluetooth_headset_profile_summary_connected; + } else { + return SettingsBtStatus.getConnectionStatusSummary(connectionStatus); + } + } + + private static int convertState(int headsetState) { + switch (headsetState) { + case BluetoothHeadset.STATE_CONNECTED: + return SettingsBtStatus.CONNECTION_STATUS_CONNECTED; + case BluetoothHeadset.STATE_CONNECTING: + return SettingsBtStatus.CONNECTION_STATUS_CONNECTING; + case BluetoothHeadset.STATE_DISCONNECTED: + return SettingsBtStatus.CONNECTION_STATUS_DISCONNECTED; + default: + return SettingsBtStatus.CONNECTION_STATUS_UNKNOWN; + } + } + } + +} diff --git a/src/com/android/settings/bluetooth/SettingsBtStatus.java b/src/com/android/settings/bluetooth/SettingsBtStatus.java new file mode 100644 index 0000000..051d666 --- /dev/null +++ b/src/com/android/settings/bluetooth/SettingsBtStatus.java @@ -0,0 +1,85 @@ +/* + * 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; + +/** + * SettingsBtStatus is a helper class that contains constants for various status + * codes. + */ +public class SettingsBtStatus { + private static final String TAG = "SettingsBtStatus"; + + // Connection status + + public static final int CONNECTION_STATUS_UNKNOWN = 0; + public static final int CONNECTION_STATUS_ACTIVE = 1; + /** Use {@link #isConnected} to check for the connected state */ + public static final int CONNECTION_STATUS_CONNECTED = 2; + public static final int CONNECTION_STATUS_CONNECTING = 3; + public static final int CONNECTION_STATUS_DISCONNECTED = 4; + public static final int CONNECTION_STATUS_DISCONNECTING = 5; + + public static final int getConnectionStatusSummary(int connectionStatus) { + switch (connectionStatus) { + case CONNECTION_STATUS_ACTIVE: + return R.string.bluetooth_connected; + case CONNECTION_STATUS_CONNECTED: + return R.string.bluetooth_connected; + case CONNECTION_STATUS_CONNECTING: + return R.string.bluetooth_connecting; + case CONNECTION_STATUS_DISCONNECTED: + return R.string.bluetooth_disconnected; + case CONNECTION_STATUS_DISCONNECTING: + return R.string.bluetooth_disconnecting; + case CONNECTION_STATUS_UNKNOWN: + return R.string.bluetooth_unknown; + default: + return 0; + } + } + + public static final boolean isConnectionStatusConnected(int connectionStatus) { + return connectionStatus == CONNECTION_STATUS_ACTIVE + || connectionStatus == CONNECTION_STATUS_CONNECTED; + } + + public static final boolean isConnectionStatusBusy(int connectionStatus) { + return connectionStatus == CONNECTION_STATUS_CONNECTING + || connectionStatus == CONNECTION_STATUS_DISCONNECTING; + } + + // Pairing status + + public static final int PAIRING_STATUS_UNPAIRED = 0; + public static final int PAIRING_STATUS_PAIRED = 1; + public static final int PAIRING_STATUS_PAIRING = 2; + + public static final int getPairingStatusSummary(int pairingStatus) { + switch (pairingStatus) { + case PAIRING_STATUS_PAIRED: + return R.string.bluetooth_paired; + case PAIRING_STATUS_PAIRING: + return R.string.bluetooth_pairing; + case PAIRING_STATUS_UNPAIRED: + return R.string.bluetooth_not_connected; + default: + return 0; + } + } +} |