Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2489494c authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Implement advanced device battery prediction"

parents 2ac45dd4 fa75a469
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -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
+3 −0
Original line number Diff line number Diff line
@@ -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>
+91 −7
Original line number Diff line number Diff line
@@ -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) {
+63 −0
Original line number Diff line number Diff line
@@ -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(