diff --git a/res/xml/bluetooth_device_details_fragment.xml b/res/xml/bluetooth_device_details_fragment.xml
new file mode 100644
index 00000000000..098daaa35f4
--- /dev/null
+++ b/res/xml/bluetooth_device_details_fragment.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsButtonsController.java b/src/com/android/settings/bluetooth/BluetoothDetailsButtonsController.java
new file mode 100644
index 00000000000..41cd28d8926
--- /dev/null
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsButtonsController.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2017 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.content.Context;
+import android.support.v14.preference.PreferenceFragment;
+import android.support.v7.preference.PreferenceScreen;
+import android.text.TextUtils;
+import android.widget.Button;
+
+import com.android.settings.R;
+import com.android.settings.applications.LayoutPreference;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+/**
+ * This class adds two buttons: one to connect/disconnect from a device (depending on the current
+ * connected state), and one to "forget" (ie unpair) the device.
+ */
+public class BluetoothDetailsButtonsController extends BluetoothDetailsController {
+ private static final String KEY_ACTION_BUTTONS = "action_buttons";
+ private boolean mIsConnected;
+
+ private LayoutPreference mActionButtons;
+
+ public BluetoothDetailsButtonsController(Context context, PreferenceFragment fragment,
+ CachedBluetoothDevice device, Lifecycle lifecycle) {
+ super(context, fragment, device, lifecycle);
+ mIsConnected = device.isConnected();
+ }
+
+ @Override
+ protected void init(PreferenceScreen screen) {
+ mActionButtons = (LayoutPreference) screen.findPreference(getPreferenceKey());
+ Button rightButton = (Button) mActionButtons.findViewById(R.id.right_button);
+ rightButton.setText(R.string.forget);
+ rightButton.setOnClickListener((view) -> {
+ mCachedDevice.unpair();
+ mFragment.getActivity().finish();
+ });
+ }
+
+ @Override
+ protected void refresh() {
+ Button leftButton = (Button) mActionButtons.findViewById(R.id.left_button);
+ leftButton.setEnabled(!mCachedDevice.isBusy());
+ boolean notInitialized = TextUtils.isEmpty(leftButton.getText());
+
+ boolean previouslyConnected = mIsConnected;
+ mIsConnected = mCachedDevice.isConnected();
+ if (mIsConnected) {
+ if (notInitialized || !previouslyConnected) {
+ leftButton.setText(R.string.bluetooth_device_context_disconnect);
+ leftButton.setOnClickListener((view) -> {
+ mCachedDevice.disconnect();
+ });
+ }
+ } else {
+ if (notInitialized || previouslyConnected) {
+ leftButton.setText(R.string.bluetooth_device_context_connect);
+ leftButton.setOnClickListener((view) -> {
+ mCachedDevice.connect(true);
+ });
+ }
+ }
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return KEY_ACTION_BUTTONS;
+ }
+}
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsController.java b/src/com/android/settings/bluetooth/BluetoothDetailsController.java
new file mode 100644
index 00000000000..73e9f31c685
--- /dev/null
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsController.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2017 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.content.Context;
+import android.support.v14.preference.PreferenceFragment;
+import android.support.v7.preference.PreferenceScreen;
+
+import com.android.settings.core.PreferenceController;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
+import com.android.settingslib.core.lifecycle.events.OnPause;
+import com.android.settingslib.core.lifecycle.events.OnResume;
+
+/**
+ * This class provides common lifecycle and bluetooth device event registration for Bluetooth device
+ * details controllers.
+ */
+public abstract class BluetoothDetailsController extends PreferenceController
+ implements CachedBluetoothDevice.Callback, LifecycleObserver, OnPause, OnResume {
+
+ protected final Context mContext;
+ protected final PreferenceFragment mFragment;
+ protected final CachedBluetoothDevice mCachedDevice;
+
+ public BluetoothDetailsController(Context context, PreferenceFragment fragment,
+ CachedBluetoothDevice device, Lifecycle lifecycle) {
+ super(context);
+ mContext = context;
+ mFragment = fragment;
+ mCachedDevice = device;
+ lifecycle.addObserver(this);
+ }
+
+ @Override
+ public void onPause() {
+ mCachedDevice.unregisterCallback(this);
+ }
+
+ @Override
+ public void onResume() {
+ mCachedDevice.registerCallback(this);
+ refresh();
+ }
+
+ @Override
+ public boolean isAvailable() {
+ return true;
+ }
+
+ @Override
+ public void onDeviceAttributesChanged() {
+ refresh();
+ }
+
+ @Override
+ public final void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ init(screen);
+ }
+
+ /**
+ * This is a method to do one-time initialization when the screen is first created, such as
+ * adding preferences.
+ * @param screen the screen where this controller's preferences should be added
+ */
+ protected abstract void init(PreferenceScreen screen);
+
+ /**
+ * This method is called when something about the bluetooth device has changed, and this object
+ * should update the preferences it manages based on the new state.
+ */
+ protected abstract void refresh();
+}
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHeaderController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHeaderController.java
new file mode 100644
index 00000000000..de503b28b8c
--- /dev/null
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsHeaderController.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2017 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.content.Context;
+import android.support.v14.preference.PreferenceFragment;
+import android.support.v7.preference.PreferenceScreen;
+import android.util.Pair;
+
+import com.android.settings.applications.LayoutPreference;
+import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+/**
+ * This class adds a header with device name and status (connected/disconnected, etc.).
+ */
+public class BluetoothDetailsHeaderController extends BluetoothDetailsController {
+
+ private EntityHeaderController mHeaderController;
+
+ public BluetoothDetailsHeaderController(Context context, PreferenceFragment fragment,
+ CachedBluetoothDevice device, Lifecycle lifecycle) {
+ super(context, fragment, device, lifecycle);
+ }
+
+ @Override
+ protected void init(PreferenceScreen screen) {
+ mHeaderController = EntityHeaderController.newInstance(mFragment.getActivity(), mFragment,
+ null);
+ LayoutPreference pref = mHeaderController.done(mFragment.getActivity(), mContext);
+ screen.addPreference(pref);
+ }
+
+ protected void setHeaderProperties() {
+ Pair pair = Utils.getBtClassDrawableWithDescription
+ (mContext.getResources(), mCachedDevice);
+ int summaryResourceId = mCachedDevice.getConnectionSummary();
+ mHeaderController.setLabel(mCachedDevice.getName());
+ mHeaderController.setIcon(mContext.getDrawable(pair.first));
+ mHeaderController.setIconContentDescription(pair.second);
+ mHeaderController.setSummary(
+ summaryResourceId > 0 ? mContext.getString(summaryResourceId) : null);
+ }
+
+ @Override
+ protected void refresh() {
+ setHeaderProperties();
+ mHeaderController.done(mFragment.getActivity(), false);
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return EntityHeaderController.PREF_KEY_APP_HEADER;
+ }
+}
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsMacAddressController.java b/src/com/android/settings/bluetooth/BluetoothDetailsMacAddressController.java
new file mode 100644
index 00000000000..c5cb74b18a6
--- /dev/null
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsMacAddressController.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2017 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.content.Context;
+import android.support.v14.preference.PreferenceFragment;
+import android.support.v7.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+import com.android.settingslib.widget.FooterPreference;
+import com.android.settingslib.widget.FooterPreferenceMixin;
+
+/**
+ * This class adds the device MAC address to a footer.
+ */
+public class BluetoothDetailsMacAddressController extends BluetoothDetailsController {
+ FooterPreferenceMixin mFooterPreferenceMixin;
+ FooterPreference mFooterPreference;
+
+ public BluetoothDetailsMacAddressController(Context context,
+ PreferenceFragment fragment,
+ CachedBluetoothDevice device,
+ Lifecycle lifecycle) {
+ super(context, fragment, device, lifecycle);
+ mFooterPreferenceMixin = new FooterPreferenceMixin(fragment, lifecycle);
+ }
+
+ @Override
+ protected void init(PreferenceScreen screen) {
+ mFooterPreference = mFooterPreferenceMixin.createFooterPreference();
+ mFooterPreference.setTitle(mContext.getString(
+ R.string.bluetooth_device_mac_address, mCachedDevice.getDevice().getAddress()));
+ }
+
+ @Override
+ protected void refresh() {}
+
+ @Override
+ public String getPreferenceKey() {
+ return mFooterPreference.getKey();
+ }
+}
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java b/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java
new file mode 100644
index 00000000000..b0ed05681d6
--- /dev/null
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2017 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.BluetoothProfile;
+import android.content.Context;
+import android.support.annotation.VisibleForTesting;
+import android.support.v14.preference.PreferenceFragment;
+import android.support.v14.preference.SwitchPreference;
+import android.support.v7.preference.Preference;
+import android.support.v7.preference.PreferenceCategory;
+import android.support.v7.preference.PreferenceScreen;
+import android.text.TextUtils;
+
+import com.android.settingslib.bluetooth.A2dpProfile;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.bluetooth.MapProfile;
+import com.android.settingslib.bluetooth.PanProfile;
+import com.android.settingslib.bluetooth.PbapServerProfile;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import java.util.List;
+
+/**
+ * This class adds switches for toggling the individual profiles that a Bluetooth device
+ * supports, such as "Phone audio", "Media audio", "Contact sharing", etc.
+ */
+public class BluetoothDetailsProfilesController extends BluetoothDetailsController
+ implements Preference.OnPreferenceClickListener {
+ private static final String KEY_PROFILES_GROUP = "bluetooth_profiles";
+
+ @VisibleForTesting
+ static final String HIGH_QUALITY_AUDIO_PREF_TAG = "A2dpProfileHighQualityAudio";
+
+ private LocalBluetoothManager mManager;
+ private LocalBluetoothProfileManager mProfileManager;
+ private CachedBluetoothDevice mCachedDevice;
+ private PreferenceCategory mProfilesContainer;
+
+ public BluetoothDetailsProfilesController(Context context, PreferenceFragment fragment,
+ LocalBluetoothManager manager, CachedBluetoothDevice device, Lifecycle lifecycle) {
+ super(context, fragment, device, lifecycle);
+ mManager = manager;
+ mProfileManager = mManager.getProfileManager();
+ mCachedDevice = device;
+ lifecycle.addObserver(this);
+ }
+
+ @Override
+ protected void init(PreferenceScreen screen) {
+ mProfilesContainer = (PreferenceCategory)screen.findPreference(getPreferenceKey());
+ // Call refresh here even though it will get called later in onResume, to avoid the
+ // list of switches appearing to "pop" into the page.
+ refresh();
+ }
+
+ /**
+ * Creates a switch preference for the particular profile.
+ *
+ * @param context The context to use when creating the SwitchPreference
+ * @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 SwitchPreference createProfilePreference(Context context,
+ LocalBluetoothProfile profile) {
+ SwitchPreference pref = new SwitchPreference(context);
+ pref.setKey(profile.toString());
+ pref.setTitle(profile.getNameResource(mCachedDevice.getDevice()));
+ pref.setOnPreferenceClickListener(this);
+ return pref;
+ }
+
+ /**
+ * Refreshes the state for an existing SwitchPreference for a profile.
+ */
+ private void refreshProfilePreference(SwitchPreference profilePref,
+ LocalBluetoothProfile profile) {
+ BluetoothDevice device = mCachedDevice.getDevice();
+ profilePref.setEnabled(!mCachedDevice.isBusy());
+ if (profile instanceof MapProfile) {
+ profilePref.setChecked(mCachedDevice.getMessagePermissionChoice()
+ == CachedBluetoothDevice.ACCESS_ALLOWED);
+ } else if (profile instanceof PbapServerProfile) {
+ profilePref.setChecked(mCachedDevice.getPhonebookPermissionChoice()
+ == CachedBluetoothDevice.ACCESS_ALLOWED);
+ } else if (profile instanceof PanProfile) {
+ profilePref.setChecked(profile.getConnectionStatus(device) ==
+ BluetoothProfile.STATE_CONNECTED);
+ } else {
+ profilePref.setChecked(profile.isPreferred(device));
+ }
+
+ if (profile instanceof A2dpProfile) {
+ A2dpProfile a2dp = (A2dpProfile) profile;
+ SwitchPreference highQualityPref = (SwitchPreference) mProfilesContainer.findPreference(
+ HIGH_QUALITY_AUDIO_PREF_TAG);
+ if (highQualityPref != null) {
+ if (a2dp.isPreferred(device) && a2dp.supportsHighQualityAudio(device)) {
+ highQualityPref.setVisible(true);
+ highQualityPref.setTitle(a2dp.getHighQualityAudioOptionLabel(device));
+ highQualityPref.setChecked(a2dp.isHighQualityAudioEnabled(device));
+ highQualityPref.setEnabled(!mCachedDevice.isBusy());
+ } else {
+ highQualityPref.setVisible(false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper method to enable a profile for a device.
+ */
+ private void enableProfile(LocalBluetoothProfile profile, BluetoothDevice device,
+ SwitchPreference profilePref) {
+ if (profile instanceof PbapServerProfile) {
+ mCachedDevice.setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
+ // We don't need to do the additional steps below for this profile.
+ return;
+ }
+ if (profile instanceof MapProfile) {
+ mCachedDevice.setMessagePermissionChoice(BluetoothDevice.ACCESS_ALLOWED);
+ }
+ profile.setPreferred(device, true);
+ mCachedDevice.connectProfile(profile);
+ }
+
+ /**
+ * Helper method to disable a profile for a device
+ */
+ private void disableProfile(LocalBluetoothProfile profile, BluetoothDevice device,
+ SwitchPreference profilePref) {
+ if (profile instanceof PbapServerProfile) {
+ mCachedDevice.setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
+ // We don't need to do the additional steps below for this profile.
+ return;
+ }
+ mCachedDevice.disconnect(profile);
+ profile.setPreferred(device, false);
+ if (profile instanceof MapProfile) {
+ mCachedDevice.setMessagePermissionChoice(BluetoothDevice.ACCESS_REJECTED);
+ }
+ }
+
+ /**
+ * When the pref for a bluetooth profile is clicked on, we want to toggle the enabled/disabled
+ * state for that profile.
+ */
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ LocalBluetoothProfile profile = mProfileManager.getProfileByName(preference.getKey());
+ if (profile == null) {
+ // It might be the PbapServerProfile, which is not stored by name.
+ PbapServerProfile psp = mManager.getProfileManager().getPbapProfile();
+ if (TextUtils.equals(preference.getKey(), psp.toString())) {
+ profile = psp;
+ } else {
+ return false;
+ }
+ }
+ SwitchPreference profilePref = (SwitchPreference) preference;
+ BluetoothDevice device = mCachedDevice.getDevice();
+ if (profilePref.isChecked()) {
+ enableProfile(profile, device, profilePref);
+ } else {
+ disableProfile(profile, device, profilePref);
+ }
+ refreshProfilePreference(profilePref, profile);
+ return true;
+ }
+
+
+ /**
+ * Helper to get the list of connectable and special profiles.
+ */
+ private List getProfiles() {
+ List result = mCachedDevice.getConnectableProfiles();
+
+ final int pbapPermission = mCachedDevice.getPhonebookPermissionChoice();
+ // Only provide PBAP cabability if the client device has requested PBAP.
+ if (pbapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) {
+ final PbapServerProfile psp = mManager.getProfileManager().getPbapProfile();
+ result.add(psp);
+ }
+
+ final MapProfile mapProfile = mManager.getProfileManager().getMapProfile();
+ final int mapPermission = mCachedDevice.getMessagePermissionChoice();
+ if (mapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) {
+ result.add(mapProfile);
+ }
+
+ return result;
+ }
+
+ /**
+ * This is a helper method to be called after adding a Preference for a profile. If that
+ * profile happened to be A2dp and the device supports high quality audio, it will add a
+ * separate preference for controlling whether to actually use high quality audio.
+ *
+ * @param profile the profile just added
+ */
+ private void maybeAddHighQualityAudioPref(LocalBluetoothProfile profile) {
+ if (!(profile instanceof A2dpProfile)) {
+ return;
+ }
+ BluetoothDevice device = mCachedDevice.getDevice();
+ A2dpProfile a2dp = (A2dpProfile) profile;
+ if (a2dp.supportsHighQualityAudio(device)) {
+ SwitchPreference highQualityAudioPref = new SwitchPreference(
+ mProfilesContainer.getContext());
+ highQualityAudioPref.setKey(HIGH_QUALITY_AUDIO_PREF_TAG);
+ highQualityAudioPref.setVisible(false);
+ highQualityAudioPref.setOnPreferenceClickListener(clickedPref -> {
+ boolean enable = ((SwitchPreference) clickedPref).isChecked();
+ a2dp.setHighQualityAudioEnabled(mCachedDevice.getDevice(), enable);
+ return true;
+ });
+ mProfilesContainer.addPreference(highQualityAudioPref);
+ }
+ }
+
+ /**
+ * Refreshes the state of the switches for all profiles, possibly adding or removing switches as
+ * needed.
+ */
+ @Override
+ protected void refresh() {
+ for (LocalBluetoothProfile profile : getProfiles()) {
+ SwitchPreference pref = (SwitchPreference) mProfilesContainer.findPreference(
+ profile.toString());
+ if (pref == null) {
+ pref = createProfilePreference(mProfilesContainer.getContext(), profile);
+ mProfilesContainer.addPreference(pref);
+ maybeAddHighQualityAudioPref(profile);
+ }
+ refreshProfilePreference(pref, profile);
+ }
+ for (LocalBluetoothProfile removedProfile : mCachedDevice.getRemovedProfiles()) {
+ SwitchPreference pref = (SwitchPreference) mProfilesContainer.findPreference(
+ removedProfile.toString());
+ if (pref != null) {
+ mProfilesContainer.removePreference(pref);
+ }
+ }
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return KEY_PROFILES_GROUP;
+ }
+}
diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
new file mode 100644
index 00000000000..c81e1eef428
--- /dev/null
+++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2017 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 static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.settings.R;
+import com.android.settings.core.PreferenceController;
+import com.android.settings.dashboard.RestrictedDashboardFragment;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment {
+ public static final String KEY_DEVICE_ADDRESS = "device_address";
+ private static final String TAG = "BTDeviceDetailsFrg";
+
+ private String mDeviceAddress;
+
+ public BluetoothDeviceDetailsFragment() {
+ super(DISALLOW_CONFIG_BLUETOOTH);
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ mDeviceAddress = getArguments().getString(KEY_DEVICE_ADDRESS);
+ super.onAttach(context);
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ return MetricsProto.MetricsEvent.BLUETOOTH_DEVICE_DETAILS;
+ }
+
+ @Override
+ protected String getLogTag() {
+ return TAG;
+ }
+
+ @Override
+ protected int getPreferenceScreenResId() {
+ return R.xml.bluetooth_device_details_fragment;
+ }
+
+ @Override
+ protected List getPreferenceControllers(Context context) {
+ ArrayList controllers = new ArrayList<>();
+ LocalBluetoothManager manager = Utils.getLocalBtManager(context);
+ BluetoothDevice remoteDevice = manager.getBluetoothAdapter().getRemoteDevice(
+ mDeviceAddress);
+ CachedBluetoothDevice device = manager.getCachedDeviceManager().findDevice(remoteDevice);
+ if (device != null) {
+ Lifecycle lifecycle = getLifecycle();
+ controllers.add(new BluetoothDetailsHeaderController(context, this, device, lifecycle));
+ controllers.add(new BluetoothDetailsButtonsController(context, this, device,
+ lifecycle));
+ controllers.add(new BluetoothDetailsProfilesController(context, this, manager, device,
+ lifecycle));
+ controllers.add(new BluetoothDetailsMacAddressController(context, this, device,
+ lifecycle));
+ }
+ return controllers;
+ }
+}
diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java
index 7be6dd711fe..8281d76d670 100644
--- a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java
+++ b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java
@@ -17,7 +17,6 @@
package com.android.settings.bluetooth;
import android.app.AlertDialog;
-import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.DialogInterface;
@@ -27,7 +26,6 @@ import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceViewHolder;
import android.text.Html;
import android.text.TextUtils;
-import android.util.Log;
import android.util.Pair;
import android.util.TypedValue;
import android.widget.ImageView;
@@ -38,10 +36,6 @@ import com.android.settings.core.instrumentation.MetricsFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.GearPreference;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
-import com.android.settingslib.bluetooth.HidProfile;
-import com.android.settingslib.bluetooth.LocalBluetoothProfile;
-
-import java.util.List;
import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
@@ -63,18 +57,11 @@ public final class BluetoothDevicePreference extends GearPreference implements
private String contentDescription = null;
/* Talk-back descriptions for various BT icons */
- Resources r = getContext().getResources();
- public final String COMPUTER = r.getString(R.string.bluetooth_talkback_computer);
- public final String INPUT_PERIPHERAL = r.getString(
- R.string.bluetooth_talkback_input_peripheral);
- public final String HEADSET = r.getString(R.string.bluetooth_talkback_headset);
- public final String PHONE = r.getString(R.string.bluetooth_talkback_phone);
- public final String IMAGING = r.getString(R.string.bluetooth_talkback_imaging);
- public final String HEADPHONE = r.getString(R.string.bluetooth_talkback_headphone);
- public final String BLUETOOTH = r.getString(R.string.bluetooth_talkback_bluetooth);
+ Resources mResources;
public BluetoothDevicePreference(Context context, CachedBluetoothDevice cachedDevice) {
super(context, null);
+ mResources = getContext().getResources();
mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
if (sDimAlpha == Integer.MIN_VALUE) {
@@ -139,7 +126,8 @@ public final class BluetoothDevicePreference extends GearPreference implements
}
- Pair pair = getBtClassDrawableWithDescription();
+ Pair pair = Utils.getBtClassDrawableWithDescription(mResources,
+ mCachedDevice);
if (pair.first != 0) {
setIcon(pair.first);
contentDescription = pair.second;
@@ -246,45 +234,4 @@ public final class BluetoothDevicePreference extends GearPreference implements
}
}
- private Pair getBtClassDrawableWithDescription() {
- BluetoothClass btClass = mCachedDevice.getBtClass();
- if (btClass != null) {
- switch (btClass.getMajorDeviceClass()) {
- case BluetoothClass.Device.Major.COMPUTER:
- return new Pair(R.drawable.ic_bt_laptop, COMPUTER);
-
- case BluetoothClass.Device.Major.PHONE:
- return new Pair(R.drawable.ic_bt_cellphone, PHONE);
-
- case BluetoothClass.Device.Major.PERIPHERAL:
- return new Pair(HidProfile.getHidClassDrawable(btClass),
- INPUT_PERIPHERAL);
-
- case BluetoothClass.Device.Major.IMAGING:
- return new Pair(R.drawable.ic_bt_imaging, IMAGING);
-
- default:
- // unrecognized device class; continue
- }
- } else {
- Log.w(TAG, "mBtClass is null");
- }
-
- List profiles = mCachedDevice.getProfiles();
- for (LocalBluetoothProfile profile : profiles) {
- int resId = profile.getDrawableResource(btClass);
- if (resId != 0) {
- return new Pair(resId, null);
- }
- }
- if (btClass != null) {
- if (btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
- return new Pair(R.drawable.ic_bt_headset_hfp, HEADSET);
- }
- if (btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
- return new Pair(R.drawable.ic_bt_headphones_a2dp, HEADPHONE);
- }
- }
- return new Pair(R.drawable.ic_settings_bluetooth, BLUETOOTH);
- }
}
diff --git a/src/com/android/settings/bluetooth/BluetoothSettings.java b/src/com/android/settings/bluetooth/BluetoothSettings.java
index b37770abf33..f8648efd1b8 100644
--- a/src/com/android/settings/bluetooth/BluetoothSettings.java
+++ b/src/com/android/settings/bluetooth/BluetoothSettings.java
@@ -17,6 +17,7 @@
package com.android.settings.bluetooth;
import android.app.Activity;
+import android.app.Fragment;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.ContentResolver;
@@ -25,6 +26,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.os.Bundle;
+import android.os.SystemProperties;
import android.provider.Settings;
import android.support.annotation.VisibleForTesting;
import android.support.v7.preference.Preference;
@@ -352,12 +354,28 @@ public class BluetoothSettings extends DeviceListPreferenceFragment implements I
return;
}
final Bundle args = new Bundle();
- args.putString(DeviceProfilesSettings.ARG_DEVICE_ADDRESS,
- device.getDevice().getAddress());
- final DeviceProfilesSettings profileSettings = new DeviceProfilesSettings();
- profileSettings.setArguments(args);
- profileSettings.show(getFragmentManager(),
- DeviceProfilesSettings.class.getSimpleName());
+ Context context = getActivity();
+ boolean useDetailPage = FeatureFactory.getFactory(context).getBluetoothFeatureProvider(
+ context).isDeviceDetailPageEnabled();
+ if (!useDetailPage) {
+ // Old version - uses a dialog.
+ args.putString(DeviceProfilesSettings.ARG_DEVICE_ADDRESS,
+ device.getDevice().getAddress());
+ final DeviceProfilesSettings profileSettings = new DeviceProfilesSettings();
+ profileSettings.setArguments(args);
+ profileSettings.show(getFragmentManager(),
+ DeviceProfilesSettings.class.getSimpleName());
+ } else {
+ // New version - uses a separate screen.
+ args.putString(BluetoothDeviceDetailsFragment.KEY_DEVICE_ADDRESS,
+ device.getDevice().getAddress());
+ BluetoothDeviceDetailsFragment fragment = new BluetoothDeviceDetailsFragment();
+ final SettingsActivity activity =
+ (SettingsActivity) BluetoothSettings.this.getActivity();
+ activity.startPreferencePanel(this,
+ BluetoothDeviceDetailsFragment.class.getName(), args,
+ R.string.device_details_title, null, null, 0);
+ }
};
/**
diff --git a/src/com/android/settings/bluetooth/Utils.java b/src/com/android/settings/bluetooth/Utils.java
index eb194f4443b..b370c11beb1 100755
--- a/src/com/android/settings/bluetooth/Utils.java
+++ b/src/com/android/settings/bluetooth/Utils.java
@@ -17,20 +17,28 @@
package com.android.settings.bluetooth;
import android.app.AlertDialog;
+import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.DialogInterface;
+import android.content.res.Resources;
import android.support.annotation.VisibleForTesting;
+import android.util.Pair;
import android.widget.Toast;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.HidProfile;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.bluetooth.LocalBluetoothManager.BluetoothManagerCallback;
+import com.android.settingslib.bluetooth.LocalBluetoothProfile;
import com.android.settingslib.bluetooth.Utils.ErrorListener;
+import java.util.List;
+
/**
* Utils is a helper class that contains constants for various
* Android resource IDs, debug logging flags, and static methods
@@ -141,4 +149,52 @@ public final class Utils {
com.android.settingslib.bluetooth.Utils.setErrorListener(mErrorListener);
}
};
+
+ static Pair getBtClassDrawableWithDescription(Resources r,
+ CachedBluetoothDevice cachedDevice) {
+ BluetoothClass btClass = cachedDevice.getBtClass();
+ if (btClass != null) {
+ switch (btClass.getMajorDeviceClass()) {
+ case BluetoothClass.Device.Major.COMPUTER:
+ return new Pair(R.drawable.ic_bt_laptop,
+ r.getString(R.string.bluetooth_talkback_computer));
+
+ case BluetoothClass.Device.Major.PHONE:
+ return new Pair(R.drawable.ic_bt_cellphone,
+ r.getString(R.string.bluetooth_talkback_phone));
+
+ case BluetoothClass.Device.Major.PERIPHERAL:
+ return new Pair(HidProfile.getHidClassDrawable(btClass),
+ r.getString(
+ R.string.bluetooth_talkback_input_peripheral));
+
+ case BluetoothClass.Device.Major.IMAGING:
+ return new Pair(R.drawable.ic_bt_imaging,
+ r.getString(R.string.bluetooth_talkback_imaging));
+
+ default:
+ // unrecognized device class; continue
+ }
+ }
+
+ List profiles = cachedDevice.getProfiles();
+ for (LocalBluetoothProfile profile : profiles) {
+ int resId = profile.getDrawableResource(btClass);
+ if (resId != 0) {
+ return new Pair(resId, null);
+ }
+ }
+ if (btClass != null) {
+ if (btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
+ return new Pair(R.drawable.ic_bt_headset_hfp,
+ r.getString(R.string.bluetooth_talkback_headset));
+ }
+ if (btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
+ return new Pair(R.drawable.ic_bt_headphones_a2dp,
+ r.getString(R.string.bluetooth_talkback_headphone));
+ }
+ }
+ return new Pair(R.drawable.ic_settings_bluetooth,
+ r.getString(R.string.bluetooth_talkback_bluetooth));
+ }
}
diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java
index bd43b9b63a0..f9aa887c0e0 100644
--- a/src/com/android/settings/core/gateway/SettingsGateway.java
+++ b/src/com/android/settings/core/gateway/SettingsGateway.java
@@ -57,6 +57,7 @@ import com.android.settings.applications.UsageAccessDetails;
import com.android.settings.applications.VrListenerSettings;
import com.android.settings.applications.WriteSettingsDetails;
import com.android.settings.applications.assist.ManageAssist;
+import com.android.settings.bluetooth.BluetoothDeviceDetailsFragment;
import com.android.settings.bluetooth.BluetoothSettings;
import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment;
import com.android.settings.dashboard.SupportFragment;
@@ -247,6 +248,7 @@ public class SettingsGateway {
EnterprisePrivacySettings.class.getName(),
WebViewAppPicker.class.getName(),
LockscreenDashboardFragment.class.getName(),
+ BluetoothDeviceDetailsFragment.class.getName(),
};
public static final String[] SETTINGS_FOR_RESTRICTED = {
diff --git a/tests/robotests/assets/grandfather_not_implementing_index_provider b/tests/robotests/assets/grandfather_not_implementing_index_provider
index c6687799fd9..b9e328c347a 100644
--- a/tests/robotests/assets/grandfather_not_implementing_index_provider
+++ b/tests/robotests/assets/grandfather_not_implementing_index_provider
@@ -1,4 +1,5 @@
com.android.settings.bluetooth.DevicePickerFragment
+com.android.settings.bluetooth.BluetoothDeviceDetailsFragment
com.android.settings.bluetooth.BluetoothPairingDetail
com.android.settings.notification.ZenModePrioritySettings
com.android.settings.accounts.AccountDetailDashboardFragment
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsButtonsControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsButtonsControllerTest.java
new file mode 100644
index 00000000000..386601d07e5
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsButtonsControllerTest.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2017 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.widget.Button;
+
+import com.android.settings.R;
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.TestConfig;
+import com.android.settings.applications.LayoutPreference;
+import com.android.settings.testutils.shadow.SettingsShadowBluetoothDevice;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION,
+ shadows=SettingsShadowBluetoothDevice.class)
+public class BluetoothDetailsButtonsControllerTest extends BluetoothDetailsControllerTestBase {
+ private BluetoothDetailsButtonsController mController;
+ private LayoutPreference mLayoutPreference;
+ private Button mLeftButton;
+ private Button mRightButton;
+
+ @Override
+ public void setUp() {
+ super.setUp();
+ mController = new BluetoothDetailsButtonsController(mContext, mFragment, mCachedDevice,
+ mLifecycle);
+ mLeftButton = new Button(mContext);
+ mRightButton = new Button(mContext);
+ mLayoutPreference = new LayoutPreference(mContext, R.layout.app_action_buttons);
+ mLayoutPreference.setKey(mController.getPreferenceKey());
+ mScreen.addPreference(mLayoutPreference);
+ mLeftButton = (Button) mLayoutPreference.findViewById(R.id.left_button);
+ mRightButton = (Button) mLayoutPreference.findViewById(R.id.right_button);
+ setupDevice(mDeviceConfig);
+ when(mCachedDevice.isBusy()).thenReturn(false);
+ }
+
+ @Test
+ public void connected() {
+ showScreen(mController);
+ assertThat(mLeftButton.getText()).isEqualTo(
+ mContext.getString(R.string.bluetooth_device_context_disconnect));
+ assertThat(mRightButton.getText()).isEqualTo(mContext.getString(R.string.forget));
+ }
+
+ @Test
+ public void clickOnDisconnect() {
+ showScreen(mController);
+ mLeftButton.callOnClick();
+ verify(mCachedDevice).disconnect();
+ }
+
+ @Test
+ public void clickOnConnect() {
+ when(mCachedDevice.isConnected()).thenReturn(false);
+ showScreen(mController);
+
+ assertThat(mLeftButton.getText()).isEqualTo(
+ mContext.getString(R.string.bluetooth_device_context_connect));
+
+ mLeftButton.callOnClick();
+ verify(mCachedDevice).connect(eq(true));
+ }
+
+ @Test
+ public void becomeDisconnected() {
+ showScreen(mController);
+ // By default we start out with the device connected.
+ assertThat(mLeftButton.getText()).isEqualTo(
+ mContext.getString(R.string.bluetooth_device_context_disconnect));
+
+ // Now make the device appear to have changed to disconnected.
+ when(mCachedDevice.isConnected()).thenReturn(false);
+ mController.onDeviceAttributesChanged();
+ assertThat(mLeftButton.getText()).isEqualTo(
+ mContext.getString(R.string.bluetooth_device_context_connect));
+
+ // Click the button and make sure that connect (not disconnect) gets called.
+ mLeftButton.callOnClick();
+ verify(mCachedDevice).connect(eq(true));
+ }
+
+ @Test
+ public void becomeConnected() {
+ // Start out with the device disconnected.
+ when(mCachedDevice.isConnected()).thenReturn(false);
+ showScreen(mController);
+
+ assertThat(mLeftButton.getText()).isEqualTo(
+ mContext.getString(R.string.bluetooth_device_context_connect));
+
+ // Now make the device appear to have changed to connected.
+ when(mCachedDevice.isConnected()).thenReturn(true);
+ mController.onDeviceAttributesChanged();
+ assertThat(mLeftButton.getText()).isEqualTo(
+ mContext.getString(R.string.bluetooth_device_context_disconnect));
+
+ // Click the button and make sure that disconnnect (not connect) gets called.
+ mLeftButton.callOnClick();
+ verify(mCachedDevice).disconnect();
+ }
+
+ @Test
+ public void forget() {
+ showScreen(mController);
+ mRightButton.callOnClick();
+ verify(mCachedDevice).unpair();
+ verify(mActivity).finish();
+ }
+
+
+ @Test
+ public void startsOutBusy() {
+ when(mCachedDevice.isBusy()).thenReturn(true);
+ showScreen(mController);
+ assertThat(mLeftButton.getText()).isEqualTo(
+ mContext.getString(R.string.bluetooth_device_context_disconnect));
+ assertThat(mRightButton.getText()).isEqualTo(mContext.getString(R.string.forget));
+ assertThat(mLeftButton.isEnabled()).isFalse();
+
+ // Now pretend the device became non-busy.
+ when(mCachedDevice.isBusy()).thenReturn(false);
+ mController.onDeviceAttributesChanged();
+ assertThat(mLeftButton.isEnabled()).isTrue();
+ }
+
+ @Test
+ public void becomesBusy() {
+ showScreen(mController);
+ assertThat(mLeftButton.isEnabled()).isTrue();
+
+ when(mCachedDevice.isBusy()).thenReturn(true);
+ mController.onDeviceAttributesChanged();
+ assertThat(mLeftButton.isEnabled()).isFalse();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsControllerEventsTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsControllerEventsTest.java
new file mode 100644
index 00000000000..f14a498c42f
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsControllerEventsTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017 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 static org.mockito.Matchers.any;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.support.v14.preference.PreferenceFragment;
+import android.support.v7.preference.PreferenceScreen;
+
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.TestConfig;
+import com.android.settings.testutils.shadow.SettingsShadowBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION,
+ shadows=SettingsShadowBluetoothDevice.class)
+public class BluetoothDetailsControllerEventsTest extends BluetoothDetailsControllerTestBase {
+
+ static class TestController extends BluetoothDetailsController {
+ public TestController(Context context, PreferenceFragment fragment,
+ CachedBluetoothDevice device,
+ Lifecycle lifecycle) {
+ super(context, fragment, device, lifecycle);
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return null;
+ }
+
+ @Override
+ protected void init(PreferenceScreen screen) {}
+
+ @Override
+ protected void refresh() {}
+ }
+
+ @Test
+ public void pauseResumeEvents() {
+
+ TestController controller = spy(new TestController(mContext, mFragment, mCachedDevice,
+ mLifecycle));
+ verify(mLifecycle).addObserver(any(BluetoothDetailsController.class));
+
+ showScreen(controller);
+ verify(mCachedDevice, times(1)).registerCallback(controller);
+ verify(controller, times(1)).refresh();
+
+ controller.onPause();
+ verify(controller, times(1)).refresh();
+ verify(mCachedDevice).unregisterCallback(controller);
+
+ controller.onResume();
+ verify(controller, times(2)).refresh();
+ verify(mCachedDevice, times(2)).registerCallback(controller);
+
+ // The init function should only have been called once
+ verify(controller, times(1)).init(mScreen);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsControllerTestBase.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsControllerTestBase.java
new file mode 100644
index 00000000000..95befa9ce85
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsControllerTestBase.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2017 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 static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.support.v7.preference.PreferenceManager;
+import android.support.v7.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import org.junit.Before;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+
+public class BluetoothDetailsControllerTestBase {
+ protected Context mContext = RuntimeEnvironment.application;
+ protected Lifecycle mLifecycle;
+ protected DeviceConfig mDeviceConfig;
+ protected BluetoothDevice mDevice;
+ protected BluetoothManager mBluetoothManager;
+ protected BluetoothAdapter mBluetoothAdapter;
+ protected PreferenceScreen mScreen;
+ protected PreferenceManager mPreferenceManager;
+
+ @Mock
+ protected BluetoothDeviceDetailsFragment mFragment;
+ @Mock
+ protected CachedBluetoothDevice mCachedDevice;
+ @Mock
+ protected Activity mActivity;
+ @Mock
+ protected BluetoothClass mBluetoothDeviceClass;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mPreferenceManager = new PreferenceManager(mContext);
+ mScreen = mPreferenceManager.createPreferenceScreen(mContext);
+ mDeviceConfig = makeDefaultDeviceConfig();
+ when(mFragment.getActivity()).thenReturn(mActivity);
+ when(mActivity.getApplicationContext()).thenReturn(mContext);
+ when(mFragment.getContext()).thenReturn(mContext);
+ when(mFragment.getPreferenceManager()).thenReturn(mPreferenceManager);
+ when(mFragment.getPreferenceScreen()).thenReturn(mScreen);
+ mLifecycle = spy(new Lifecycle());
+ mBluetoothManager = new BluetoothManager(mContext);
+ mBluetoothAdapter = mBluetoothManager.getAdapter();
+ }
+
+ protected static class DeviceConfig {
+ private String name;
+ private String address;
+ private int majorDeviceClass;
+ private boolean connected;
+ private int connectionSummary;
+
+ public DeviceConfig setName(String newValue) {
+ this.name = newValue;
+ return this;
+ }
+
+ public DeviceConfig setAddress(String newValue) {
+ this.address = newValue;
+ return this;
+ }
+
+ public DeviceConfig setMajorDeviceClass(int newValue) {
+ this.majorDeviceClass = newValue;
+ return this;
+ }
+
+ public DeviceConfig setConnected(boolean newValue) {
+ this.connected = newValue;
+ return this;
+ }
+ public DeviceConfig setConnectionSummary(int newValue) {
+ this.connectionSummary = newValue;
+ return this;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public int getMajorDeviceClass() {
+ return majorDeviceClass;
+ }
+
+ public boolean isConnected() {
+ return connected;
+ }
+
+ public int getConnectionSummary() {
+ return connectionSummary;
+ }
+ }
+
+ protected static DeviceConfig makeDefaultDeviceConfig() {
+ return new DeviceConfig()
+ .setName("Mock Device")
+ .setAddress("B4:B0:34:B5:3B:1B")
+ .setMajorDeviceClass(BluetoothClass.Device.Major.AUDIO_VIDEO)
+ .setConnected(true)
+ .setConnectionSummary(R.string.bluetooth_connected);
+ }
+
+ /**
+ * Sets up the device mock to return various state based on a test config.
+ * @param config
+ */
+ protected void setupDevice(DeviceConfig config) {
+ when(mCachedDevice.getName()).thenReturn(config.getName());
+ when(mBluetoothDeviceClass.getMajorDeviceClass()).thenReturn(config.getMajorDeviceClass());
+ when(mCachedDevice.isConnected()).thenReturn(config.isConnected());
+ when(mCachedDevice.getConnectionSummary()).thenReturn(config.getConnectionSummary());
+
+ mDevice = mBluetoothAdapter.getRemoteDevice(mDeviceConfig.getAddress());
+ when(mCachedDevice.getDevice()).thenReturn(mDevice);
+ }
+
+ /**
+ * Convenience method to call displayPreference and onResume.
+ */
+ protected void showScreen(BluetoothDetailsController controller) {
+ controller.displayPreference(mScreen);
+ controller.onResume();
+ }
+}
+
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHeaderControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHeaderControllerTest.java
new file mode 100644
index 00000000000..ec82fe8b56f
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHeaderControllerTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2017 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 static org.mockito.Matchers.any;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.drawable.Drawable;
+import android.support.v7.preference.Preference;
+
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.TestConfig;
+import com.android.settings.testutils.FakeFeatureFactory;
+import com.android.settings.testutils.shadow.SettingsShadowBluetoothDevice;
+import com.android.settings.testutils.shadow.ShadowEntityHeaderController;
+import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.R;
+import com.android.settingslib.bluetooth.LocalBluetoothProfile;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION,
+ shadows={SettingsShadowBluetoothDevice.class, ShadowEntityHeaderController.class})
+public class BluetoothDetailsHeaderControllerTest extends BluetoothDetailsControllerTestBase {
+ private BluetoothDetailsHeaderController mController;
+ private Preference mPreference;
+
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private EntityHeaderController mHeaderController;
+
+ @Override
+ public void setUp() {
+ super.setUp();
+ FakeFeatureFactory.setupForTest(spy(mContext));
+ ShadowEntityHeaderController.setUseMock(mHeaderController);
+ mController = new BluetoothDetailsHeaderController(mContext, mFragment, mCachedDevice,
+ mLifecycle);
+ mPreference = new Preference(mContext);
+ mPreference.setKey(mController.getPreferenceKey());
+ mScreen.addPreference(mPreference);
+ setupDevice(mDeviceConfig);
+ }
+
+ @After
+ public void tearDown() {
+ ShadowEntityHeaderController.reset();
+ }
+
+ @Test
+ public void header() {
+ showScreen(mController);
+
+ verify(mHeaderController).setLabel(mDeviceConfig.getName());
+ verify(mHeaderController).setIcon(any(Drawable.class));
+ verify(mHeaderController).setIconContentDescription(any(String.class));
+ verify(mHeaderController).setSummary(any(String.class));
+ verify(mHeaderController).done(mActivity, mContext);
+ verify(mHeaderController).done(mActivity, false);
+ }
+
+ @Test
+ public void connectionStatusChangesWhileScreenOpen() {
+ ArrayList profiles = new ArrayList<>();
+ InOrder inOrder = inOrder(mHeaderController);
+ when(mCachedDevice.getConnectionSummary()).thenReturn(R.string.bluetooth_connected);
+ showScreen(mController);
+ inOrder.verify(mHeaderController).setSummary(mContext.getString(R.string.bluetooth_connected));
+
+ when(mCachedDevice.getConnectionSummary()).thenReturn(0);
+ mController.onDeviceAttributesChanged();
+ inOrder.verify(mHeaderController).setSummary((CharSequence) null);
+
+ when(mCachedDevice.getConnectionSummary()).thenReturn(R.string.bluetooth_connecting);
+ mController.onDeviceAttributesChanged();
+ inOrder.verify(mHeaderController).setSummary(
+ mContext.getString(R.string.bluetooth_connecting));
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsMacAddressControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsMacAddressControllerTest.java
new file mode 100644
index 00000000000..dbd22e111e7
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsMacAddressControllerTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017 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 static com.google.common.truth.Truth.assertThat;
+
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.TestConfig;
+import com.android.settings.testutils.shadow.SettingsShadowBluetoothDevice;
+import com.android.settingslib.widget.FooterPreference;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION,
+ shadows=SettingsShadowBluetoothDevice.class)
+public class BluetoothDetailsMacAddressControllerTest extends BluetoothDetailsControllerTestBase {
+ private BluetoothDetailsMacAddressController mController;
+
+ @Override
+ public void setUp() {
+ super.setUp();
+ mController = new BluetoothDetailsMacAddressController(mContext, mFragment, mCachedDevice,
+ mLifecycle);
+ setupDevice(mDeviceConfig);
+ }
+
+ @Test
+ public void macAddress() {
+ showScreen(mController);
+ FooterPreference footer = (FooterPreference) mScreen.findPreference(
+ mController.getPreferenceKey());
+ assertThat(footer.getTitle().toString()).endsWith(mDeviceConfig.getAddress());
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsProfilesControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsProfilesControllerTest.java
new file mode 100644
index 00000000000..f9834f8a165
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsProfilesControllerTest.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright (C) 2017 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothClass;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.support.v14.preference.SwitchPreference;
+import android.support.v7.preference.PreferenceCategory;
+import android.support.v7.preference.PreferenceManager;
+
+import com.android.settings.R;
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.TestConfig;
+import com.android.settings.testutils.shadow.SettingsShadowBluetoothDevice;
+import com.android.settingslib.bluetooth.A2dpProfile;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.bluetooth.MapProfile;
+import com.android.settingslib.bluetooth.PbapServerProfile;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION,
+ shadows=SettingsShadowBluetoothDevice.class)
+public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsControllerTestBase {
+ private BluetoothDetailsProfilesController mController;
+ private List mConnectableProfiles;
+ private PreferenceCategory mProfiles;
+
+ @Mock
+ private LocalBluetoothManager mLocalManager;
+ @Mock
+ private LocalBluetoothProfileManager mProfileManager;
+
+ @Override
+ public void setUp() {
+ super.setUp();
+
+ mProfiles = spy(new PreferenceCategory(mContext));
+ when(mProfiles.getPreferenceManager()).thenReturn(mPreferenceManager);
+
+ mConnectableProfiles = new ArrayList<>();
+ when(mLocalManager.getProfileManager()).thenReturn(mProfileManager);
+ when(mCachedDevice.getConnectableProfiles()).thenAnswer(invocation ->
+ new ArrayList<>(mConnectableProfiles)
+ );
+
+ setupDevice(mDeviceConfig);
+ mController = new BluetoothDetailsProfilesController(mContext, mFragment, mLocalManager,
+ mCachedDevice, mLifecycle);
+ mProfiles.setKey(mController.getPreferenceKey());
+ mScreen.addPreference(mProfiles);
+ }
+
+ static class FakeBluetoothProfile implements LocalBluetoothProfile {
+ protected HashSet mConnectedDevices;
+ protected HashMap mPreferred;
+ protected Context mContext;
+ protected int mNameResourceId;
+
+ public FakeBluetoothProfile(Context context, int nameResourceId) {
+ mConnectedDevices = new HashSet<>();
+ mPreferred = new HashMap<>();
+ mContext = context;
+ mNameResourceId = nameResourceId;
+ }
+
+ @Override
+ public String toString() {
+ return mContext.getString(mNameResourceId);
+ }
+
+ @Override
+ public boolean isConnectable() {
+ return true;
+ }
+
+ @Override
+ public boolean isAutoConnectable() {
+ return true;
+ }
+
+ @Override
+ public boolean connect(BluetoothDevice device) {
+ mConnectedDevices.add(device);
+ return true;
+ }
+
+ @Override
+ public boolean disconnect(BluetoothDevice device) {
+ mConnectedDevices.remove(device);
+ return false;
+ }
+
+ @Override
+ public int getConnectionStatus(BluetoothDevice device) {
+ if (mConnectedDevices.contains(device)) {
+ return BluetoothProfile.STATE_CONNECTED;
+ } else {
+ return BluetoothProfile.STATE_DISCONNECTED;
+ }
+ }
+
+ @Override
+ public boolean isPreferred(BluetoothDevice device) {
+ return mPreferred.getOrDefault(device, false);
+ }
+
+ @Override
+ public int getPreferred(BluetoothDevice device) {
+ return isPreferred(device) ?
+ BluetoothProfile.PRIORITY_ON : BluetoothProfile.PRIORITY_OFF;
+ }
+
+ @Override
+ public void setPreferred(BluetoothDevice device, boolean preferred) {
+ mPreferred.put(device, preferred);
+ }
+
+ @Override
+ public boolean isProfileReady() {
+ return true;
+ }
+
+ @Override
+ public int getOrdinal() {
+ return 0;
+ }
+
+ @Override
+ public int getNameResource(BluetoothDevice device) {
+ return mNameResourceId;
+ }
+
+ @Override
+ public int getSummaryResourceForDevice(BluetoothDevice device) {
+ return Utils.getConnectionStateSummary(getConnectionStatus(device));
+ }
+
+ @Override
+ public int getDrawableResource(BluetoothClass btClass) {
+ return 0;
+ }
+ }
+
+ /**
+ * Creates and adds a mock LocalBluetoothProfile to the list of connectable profiles for the
+ * device.
+ @param profileNameResId the resource id for the name used by this profile
+ @param deviceIsPreferred whether this profile should start out as enabled for the device
+ */
+ private LocalBluetoothProfile addFakeProfile(int profileNameResId,
+ boolean deviceIsPreferred) {
+ LocalBluetoothProfile profile = new FakeBluetoothProfile(mContext, profileNameResId);
+ profile.setPreferred(mDevice, deviceIsPreferred);
+ mConnectableProfiles.add(profile);
+ when(mProfileManager.getProfileByName(eq(profile.toString()))).thenReturn(profile);
+ return profile;
+ }
+
+ /** Returns the list of SwitchPreference objects added to the screen - there should be one per
+ * Bluetooth profile.
+ */
+ private List getProfileSwitches(boolean expectOnlyMConnectable) {
+ if (expectOnlyMConnectable) {
+ assertThat(mConnectableProfiles).isNotEmpty();
+ assertThat(mProfiles.getPreferenceCount()).isEqualTo(mConnectableProfiles.size());
+ }
+ ArrayList result = new ArrayList<>();
+ for (int i = 0; i < mProfiles.getPreferenceCount(); i++) {
+ result.add((SwitchPreference)mProfiles.getPreference(i));
+ }
+ return result;
+ }
+
+ private void verifyProfileSwitchTitles(List switches) {
+ for (int i = 0; i < switches.size(); i++) {
+ String expectedTitle = mContext.getString(
+ mConnectableProfiles.get(i).getNameResource(mDevice));
+ assertThat(switches.get(i).getTitle()).isEqualTo(expectedTitle);
+ }
+ }
+
+ @Test
+ public void oneProfile() {
+ addFakeProfile(R.string.bluetooth_profile_a2dp, true);
+ showScreen(mController);
+ verifyProfileSwitchTitles(getProfileSwitches(true));
+ }
+
+ @Test
+ public void multipleProfiles() {
+ addFakeProfile(R.string.bluetooth_profile_a2dp, true);
+ addFakeProfile(R.string.bluetooth_profile_headset, false);
+ showScreen(mController);
+ List switches = getProfileSwitches(true);
+ verifyProfileSwitchTitles(switches);
+ assertThat(switches.get(0).isChecked()).isTrue();
+ assertThat(switches.get(1).isChecked()).isFalse();
+
+ // Both switches should be enabled.
+ assertThat(switches.get(0).isEnabled()).isTrue();
+ assertThat(switches.get(1).isEnabled()).isTrue();
+
+ // Make device busy.
+ when(mCachedDevice.isBusy()).thenReturn(true);
+ mController.onDeviceAttributesChanged();
+
+ // There should have been no new switches added.
+ assertThat(mProfiles.getPreferenceCount()).isEqualTo(2);
+
+ // Make sure both switches got disabled.
+ assertThat(switches.get(0).isEnabled()).isFalse();
+ assertThat(switches.get(1).isEnabled()).isFalse();
+ }
+
+ @Test
+ public void disableThenReenableOneProfile() {
+ addFakeProfile(R.string.bluetooth_profile_a2dp, true);
+ addFakeProfile(R.string.bluetooth_profile_headset, true);
+ showScreen(mController);
+ List switches = getProfileSwitches(true);
+ SwitchPreference pref = switches.get(0);
+
+ // Clicking the pref should cause the profile to become not-preferred.
+ assertThat(pref.isChecked()).isTrue();
+ pref.performClick();
+ assertThat(pref.isChecked()).isFalse();
+ assertThat(mConnectableProfiles.get(0).isPreferred(mDevice)).isFalse();
+
+ // Make sure no new preferences were added.
+ assertThat(mProfiles.getPreferenceCount()).isEqualTo(2);
+
+ // Clicking the pref again should make the profile once again preferred.
+ pref.performClick();
+ assertThat(pref.isChecked()).isTrue();
+ assertThat(mConnectableProfiles.get(0).isPreferred(mDevice)).isTrue();
+
+ // Make sure we still haven't gotten any new preferences added.
+ assertThat(mProfiles.getPreferenceCount()).isEqualTo(2);
+ }
+
+ @Test
+ public void disconnectedDeviceOneProfile() {
+ setupDevice(makeDefaultDeviceConfig().setConnected(false).setConnectionSummary(0));
+ addFakeProfile(R.string.bluetooth_profile_a2dp, true);
+ showScreen(mController);
+ verifyProfileSwitchTitles(getProfileSwitches(true));
+ }
+
+ @Test
+ public void pbapProfileStartsEnabled() {
+ setupDevice(makeDefaultDeviceConfig());
+ when(mCachedDevice.getPhonebookPermissionChoice()).thenReturn(
+ CachedBluetoothDevice.ACCESS_ALLOWED);
+ PbapServerProfile psp = mock(PbapServerProfile.class);
+ when(psp.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_pbap);
+ when(psp.toString()).thenReturn(PbapServerProfile.NAME);
+ when(mProfileManager.getPbapProfile()).thenReturn(psp);
+
+ showScreen(mController);
+ List switches = getProfileSwitches(false);
+ assertThat(switches.size()).isEqualTo(1);
+ SwitchPreference pref = switches.get(0);
+ assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_pbap));
+ assertThat(pref.isChecked()).isTrue();
+
+ pref.performClick();
+ assertThat(mProfiles.getPreferenceCount()).isEqualTo(1);
+ verify(mCachedDevice).setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED);
+ }
+
+ @Test
+ public void pbapProfileStartsDisabled() {
+ setupDevice(makeDefaultDeviceConfig());
+ when(mCachedDevice.getPhonebookPermissionChoice()).thenReturn(
+ CachedBluetoothDevice.ACCESS_REJECTED);
+ PbapServerProfile psp = mock(PbapServerProfile.class);
+ when(psp.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_pbap);
+ when(psp.toString()).thenReturn(PbapServerProfile.NAME);
+ when(mProfileManager.getPbapProfile()).thenReturn(psp);
+
+ showScreen(mController);
+ List switches = getProfileSwitches(false);
+ assertThat(switches.size()).isEqualTo(1);
+ SwitchPreference pref = switches.get(0);
+ assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_pbap));
+ assertThat(pref.isChecked()).isFalse();
+
+ pref.performClick();
+ assertThat(mProfiles.getPreferenceCount()).isEqualTo(1);
+ verify(mCachedDevice).setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED);
+ }
+
+ @Test
+ public void mapProfile() {
+ setupDevice(makeDefaultDeviceConfig());
+ MapProfile mapProfile = mock(MapProfile.class);
+ when(mapProfile.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_map);
+ when(mProfileManager.getMapProfile()).thenReturn(mapProfile);
+ when(mProfileManager.getProfileByName(eq(mapProfile.toString()))).thenReturn(mapProfile);
+ when(mCachedDevice.getMessagePermissionChoice()).thenReturn(
+ CachedBluetoothDevice.ACCESS_REJECTED);
+ showScreen(mController);
+ List switches = getProfileSwitches(false);
+ assertThat(switches.size()).isEqualTo(1);
+ SwitchPreference pref = switches.get(0);
+ assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_map));
+ assertThat(pref.isChecked()).isFalse();
+
+ pref.performClick();
+ assertThat(mProfiles.getPreferenceCount()).isEqualTo(1);
+ verify(mCachedDevice).setMessagePermissionChoice(BluetoothDevice.ACCESS_ALLOWED);
+ }
+
+ private A2dpProfile addMockA2dpProfile(boolean preferred, boolean supportsHighQualityAudio,
+ boolean highQualityAudioEnabled) {
+ A2dpProfile profile = mock(A2dpProfile.class);
+ when(mProfileManager.getProfileByName(eq(profile.toString()))).thenReturn(profile);
+ when(profile.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_a2dp);
+ when(profile.getHighQualityAudioOptionLabel(mDevice)).thenReturn(mContext.getString(
+ R.string.bluetooth_profile_a2dp_high_quality_unknown_codec));
+ when(profile.supportsHighQualityAudio(mDevice)).thenReturn(supportsHighQualityAudio);
+ when(profile.isHighQualityAudioEnabled(mDevice)).thenReturn(highQualityAudioEnabled);
+ when(profile.isPreferred(mDevice)).thenReturn(preferred);
+ mConnectableProfiles.add(profile);
+ return profile;
+ }
+
+ private SwitchPreference getHighQualityAudioPref() {
+ return (SwitchPreference) mScreen.findPreference(
+ BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG);
+ }
+
+ @Test
+ public void highQualityAudio_prefIsPresentWhenSupported() {
+ setupDevice(makeDefaultDeviceConfig());
+ addMockA2dpProfile(true, true, true);
+ showScreen(mController);
+ SwitchPreference pref = getHighQualityAudioPref();
+ assertThat(pref.getKey()).isEqualTo(
+ BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG);
+
+ // Make sure the preference works when clicked on.
+ pref.performClick();
+ A2dpProfile profile = (A2dpProfile) mConnectableProfiles.get(0);
+ verify(profile).setHighQualityAudioEnabled(mDevice, false);
+ pref.performClick();
+ verify(profile).setHighQualityAudioEnabled(mDevice, true);
+ }
+
+ @Test
+ public void highQualityAudio_prefIsAbsentWhenNotSupported() {
+ setupDevice(makeDefaultDeviceConfig());
+ addMockA2dpProfile(true, false, false);
+ showScreen(mController);
+ assertThat(mProfiles.getPreferenceCount()).isEqualTo(1);
+ SwitchPreference pref = (SwitchPreference) mProfiles.getPreference(0);
+ assertThat(pref.getKey()).isNotEqualTo(
+ BluetoothDetailsProfilesController.HIGH_QUALITY_AUDIO_PREF_TAG);
+ assertThat(pref.getTitle()).isEqualTo(mContext.getString(R.string.bluetooth_profile_a2dp));
+ }
+
+ @Test
+ public void highQualityAudio_busyDeviceDisablesSwitch() {
+ setupDevice(makeDefaultDeviceConfig());
+ addMockA2dpProfile(true, true, true);
+ when(mCachedDevice.isBusy()).thenReturn(true);
+ showScreen(mController);
+ SwitchPreference pref = getHighQualityAudioPref();
+ assertThat(pref.isEnabled()).isFalse();
+ }
+
+ @Test
+ public void highQualityAudio_mediaAudioDisabledAndReEnabled() {
+ setupDevice(makeDefaultDeviceConfig());
+ A2dpProfile audioProfile = addMockA2dpProfile(true, true, true);
+ showScreen(mController);
+ assertThat(mProfiles.getPreferenceCount()).isEqualTo(2);
+
+ // Disabling media audio should cause the high quality audio switch to disappear, but not
+ // the regular audio one.
+ SwitchPreference audioPref = (SwitchPreference) mScreen.findPreference(
+ audioProfile.toString());
+ audioPref.performClick();
+ verify(audioProfile).setPreferred(mDevice, false);
+ when(audioProfile.isPreferred(mDevice)).thenReturn(false);
+ mController.onDeviceAttributesChanged();
+ assertThat(audioPref.isVisible()).isTrue();
+ SwitchPreference highQualityAudioPref = getHighQualityAudioPref();
+ assertThat(highQualityAudioPref.isVisible()).isFalse();
+
+ // And re-enabling media audio should make high quality switch to reappear.
+ audioPref.performClick();
+ verify(audioProfile).setPreferred(mDevice, true);
+ when(audioProfile.isPreferred(mDevice)).thenReturn(true);
+ mController.onDeviceAttributesChanged();
+ highQualityAudioPref = getHighQualityAudioPref();
+ assertThat(highQualityAudioPref.isVisible()).isTrue();
+ }
+
+ @Test
+ public void highQualityAudio_mediaAudioStartsDisabled() {
+ setupDevice(makeDefaultDeviceConfig());
+ A2dpProfile audioProfile = addMockA2dpProfile(false, true, true);
+ showScreen(mController);
+ SwitchPreference audioPref = (SwitchPreference) mScreen.findPreference(
+ audioProfile.toString());
+ SwitchPreference highQualityAudioPref = getHighQualityAudioPref();
+ assertThat(audioPref).isNotNull();
+ assertThat(audioPref.isChecked()).isFalse();
+ assertThat(highQualityAudioPref).isNotNull();
+ assertThat(highQualityAudioPref.isVisible()).isFalse();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/SettingsShadowBluetoothDevice.java b/tests/robotests/src/com/android/settings/testutils/shadow/SettingsShadowBluetoothDevice.java
new file mode 100644
index 00000000000..00aa9ec8244
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/testutils/shadow/SettingsShadowBluetoothDevice.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017 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.testutils.shadow;
+
+import android.bluetooth.BluetoothDevice;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.robolectric.shadows.ShadowBluetoothDevice;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+@Implements(BluetoothDevice.class)
+public class SettingsShadowBluetoothDevice {
+ private String mAddress;
+
+ public void __constructor__(String address) {
+ mAddress = address;
+ }
+
+ @Implementation
+ public String getAddress() {
+ return mAddress;
+ }
+
+ @Implementation
+ public int hashCode() {
+ return mAddress.hashCode();
+ }
+}