Merge "Implement advanced device battery prediction"
diff --git a/res/layout/advanced_bt_entity_sub.xml b/res/layout/advanced_bt_entity_sub.xml
index 0c9374f..3f1b3d3 100644
--- a/res/layout/advanced_bt_entity_sub.xml
+++ b/res/layout/advanced_bt_entity_sub.xml
@@ -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>
\ No newline at end of file
diff --git a/res/values/config.xml b/res/values/config.xml
index 3c58a06..72fbdf2 100755
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -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>
diff --git a/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java b/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java
index a147656..1ab3a65 100644
--- a/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java
+++ b/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java
@@ -18,8 +18,10 @@
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.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 @@
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 @@
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 @@
}
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 @@
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 @@
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 @@
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) {
diff --git a/tests/robotests/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderControllerTest.java
index 5c4e03d..4d2ad36 100644
--- a/tests/robotests/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderControllerTest.java
+++ b/tests/robotests/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderControllerTest.java
@@ -42,6 +42,7 @@
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 @@
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(