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