diff --git a/res/layout/advanced_bt_entity_sub.xml b/res/layout/advanced_bt_entity_sub.xml index 0c9374fdc7c..3f1b3d37f9a 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"/> + + \ No newline at end of file diff --git a/res/values/config.xml b/res/values/config.xml index 3c58a0618c8..72fbdf27d5d 100755 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -467,4 +467,7 @@ false + + + diff --git a/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java b/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java index a147656f6c2..1ab3a6545aa 100644 --- a/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java +++ b/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java @@ -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) { diff --git a/tests/robotests/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderControllerTest.java index 5c4e03d8238..4d2ad369ba6 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.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(