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
This commit is contained in:
Antony Sargent
2017-05-04 15:06:32 -07:00
parent 8d9177a06e
commit 04a3b2199e
19 changed files with 1823 additions and 63 deletions

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
android:title="@string/bluetooth_device_advanced_title">
<com.android.settings.applications.LayoutPreference
android:key="action_buttons"
android:layout="@layout/app_action_buttons"
android:selectable="false"/>
<PreferenceCategory
android:key="bluetooth_profiles"/>
</PreferenceScreen>

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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<Integer, String> 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;
}
}

View File

@@ -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();
}
}

View File

@@ -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<LocalBluetoothProfile> getProfiles() {
List<LocalBluetoothProfile> 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;
}
}

View File

@@ -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<PreferenceController> getPreferenceControllers(Context context) {
ArrayList<PreferenceController> 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;
}
}

View File

@@ -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<Integer, String> pair = getBtClassDrawableWithDescription();
Pair<Integer, String> 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<Integer, String> getBtClassDrawableWithDescription() {
BluetoothClass btClass = mCachedDevice.getBtClass();
if (btClass != null) {
switch (btClass.getMajorDeviceClass()) {
case BluetoothClass.Device.Major.COMPUTER:
return new Pair<Integer, String>(R.drawable.ic_bt_laptop, COMPUTER);
case BluetoothClass.Device.Major.PHONE:
return new Pair<Integer, String>(R.drawable.ic_bt_cellphone, PHONE);
case BluetoothClass.Device.Major.PERIPHERAL:
return new Pair<Integer, String>(HidProfile.getHidClassDrawable(btClass),
INPUT_PERIPHERAL);
case BluetoothClass.Device.Major.IMAGING:
return new Pair<Integer, String>(R.drawable.ic_bt_imaging, IMAGING);
default:
// unrecognized device class; continue
}
} else {
Log.w(TAG, "mBtClass is null");
}
List<LocalBluetoothProfile> profiles = mCachedDevice.getProfiles();
for (LocalBluetoothProfile profile : profiles) {
int resId = profile.getDrawableResource(btClass);
if (resId != 0) {
return new Pair<Integer, String>(resId, null);
}
}
if (btClass != null) {
if (btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
return new Pair<Integer, String>(R.drawable.ic_bt_headset_hfp, HEADSET);
}
if (btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
return new Pair<Integer, String>(R.drawable.ic_bt_headphones_a2dp, HEADPHONE);
}
}
return new Pair<Integer, String>(R.drawable.ic_settings_bluetooth, BLUETOOTH);
}
}

View File

@@ -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();
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);
}
};
/**

View File

@@ -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<Integer, String> getBtClassDrawableWithDescription(Resources r,
CachedBluetoothDevice cachedDevice) {
BluetoothClass btClass = cachedDevice.getBtClass();
if (btClass != null) {
switch (btClass.getMajorDeviceClass()) {
case BluetoothClass.Device.Major.COMPUTER:
return new Pair<Integer, String>(R.drawable.ic_bt_laptop,
r.getString(R.string.bluetooth_talkback_computer));
case BluetoothClass.Device.Major.PHONE:
return new Pair<Integer, String>(R.drawable.ic_bt_cellphone,
r.getString(R.string.bluetooth_talkback_phone));
case BluetoothClass.Device.Major.PERIPHERAL:
return new Pair<Integer, String>(HidProfile.getHidClassDrawable(btClass),
r.getString(
R.string.bluetooth_talkback_input_peripheral));
case BluetoothClass.Device.Major.IMAGING:
return new Pair<Integer, String>(R.drawable.ic_bt_imaging,
r.getString(R.string.bluetooth_talkback_imaging));
default:
// unrecognized device class; continue
}
}
List<LocalBluetoothProfile> profiles = cachedDevice.getProfiles();
for (LocalBluetoothProfile profile : profiles) {
int resId = profile.getDrawableResource(btClass);
if (resId != 0) {
return new Pair<Integer, String>(resId, null);
}
}
if (btClass != null) {
if (btClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
return new Pair<Integer, String>(R.drawable.ic_bt_headset_hfp,
r.getString(R.string.bluetooth_talkback_headset));
}
if (btClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
return new Pair<Integer, String>(R.drawable.ic_bt_headphones_a2dp,
r.getString(R.string.bluetooth_talkback_headphone));
}
}
return new Pair<Integer, String>(R.drawable.ic_settings_bluetooth,
r.getString(R.string.bluetooth_talkback_bluetooth));
}
}

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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<LocalBluetoothProfile> 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));
}
}

View File

@@ -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());
}
}

View File

@@ -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<LocalBluetoothProfile> 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<BluetoothDevice> mConnectedDevices;
protected HashMap<BluetoothDevice, Boolean> 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<SwitchPreference> getProfileSwitches(boolean expectOnlyMConnectable) {
if (expectOnlyMConnectable) {
assertThat(mConnectableProfiles).isNotEmpty();
assertThat(mProfiles.getPreferenceCount()).isEqualTo(mConnectableProfiles.size());
}
ArrayList<SwitchPreference> result = new ArrayList<>();
for (int i = 0; i < mProfiles.getPreferenceCount(); i++) {
result.add((SwitchPreference)mProfiles.getPreference(i));
}
return result;
}
private void verifyProfileSwitchTitles(List<SwitchPreference> 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<SwitchPreference> 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<SwitchPreference> 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<SwitchPreference> 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<SwitchPreference> 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<SwitchPreference> 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();
}
}

View File

@@ -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();
}
}