From 04a3b2199ea3fdad291f26a19ee73ad0b56c7c1a Mon Sep 17 00:00:00 2001 From: Antony Sargent Date: Thu, 4 May 2017 15:06:32 -0700 Subject: [PATCH] Add a new Bluetooth device details page Bug: 35877479 Test: make RunSettingsRoboTests The existing behavior is to bring up a dialog with Bluetooth device details with checkboxes for each supported profile. This adds a new page that serves the same purpose with a switch for each profile and a footer containing the MAC address. Whether to use the new page or old dialog is controlled by a flag accessible via BluetoothFeatureProvider. Change-Id: I026c363d4cd33932a84017a67cbef51c258bad10 --- res/xml/bluetooth_device_details_fragment.xml | 29 ++ .../BluetoothDetailsButtonsController.java | 86 ++++ .../bluetooth/BluetoothDetailsController.java | 89 ++++ .../BluetoothDetailsHeaderController.java | 71 +++ .../BluetoothDetailsMacAddressController.java | 58 +++ .../BluetoothDetailsProfilesController.java | 269 +++++++++++ .../BluetoothDeviceDetailsFragment.java | 85 ++++ .../bluetooth/BluetoothDevicePreference.java | 61 +-- .../settings/bluetooth/BluetoothSettings.java | 30 +- src/com/android/settings/bluetooth/Utils.java | 56 +++ .../core/gateway/SettingsGateway.java | 2 + ...randfather_not_implementing_index_provider | 1 + ...BluetoothDetailsButtonsControllerTest.java | 160 +++++++ .../BluetoothDetailsControllerEventsTest.java | 84 ++++ .../BluetoothDetailsControllerTestBase.java | 157 ++++++ .../BluetoothDetailsHeaderControllerTest.java | 104 ++++ ...etoothDetailsMacAddressControllerTest.java | 50 ++ ...luetoothDetailsProfilesControllerTest.java | 450 ++++++++++++++++++ .../shadow/SettingsShadowBluetoothDevice.java | 44 ++ 19 files changed, 1823 insertions(+), 63 deletions(-) create mode 100644 res/xml/bluetooth_device_details_fragment.xml create mode 100644 src/com/android/settings/bluetooth/BluetoothDetailsButtonsController.java create mode 100644 src/com/android/settings/bluetooth/BluetoothDetailsController.java create mode 100644 src/com/android/settings/bluetooth/BluetoothDetailsHeaderController.java create mode 100644 src/com/android/settings/bluetooth/BluetoothDetailsMacAddressController.java create mode 100644 src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java create mode 100644 src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsButtonsControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsControllerEventsTest.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsControllerTestBase.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHeaderControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsMacAddressControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsProfilesControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/testutils/shadow/SettingsShadowBluetoothDevice.java 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(); + } +}