Implement advanced device battery prediction

This CL implements prediction of advanced device to
let users know what time their advanced device will
be out of battery.

Bug: 153706138
Test: make -j42 SettingsGoogle
Change-Id: Iadf2f1fa425ff5f0fa1abed681d82d13c392db62
This commit is contained in:
Hugh Chen
2020-09-10 15:24:23 +08:00
parent f0cdd9cdb4
commit fa75a469da
4 changed files with 168 additions and 7 deletions

View File

@@ -64,4 +64,15 @@
android:layout_marginStart="4dp"/>
</LinearLayout>
<TextView
android:id="@+id/bt_battery_prediction"
style="@style/TextAppearance.EntityHeaderSummary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:orientation="horizontal"
android:visibility="gone"/>
</LinearLayout>

View File

@@ -467,4 +467,7 @@
<!-- Whether to show the Preference for Adaptive connectivity -->
<bool name="config_show_adaptive_connectivity">false</bool>
<!-- Authority of advanced device battery prediction -->
<string name="config_battery_prediction_authority" translatable="false"></string>
</resources>

View File

@@ -18,8 +18,10 @@ package com.android.settings.bluetooth;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
@@ -49,12 +51,14 @@ import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnDestroy;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
import com.android.settingslib.utils.StringUtil;
import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.LayoutPreference;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* This class adds a header with device name and status (connected/disconnected, etc.).
@@ -64,7 +68,22 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont
private static final String TAG = "AdvancedBtHeaderCtrl";
private static final int LOW_BATTERY_LEVEL = 15;
private static final int CASE_LOW_BATTERY_LEVEL = 19;
private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final String PATH = "time_remaining";
private static final String QUERY_PARAMETER_ADDRESS = "address";
private static final String QUERY_PARAMETER_BATTERY_ID = "battery_id";
private static final String QUERY_PARAMETER_BATTERY_LEVEL = "battery_level";
private static final String QUERY_PARAMETER_TIMESTAMP = "timestamp";
private static final String BATTERY_ESTIMATE = "battery_estimate";
private static final String ESTIMATE_READY = "estimate_ready";
private static final String DATABASE_ID = "id";
private static final String DATABASE_BLUETOOTH = "Bluetooth";
private static final long TIME_OF_HOUR = TimeUnit.SECONDS.toMillis(3600);
private static final long TIME_OF_MINUTE = TimeUnit.SECONDS.toMillis(60);
private static final int LEFT_DEVICE_ID = 1;
private static final int RIGHT_DEVICE_ID = 2;
private static final int CASE_DEVICE_ID = 3;
@VisibleForTesting
LayoutPreference mLayoutPreference;
@@ -168,19 +187,22 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont
BluetoothDevice.METADATA_UNTETHERED_LEFT_ICON,
BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY,
BluetoothDevice.METADATA_UNTETHERED_LEFT_CHARGING,
R.string.bluetooth_left_name);
R.string.bluetooth_left_name,
LEFT_DEVICE_ID);
updateSubLayout(mLayoutPreference.findViewById(R.id.layout_middle),
BluetoothDevice.METADATA_UNTETHERED_CASE_ICON,
BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY,
BluetoothDevice.METADATA_UNTETHERED_CASE_CHARGING,
R.string.bluetooth_middle_name);
R.string.bluetooth_middle_name,
CASE_DEVICE_ID);
updateSubLayout(mLayoutPreference.findViewById(R.id.layout_right),
BluetoothDevice.METADATA_UNTETHERED_RIGHT_ICON,
BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY,
BluetoothDevice.METADATA_UNTETHERED_RIGHT_CHARGING,
R.string.bluetooth_right_name);
R.string.bluetooth_right_name,
RIGHT_DEVICE_ID);
}
}
@@ -204,7 +226,7 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont
}
private void updateSubLayout(LinearLayout linearLayout, int iconMetaKey, int batteryMetaKey,
int chargeMetaKey, int titleResId) {
int chargeMetaKey, int titleResId, int batteryId) {
if (linearLayout == null) {
return;
}
@@ -217,11 +239,15 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont
final int batteryLevel = BluetoothUtils.getIntMetaData(bluetoothDevice, batteryMetaKey);
final boolean charging = BluetoothUtils.getBooleanMetaData(bluetoothDevice, chargeMetaKey);
if (DBG) {
if (DEBUG) {
Log.d(TAG, "updateSubLayout() icon : " + iconMetaKey + ", battery : " + batteryMetaKey
+ ", charge : " + chargeMetaKey + ", batteryLevel : " + batteryLevel
+ ", charging : " + charging + ", iconUri : " + iconUri);
}
if (batteryId != CASE_DEVICE_ID) {
showBatteryPredictionIfNecessary(linearLayout, batteryId, batteryLevel);
}
if (batteryLevel != BluetoothUtils.META_INT_ERROR) {
linearLayout.setVisibility(View.VISIBLE);
final TextView textView = linearLayout.findViewById(R.id.bt_battery_summary);
@@ -238,6 +264,64 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont
textView.setVisibility(View.VISIBLE);
}
private void showBatteryPredictionIfNecessary(LinearLayout linearLayout, int batteryId,
int batteryLevel) {
ThreadUtils.postOnBackgroundThread(() -> {
final Uri contentUri = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(mContext.getString(R.string.config_battery_prediction_authority))
.appendPath(PATH)
.appendPath(DATABASE_ID)
.appendPath(DATABASE_BLUETOOTH)
.appendQueryParameter(QUERY_PARAMETER_ADDRESS, mCachedDevice.getAddress())
.appendQueryParameter(QUERY_PARAMETER_BATTERY_ID, String.valueOf(batteryId))
.appendQueryParameter(QUERY_PARAMETER_BATTERY_LEVEL,
String.valueOf(batteryLevel))
.appendQueryParameter(QUERY_PARAMETER_TIMESTAMP,
String.valueOf(System.currentTimeMillis()))
.build();
final String[] columns = new String[] {BATTERY_ESTIMATE, ESTIMATE_READY};
final Cursor cursor =
mContext.getContentResolver().query(contentUri, columns, null, null, null);
if (cursor == null) {
Log.w(TAG, "showBatteryPredictionIfNecessary() cursor is null!");
return;
}
try {
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
final int estimateReady =
cursor.getInt(cursor.getColumnIndex(ESTIMATE_READY));
final long batteryEstimate =
cursor.getLong(cursor.getColumnIndex(BATTERY_ESTIMATE));
if (DEBUG) {
Log.d(TAG, "showBatteryTimeIfNecessary() batteryId : " + batteryId
+ ", ESTIMATE_READY : " + estimateReady
+ ", BATTERY_ESTIMATE : " + batteryEstimate);
}
showBatteryPredictionIfNecessary(estimateReady, batteryEstimate,
linearLayout);
}
} finally {
cursor.close();
}
});
}
@VisibleForTesting
void showBatteryPredictionIfNecessary(int estimateReady, long batteryEstimate,
LinearLayout linearLayout) {
ThreadUtils.postOnMainThread(() -> {
final TextView textView = linearLayout.findViewById(R.id.bt_battery_prediction);
if (estimateReady == 1) {
textView.setVisibility(View.VISIBLE);
textView.setText(StringUtil.formatElapsedTime(mContext, batteryEstimate, false));
} else {
textView.setVisibility(View.GONE);
}
});
}
private void showBatteryIcon(LinearLayout linearLayout, int level, boolean charging,
int batteryMetaKey) {
final int lowBatteryLevel =
@@ -279,7 +363,7 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont
final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice();
final String iconUri = BluetoothUtils.getStringMetaData(bluetoothDevice,
BluetoothDevice.METADATA_MAIN_ICON);
if (DBG) {
if (DEBUG) {
Log.d(TAG, "updateDisconnectLayout() iconUri : " + iconUri);
}
if (iconUri != null) {

View File

@@ -42,6 +42,7 @@ import com.android.settings.fuelgauge.BatteryMeterView;
import com.android.settings.testutils.shadow.ShadowDeviceConfig;
import com.android.settings.testutils.shadow.ShadowEntityHeaderController;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.utils.StringUtil;
import com.android.settingslib.widget.LayoutPreference;
import org.junit.Before;
@@ -285,6 +286,68 @@ public class AdvancedBluetoothDetailsHeaderControllerTest {
verify(mBitmap).recycle();
}
@Test
public void showBatteryPredictionIfNecessary_estimateReadyIsAvailable_showView() {
mController.showBatteryPredictionIfNecessary(1, 14218009,
mLayoutPreference.findViewById(R.id.layout_left));
mController.showBatteryPredictionIfNecessary(1, 14218009,
mLayoutPreference.findViewById(R.id.layout_middle));
mController.showBatteryPredictionIfNecessary(1, 14218009,
mLayoutPreference.findViewById(R.id.layout_right));
assertBatteryPredictionVisible(mLayoutPreference.findViewById(R.id.layout_left),
View.VISIBLE);
assertBatteryPredictionVisible(mLayoutPreference.findViewById(R.id.layout_middle),
View.VISIBLE);
assertBatteryPredictionVisible(mLayoutPreference.findViewById(R.id.layout_right),
View.VISIBLE);
}
@Test
public void showBatteryPredictionIfNecessary_estimateReadyIsNotAvailable_notShowView() {
mController.showBatteryPredictionIfNecessary(0, 14218009,
mLayoutPreference.findViewById(R.id.layout_left));
mController.showBatteryPredictionIfNecessary(0, 14218009,
mLayoutPreference.findViewById(R.id.layout_middle));
mController.showBatteryPredictionIfNecessary(0, 14218009,
mLayoutPreference.findViewById(R.id.layout_right));
assertBatteryPredictionVisible(mLayoutPreference.findViewById(R.id.layout_left),
View.GONE);
assertBatteryPredictionVisible(mLayoutPreference.findViewById(R.id.layout_middle),
View.GONE);
assertBatteryPredictionVisible(mLayoutPreference.findViewById(R.id.layout_right),
View.GONE);
}
@Test
public void showBatteryPredictionIfNecessary_estimateReadyIsAvailable_showCorrectValue() {
final String leftBatteryPrediction =
StringUtil.formatElapsedTime(mContext, 12000000, false).toString();
final String rightBatteryPrediction =
StringUtil.formatElapsedTime(mContext, 1200000, false).toString();
mController.showBatteryPredictionIfNecessary(1, 12000000,
mLayoutPreference.findViewById(R.id.layout_left));
mController.showBatteryPredictionIfNecessary(1, 1200000,
mLayoutPreference.findViewById(R.id.layout_right));
assertBatteryPrediction(mLayoutPreference.findViewById(R.id.layout_left),
leftBatteryPrediction);
assertBatteryPrediction(mLayoutPreference.findViewById(R.id.layout_right),
rightBatteryPrediction);
}
private void assertBatteryPredictionVisible(LinearLayout linearLayout, int visible) {
final TextView textView = linearLayout.findViewById(R.id.bt_battery_prediction);
assertThat(textView.getVisibility()).isEqualTo(visible);
}
private void assertBatteryPrediction(LinearLayout linearLayout, String prediction) {
final TextView textView = linearLayout.findViewById(R.id.bt_battery_prediction);
assertThat(textView.getText().toString()).isEqualTo(prediction);
}
private void assertBatteryLevel(LinearLayout linearLayout, int batteryLevel) {
final TextView textView = linearLayout.findViewById(R.id.bt_battery_summary);
assertThat(textView.getText().toString()).isEqualTo(