Merge changes Ifb5507cb,Ide044cf9

* changes:
  Update BT header using BT metadata
  Add layout for advanced BT detail header
This commit is contained in:
Lei Yu
2019-01-29 01:29:23 +00:00
committed by Android (Google) Code Review
15 changed files with 501 additions and 9 deletions

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2019 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/entity_header"
style="@style/EntityHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/entity_header_title"
style="@style/TextAppearance.EntityHeaderTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:singleLine="false"
android:ellipsize="marquee"
android:textDirection="locale"/>
<TextView
android:id="@+id/entity_header_summary"
style="@style/TextAppearance.EntityHeaderSummary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="2dp"
android:singleLine="false"
android:ellipsize="marquee"
android:textDirection="locale"
android:text="test_summary"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<include
android:id="@+id/layout_left"
layout="@layout/advanced_bt_entity_sub"/>
<include
android:id="@+id/layout_middle"
layout="@layout/advanced_bt_entity_sub"/>
<include
android:id="@+id/layout_right"
layout="@layout/advanced_bt_entity_sub"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2019 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<ImageView
android:id="@+id/header_icon"
android:layout_width="72dp"
android:layout_height="72dp"
android:scaleType="fitCenter"
android:layout_gravity="center_horizontal"
android:antialias="true"/>
<TextView
android:id="@+id/header_title"
style="@style/TextAppearance.EntityHeaderTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:singleLine="false"
android:ellipsize="marquee"
android:textDirection="locale"
android:layout_marginTop="24dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_gravity="center_horizontal"
android:orientation="horizontal">
<ImageView
android:id="@+id/bt_battery_icon"
android:layout_width="13dp"
android:layout_height="20dp"/>
<TextView
android:id="@+id/bt_battery_summary"
style="@style/TextAppearance.EntityHeaderSummary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_gravity="center_horizontal"/>
</LinearLayout>
</LinearLayout>

View File

@@ -10513,6 +10513,13 @@
<!-- Title for no bluetooth devices in Bluetooth devices slice. [CHAR LIMIT=NONE] -->
<string name="no_bluetooth_devices">No Bluetooth devices</string>
<!-- Title for left bluetooth device. [CHAR LIMIT=NONE] -->
<string name="bluetooth_left_name">Left</string>
<!-- Title for right bluetooth device. [CHAR LIMIT=NONE] -->
<string name="bluetooth_right_name">Right</string>
<!-- Title for middle bluetooth device. [CHAR LIMIT=NONE] -->
<string name="bluetooth_middle_name">Case</string>
<!-- Default title for the settings panel [CHAR LIMIT=NONE] -->
<string name="settings_panel_title">Settings Panel</string>

View File

@@ -23,7 +23,16 @@
android:key="bluetooth_device_header"
android:layout="@layout/settings_entity_header"
android:selectable="false"
settings:allowDividerBelow="true"/>
settings:allowDividerBelow="true"
settings:searchable="false"/>
<com.android.settingslib.widget.LayoutPreference
android:key="advanced_bluetooth_device_header"
android:layout="@layout/advanced_bt_entity_header"
android:selectable="false"
settings:allowDividerBelow="true"
settings:searchable="false"
settings:controller="com.android.settings.bluetooth.AdvancedBluetoothDetailsHeaderController"/>
<com.android.settingslib.widget.ActionButtonsPreference
android:key="action_buttons"

View File

@@ -0,0 +1,134 @@
/*
* Copyright (C) 2019 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.content.Context;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.drawable.IconCompat;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.fuelgauge.BatteryMeterView;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.widget.LayoutPreference;
/**
* This class adds a header with device name and status (connected/disconnected, etc.).
*/
public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceController {
@VisibleForTesting
LayoutPreference mLayoutPreference;
private CachedBluetoothDevice mCachedDevice;
public AdvancedBluetoothDetailsHeaderController(Context context, String prefKey) {
super(context, prefKey);
}
@Override
public int getAvailabilityStatus() {
final boolean unthetheredHeadset = Utils.getBooleanMetaData(mCachedDevice.getDevice(),
BluetoothDevice.METADATA_IS_UNTHETHERED_HEADSET);
return unthetheredHeadset ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mLayoutPreference = screen.findPreference(getPreferenceKey());
mLayoutPreference.setVisible(isAvailable());
refresh();
}
public void init(CachedBluetoothDevice cachedBluetoothDevice) {
mCachedDevice = cachedBluetoothDevice;
}
@VisibleForTesting
void refresh() {
if (mLayoutPreference != null && mCachedDevice != null) {
final TextView title = mLayoutPreference.findViewById(R.id.entity_header_title);
title.setText(mCachedDevice.getName());
final TextView summary = mLayoutPreference.findViewById(R.id.entity_header_summary);
summary.setText(mCachedDevice.getConnectionSummary());
updateSubLayout(mLayoutPreference.findViewById(R.id.layout_left),
BluetoothDevice.METADATA_UNTHETHERED_LEFT_ICON,
BluetoothDevice.METADATA_UNTHETHERED_LEFT_BATTERY,
R.string.bluetooth_left_name);
updateSubLayout(mLayoutPreference.findViewById(R.id.layout_middle),
BluetoothDevice.METADATA_UNTHETHERED_CASE_ICON,
BluetoothDevice.METADATA_UNTHETHERED_CASE_BATTERY,
R.string.bluetooth_middle_name);
updateSubLayout(mLayoutPreference.findViewById(R.id.layout_right),
BluetoothDevice.METADATA_UNTHETHERED_RIGHT_ICON,
BluetoothDevice.METADATA_UNTHETHERED_RIGHT_BATTERY,
R.string.bluetooth_right_name);
}
}
@VisibleForTesting
Drawable createBtBatteryIcon(Context context, int level) {
final BatteryMeterView.BatteryMeterDrawable drawable =
new BatteryMeterView.BatteryMeterDrawable(context,
context.getColor(R.color.meter_background_color));
drawable.setBatteryLevel(level);
drawable.setShowPercent(false);
drawable.setBatteryColorFilter(new PorterDuffColorFilter(
com.android.settings.Utils.getColorAttrDefaultColor(context,
android.R.attr.colorControlNormal),
PorterDuff.Mode.SRC_IN));
return drawable;
}
private void updateSubLayout(LinearLayout linearLayout, int iconMetaKey, int batteryMetaKey,
int titleResId) {
if (linearLayout == null) {
return;
}
final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice();
final String iconUri = Utils.getStringMetaData(bluetoothDevice, iconMetaKey);
if (iconUri != null) {
final ImageView imageView = linearLayout.findViewById(R.id.header_icon);
final IconCompat iconCompat = IconCompat.createWithContentUri(iconUri);
imageView.setImageBitmap(iconCompat.getBitmap());
}
final int batteryLevel = Utils.getIntMetaData(bluetoothDevice, batteryMetaKey);
if (batteryLevel != Utils.META_INT_ERROR) {
final ImageView imageView = linearLayout.findViewById(R.id.bt_battery_icon);
imageView.setImageDrawable(createBtBatteryIcon(mContext, batteryLevel));
final TextView textView = linearLayout.findViewById(R.id.bt_battery_summary);
textView.setText(com.android.settings.Utils.formatPercentage(batteryLevel));
}
final TextView textView = linearLayout.findViewById(R.id.header_title);
textView.setText(titleResId);
}
}

View File

@@ -16,6 +16,7 @@
package com.android.settings.bluetooth;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import androidx.preference.PreferenceFragmentCompat;
@@ -43,6 +44,13 @@ public class BluetoothDetailsButtonsController extends BluetoothDetailsControlle
mIsConnected = device.isConnected();
}
@Override
public boolean isAvailable() {
final boolean unthetheredHeadset = Utils.getBooleanMetaData(mCachedDevice.getDevice(),
BluetoothDevice.METADATA_IS_UNTHETHERED_HEADSET);
return !unthetheredHeadset;
}
private void onForgetButtonPressed() {
ForgetDeviceDialogFragment fragment =
ForgetDeviceDialogFragment.newInstance(mCachedDevice.getAddress());
@@ -90,4 +98,4 @@ public class BluetoothDetailsButtonsController extends BluetoothDetailsControlle
return KEY_ACTION_BUTTONS;
}
}
}

View File

@@ -89,4 +89,4 @@ public abstract class BluetoothDetailsController extends AbstractPreferenceContr
* should update the preferences it manages based on the new state.
*/
protected abstract void refresh();
}
}

View File

@@ -62,7 +62,8 @@ public class BluetoothDetailsHeaderController extends BluetoothDetailsController
protected void setHeaderProperties() {
final Pair<Drawable, String> pair = BluetoothUtils
.getBtClassDrawableWithDescription(mContext, mCachedDevice,
mContext.getResources().getFraction(R.fraction.bt_battery_scale_fraction, 1, 1));
mContext.getResources().getFraction(R.fraction.bt_battery_scale_fraction, 1,
1));
String summaryText = mCachedDevice.getConnectionSummary();
// If both the hearing aids are connected, two device status should be shown.
// If Second Summary is unavailable, to set it to null.
@@ -84,4 +85,4 @@ public class BluetoothDetailsHeaderController extends BluetoothDetailsController
public String getPreferenceKey() {
return KEY_DEVICE_HEADER;
}
}
}

View File

@@ -62,4 +62,4 @@ public class BluetoothDetailsMacAddressController extends BluetoothDetailsContro
}
return mFooterPreference.getKey();
}
}
}

View File

@@ -264,4 +264,4 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
public String getPreferenceKey() {
return KEY_PROFILES_GROUP;
}
}
}

View File

@@ -108,6 +108,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
mManager = getLocalBluetoothManager(context);
mCachedDevice = getCachedDevice(mDeviceAddress);
super.onAttach(context);
use(AdvancedBluetoothDetailsHeaderController.class).init(mCachedDevice);
final BluetoothFeatureProvider featureProvider = FeatureFactory.getFactory(
context).getBluetoothFeatureProvider(context);

View File

@@ -47,6 +47,8 @@ public final class Utils {
static final boolean V = BluetoothUtils.V; // verbose logging
static final boolean D = BluetoothUtils.D; // regular logging
public static final int META_INT_ERROR = -1;
private Utils() {
}
@@ -152,4 +154,29 @@ public final class Utils {
return Settings.Global.getInt(context.getContentResolver(),
Settings.Global.BLE_SCAN_ALWAYS_AVAILABLE, 0) == 1;
}
public static boolean getBooleanMetaData(BluetoothDevice bluetoothDevice, int key) {
if (bluetoothDevice == null) {
return false;
}
return Boolean.parseBoolean(bluetoothDevice.getMetadata(key));
}
public static String getStringMetaData(BluetoothDevice bluetoothDevice, int key) {
if (bluetoothDevice == null) {
return null;
}
return bluetoothDevice.getMetadata(key);
}
public static int getIntMetaData(BluetoothDevice bluetoothDevice, int key) {
if (bluetoothDevice == null) {
return META_INT_ERROR;
}
try {
return Integer.parseInt(bluetoothDevice.getMetadata(key));
} catch (NumberFormatException e) {
return META_INT_ERROR;
}
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright (C) 2019 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.Mockito.when;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.fuelgauge.BatteryMeterView;
import com.android.settings.testutils.shadow.ShadowEntityHeaderController;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.widget.LayoutPreference;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = ShadowEntityHeaderController.class)
public class AdvancedBluetoothDetailsHeaderControllerTest{
private static final int BATTERY_LEVEL_MAIN = 30;
private static final int BATTERY_LEVEL_LEFT = 25;
private static final int BATTERY_LEVEL_RIGHT = 45;
private Context mContext;
@Mock
private BluetoothDevice mBluetoothDevice;
@Mock
private CachedBluetoothDevice mCachedDevice;
private AdvancedBluetoothDetailsHeaderController mController;
private LayoutPreference mLayoutPreference;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
mController = new AdvancedBluetoothDetailsHeaderController(mContext, "pref_Key");
mController.init(mCachedDevice);
mLayoutPreference = new LayoutPreference(mContext,
LayoutInflater.from(mContext).inflate(R.layout.advanced_bt_entity_header, null));
mController.mLayoutPreference = mLayoutPreference;
when(mCachedDevice.getDevice()).thenReturn(mBluetoothDevice);
}
@Test
public void createBatteryIcon_hasCorrectInfo() {
final Drawable drawable = mController.createBtBatteryIcon(mContext, BATTERY_LEVEL_MAIN);
assertThat(drawable).isInstanceOf(BatteryMeterView.BatteryMeterDrawable.class);
final BatteryMeterView.BatteryMeterDrawable iconDrawable =
(BatteryMeterView.BatteryMeterDrawable) drawable;
assertThat(iconDrawable.getBatteryLevel()).isEqualTo(BATTERY_LEVEL_MAIN);
}
@Test
public void refresh_updateCorrectInfo() {
when(mBluetoothDevice.getMetadata(
BluetoothDevice.METADATA_UNTHETHERED_LEFT_BATTERY)).thenReturn(
String.valueOf(BATTERY_LEVEL_LEFT));
when(mBluetoothDevice.getMetadata(
BluetoothDevice.METADATA_UNTHETHERED_RIGHT_BATTERY)).thenReturn(
String.valueOf(BATTERY_LEVEL_RIGHT));
when(mBluetoothDevice.getMetadata(
BluetoothDevice.METADATA_UNTHETHERED_CASE_BATTERY)).thenReturn(
String.valueOf(BATTERY_LEVEL_MAIN));
mController.refresh();
assertBatteryLevel(mLayoutPreference.findViewById(R.id.layout_left), BATTERY_LEVEL_LEFT);
assertBatteryLevel(mLayoutPreference.findViewById(R.id.layout_right), BATTERY_LEVEL_RIGHT);
assertBatteryLevel(mLayoutPreference.findViewById(R.id.layout_middle), BATTERY_LEVEL_MAIN);
}
@Test
public void getAvailabilityStatus_unthetheredHeadset_returnAvailable() {
when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_IS_UNTHETHERED_HEADSET))
.thenReturn("true");
assertThat(mController.getAvailabilityStatus()).isEqualTo(
BasePreferenceController.AVAILABLE);
}
@Test
public void getAvailabilityStatus_notUnthetheredHeadset_returnUnavailable() {
when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_IS_UNTHETHERED_HEADSET))
.thenReturn("false");
assertThat(mController.getAvailabilityStatus()).isEqualTo(
BasePreferenceController.CONDITIONALLY_UNAVAILABLE);
}
private void assertBatteryLevel(LinearLayout linearLayout, int batteryLevel) {
final TextView textView = linearLayout.findViewById(R.id.bt_battery_summary);
assertThat(textView.getText().toString()).isEqualTo(
com.android.settings.Utils.formatPercentage(batteryLevel));
}
}

View File

@@ -50,7 +50,6 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = ShadowEntityHeaderController.class)
public class BluetoothDetailsHeaderControllerTest extends BluetoothDetailsControllerTestBase {

View File

@@ -15,6 +15,8 @@
*/
package com.android.settings.bluetooth;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
@@ -22,6 +24,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -40,23 +43,68 @@ import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public class UtilsTest {
private static final String STRING_METADATA = "string_metadata";
private static final String BOOL_METADATA = "true";
private static final String INT_METADATA = "25";
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Context mContext;
@Mock
private BluetoothDevice mBluetoothDevice;
private MetricsFeatureProvider mMetricsFeatureProvider;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mMetricsFeatureProvider = FakeFeatureFactory.setupForTest().getMetricsFeatureProvider();
}
@Test
public void testShowConnectingError_shouldLogBluetoothConnectError() {
public void showConnectingError_shouldLogBluetoothConnectError() {
when(mContext.getString(anyInt(), anyString())).thenReturn("testMessage");
Utils.showConnectingError(mContext, "testName", mock(LocalBluetoothManager.class));
verify(mMetricsFeatureProvider).visible(eq(mContext), anyInt(),
eq(MetricsEvent.ACTION_SETTINGS_BLUETOOTH_CONNECT_ERROR));
}
@Test
public void getStringMetaData_hasMetaData_getCorrectMetaData() {
when(mBluetoothDevice.getMetadata(
BluetoothDevice.METADATA_UNTHETHERED_LEFT_ICON)).thenReturn(STRING_METADATA);
assertThat(Utils.getStringMetaData(mBluetoothDevice,
BluetoothDevice.METADATA_UNTHETHERED_LEFT_ICON)).isEqualTo(STRING_METADATA);
}
@Test
public void getIntMetaData_hasMetaData_getCorrectMetaData() {
when(mBluetoothDevice.getMetadata(
BluetoothDevice.METADATA_UNTHETHERED_LEFT_BATTERY)).thenReturn(INT_METADATA);
assertThat(Utils.getIntMetaData(mBluetoothDevice,
BluetoothDevice.METADATA_UNTHETHERED_LEFT_BATTERY))
.isEqualTo(Integer.parseInt(INT_METADATA));
}
@Test
public void getIntMetaData_invalidMetaData_getErrorCode() {
when(mBluetoothDevice.getMetadata(
BluetoothDevice.METADATA_UNTHETHERED_LEFT_BATTERY)).thenReturn(STRING_METADATA);
assertThat(Utils.getIntMetaData(mBluetoothDevice,
BluetoothDevice.METADATA_UNTHETHERED_LEFT_ICON)).isEqualTo(Utils.META_INT_ERROR);
}
@Test
public void getBooleanMetaData_hasMetaData_getCorrectMetaData() {
when(mBluetoothDevice.getMetadata(
BluetoothDevice.METADATA_IS_UNTHETHERED_HEADSET)).thenReturn(BOOL_METADATA);
assertThat(Utils.getBooleanMetaData(mBluetoothDevice,
BluetoothDevice.METADATA_IS_UNTHETHERED_HEADSET)).isEqualTo(true);
}
}