[Battery usage U] [UI] Show total "Screen On Time" in the battery usage page

Screen record: https://drive.google.com/open?id=16ZOp1E2YBzWQXbnXl786FaLFPel-S9CF&authuser=0&resourcekey=0-oRqCrdTc9FZjVgsq9orhEw&usp=drive_link
For Arabic: https://drive.google.com/open?id=1zh_4jcUnqLC6CDgwju1qQkWJ0QCtm19c&authuser=0&resourcekey=0-kuKfDdOTWxqOUmD0RfPNLQ&usp=drive_link

Next step: show screen on time for each app

Bug: 258120710
Test: manual
Change-Id: I2085a2a85ebd50b2ac876972f6a8ebbf6f20246c
This commit is contained in:
Zaiyue Xue
2023-01-05 14:13:09 +08:00
parent c1f7222273
commit 833e47d851
9 changed files with 398 additions and 1 deletions

View File

@@ -0,0 +1,25 @@
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="8dp"
android:textAlignment="viewStart"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary" />

View File

@@ -5196,6 +5196,10 @@
<string name="battery_usage_breakdown_title_since_last_full_charge">Battery usage since last full charge</string>
<!-- [CHAR_LIMIT=NONE] Battery usage breakdown title for a selected slot -->
<string name="battery_usage_breakdown_title_for_slot">Battery usage for <xliff:g id="slot">%s</xliff:g></string>
<!-- [CHAR_LIMIT=NONE] Device screen on time category since last full charge -->
<string name="screen_time_category_last_full_charge">Screen time since last full charge</string>
<!-- [CHAR_LIMIT=NONE] Device screen on time category for a selected slot -->
<string name="screen_time_category_for_slot">Screen time for <xliff:g id="slot">%s</xliff:g></string>
<!-- [CHAR_LIMIT=NONE] The spinner item text in the battery usage breakdown. -->
<string name="battery_usage_spinner_breakdown_by_apps">Breakdown by apps</string>
<!-- [CHAR_LIMIT=NONE] The spinner item text in the battery usage breakdown. -->

View File

@@ -26,6 +26,18 @@
settings:controller=
"com.android.settings.fuelgauge.batteryusage.BatteryChartPreferenceController" />
<PreferenceCategory
android:key="screen_on_time_category"
settings:controller=
"com.android.settings.fuelgauge.batteryusage.ScreenOnTimeController"
settings:isPreferenceVisible="false">
<com.android.settings.fuelgauge.batteryusage.TextViewPreference
android:key="screen_on_time_text"
settings:isPreferenceVisible="false" />
</PreferenceCategory>
<PreferenceCategory
android:key="battery_usage_breakdown"
settings:controller=

View File

@@ -88,6 +88,19 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
BatteryDiffData slotUsageData, String slotTimestamp, boolean isAllUsageDataEmpty);
}
/**
* A callback listener for the device screen on time is updated.
* This happens when screen on time data is ready or the selected index is changed.
*/
public interface OnScreenOnTimeUpdatedListener {
/**
* The callback function for the device screen on time is updated.
* @param screenOnTime The selected slot device screen on time.
* @param slotTimestamp The selected slot timestamp information.
*/
void onScreenOnTimeUpdated(Long screenOnTime, String slotTimestamp);
}
@VisibleForTesting
Context mPrefContext;
@VisibleForTesting
@@ -100,6 +113,8 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
int mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
@VisibleForTesting
Map<Integer, Map<Integer, BatteryDiffData>> mBatteryUsageMap;
@VisibleForTesting
Map<Integer, Map<Integer, Long>> mScreenOnTimeMap;
private boolean mIs24HourFormat;
private boolean mHourlyChartVisible = true;
@@ -108,6 +123,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
private BatteryChartViewModel mDailyViewModel;
private List<BatteryChartViewModel> mHourlyViewModels;
private OnBatteryUsageUpdatedListener mOnBatteryUsageUpdatedListener;
private OnScreenOnTimeUpdatedListener mOnScreenOnTimeUpdatedListener;
private final SettingsActivity mActivity;
private final MetricsFeatureProvider mMetricsFeatureProvider;
@@ -202,6 +218,10 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
mOnBatteryUsageUpdatedListener = listener;
}
void setOnScreenOnTimeUpdatedListener(OnScreenOnTimeUpdatedListener listener) {
mOnScreenOnTimeUpdatedListener = listener;
}
void setBatteryHistoryMap(
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap) {
Log.d(TAG, "setBatteryHistoryMap() " + (batteryHistoryMap == null ? "null"
@@ -212,6 +232,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
DataProcessManager.getBatteryLevelData(mContext, mHandler, batteryHistoryMap,
batteryCallbackData -> {
mBatteryUsageMap = batteryCallbackData.getBatteryUsageMap();
mScreenOnTimeMap = batteryCallbackData.getDeviceScreenOnTime();
refreshUi();
});
Log.d(TAG, "getBatteryLevelData: " + batteryLevelData);
@@ -318,7 +339,12 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll
return false;
}
if (mOnScreenOnTimeUpdatedListener != null && mScreenOnTimeMap != null
&& mScreenOnTimeMap.get(mDailyChartIndex) != null) {
mOnScreenOnTimeUpdatedListener.onScreenOnTimeUpdated(
mScreenOnTimeMap.get(mDailyChartIndex).get(mHourlyChartIndex),
getSlotInformation());
}
if (mOnBatteryUsageUpdatedListener != null && mBatteryUsageMap != null
&& mBatteryUsageMap.get(mDailyChartIndex) != null) {
final BatteryDiffData slotUsageData =

View File

@@ -129,14 +129,18 @@ public class PowerUsageAdvanced extends PowerUsageBase {
mBatteryChartPreferenceController =
new BatteryChartPreferenceController(
context, getSettingsLifecycle(), (SettingsActivity) getActivity());
ScreenOnTimeController screenOnTimeController = new ScreenOnTimeController(context);
BatteryUsageBreakdownController batteryUsageBreakdownController =
new BatteryUsageBreakdownController(
context, getSettingsLifecycle(), (SettingsActivity) getActivity(), this);
mBatteryChartPreferenceController.setOnScreenOnTimeUpdatedListener(
screenOnTimeController::handleSceenOnTimeUpdated);
mBatteryChartPreferenceController.setOnBatteryUsageUpdatedListener(
batteryUsageBreakdownController::handleBatteryUsageUpdated);
controllers.add(mBatteryChartPreferenceController);
controllers.add(screenOnTimeController);
controllers.add(batteryUsageBreakdownController);
setBatteryChartPreferenceController();
return controllers;
@@ -192,6 +196,7 @@ public class PowerUsageAdvanced extends PowerUsageBase {
final List<AbstractPreferenceController> controllers = new ArrayList<>();
controllers.add(new BatteryChartPreferenceController(
context, null /* lifecycle */, null /* activity */));
controllers.add((new ScreenOnTimeController(context)));
controllers.add(new BatteryUsageBreakdownController(
context, null /* lifecycle */, null /* activity */,
null /* fragment */));

View File

@@ -0,0 +1,110 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.fuelgauge.batteryusage;
import android.content.Context;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.AbsoluteSizeSpan;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.utils.StringUtil;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Controller for screen on time in battery usage page. */
public class ScreenOnTimeController extends BasePreferenceController {
private static final String TAG = "ScreenOnTimeController";
private static final String ROOT_PREFERENCE_KEY = "screen_on_time_category";
private static final String SCREEN_ON_TIME_TEXT_PREFERENCE_KEY = "screen_on_time_text";
private static final Pattern NUMBER_PATTERN = Pattern.compile("[\\d]*[\\.,]?[\\d]+");
@VisibleForTesting
Context mPrefContext;
@VisibleForTesting
PreferenceCategory mRootPreference;
@VisibleForTesting
TextViewPreference mScreenOnTimeTextPreference;
public ScreenOnTimeController(Context context) {
super(context, ROOT_PREFERENCE_KEY);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPrefContext = screen.getContext();
mRootPreference = screen.findPreference(ROOT_PREFERENCE_KEY);
mScreenOnTimeTextPreference = screen.findPreference(SCREEN_ON_TIME_TEXT_PREFERENCE_KEY);
}
void handleSceenOnTimeUpdated(Long screenOnTime, String slotTimestamp) {
if (screenOnTime == null) {
mRootPreference.setVisible(false);
mScreenOnTimeTextPreference.setVisible(false);
return;
}
showCategoryTitle(slotTimestamp);
showScreenOnTimeText(screenOnTime);
}
@VisibleForTesting
void showCategoryTitle(String slotTimestamp) {
mRootPreference.setTitle(slotTimestamp == null
? mPrefContext.getString(
R.string.screen_time_category_last_full_charge)
: mPrefContext.getString(
R.string.screen_time_category_for_slot, slotTimestamp));
mRootPreference.setVisible(true);
}
@VisibleForTesting
void showScreenOnTimeText(Long screenOnTime) {
final CharSequence timeSequence =
StringUtil.formatElapsedTime(mPrefContext, (double) screenOnTime,
/*withSeconds=*/ false, /*collapseTimeUnit=*/ false);
mScreenOnTimeTextPreference.setText(enlargeFontOfNumber(timeSequence));
mScreenOnTimeTextPreference.setVisible(true);
}
@VisibleForTesting
static CharSequence enlargeFontOfNumber(CharSequence text) {
if (TextUtils.isEmpty(text)) {
return "";
}
final SpannableString spannableText = new SpannableString(text);
final Matcher matcher = NUMBER_PATTERN.matcher(text);
while (matcher.find()) {
spannableText.setSpan(new AbsoluteSizeSpan(36, true /* dip */), matcher.start(),
matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return spannableText;
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.fuelgauge.batteryusage;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.TextView;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
/** A preference for a single text view. */
public class TextViewPreference extends Preference {
private static final String TAG = "TextViewPreference";
@VisibleForTesting
CharSequence mText;
public TextViewPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setLayoutResource(R.layout.preference_text_view);
}
@Override
public void onBindViewHolder(PreferenceViewHolder view) {
final TextView textView = (TextView) view.findViewById(R.id.text);
textView.setText(mText);
}
void setText(CharSequence text) {
mText = text;
notifyChanged();
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.fuelgauge.batteryusage;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.content.res.Resources;
import android.os.LocaleList;
import androidx.preference.PreferenceCategory;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import java.util.Locale;
import java.util.TimeZone;
@RunWith(RobolectricTestRunner.class)
public final class ScreenOnTimeControllerTest {
private Context mContext;
private ScreenOnTimeController mScreenOnTimeController;
@Mock
private PreferenceCategory mRootPreference;
@Mock
private TextViewPreference mScreenOnTimeTextPreference;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
Locale.setDefault(new Locale("en_US"));
org.robolectric.shadows.ShadowSettings.set24HourTimeFormat(false);
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
mContext = spy(RuntimeEnvironment.application);
final Resources resources = spy(mContext.getResources());
resources.getConfiguration().setLocales(new LocaleList(new Locale("en_US")));
doReturn(resources).when(mContext).getResources();
mScreenOnTimeController = new ScreenOnTimeController(mContext);
mScreenOnTimeController.mPrefContext = mContext;
mScreenOnTimeController.mRootPreference = mRootPreference;
mScreenOnTimeController.mScreenOnTimeTextPreference = mScreenOnTimeTextPreference;
}
@Test
public void handleSceenOnTimeUpdated_nullScreenOnTime_hideAllPreference() {
mScreenOnTimeController.handleSceenOnTimeUpdated(
/* screenOnTime= */ null, "Friday 12:00-now");
verify(mRootPreference).setVisible(false);
verify(mScreenOnTimeTextPreference).setVisible(false);
}
@Test
public void showCategoryTitle_null_sinceLastFullCharge() {
mScreenOnTimeController.showCategoryTitle(null);
verify(mRootPreference).setTitle("Screen time since last full charge");
verify(mRootPreference).setVisible(true);
}
@Test
public void showCategoryTitle_notNull_slotTimestamp() {
mScreenOnTimeController.showCategoryTitle("Friday 12:00-now");
verify(mRootPreference).setTitle("Screen time for Friday 12:00-now");
verify(mRootPreference).setVisible(true);
}
@Test
public void showScreenOnTimeText_returnExpectedResult() {
mScreenOnTimeController.showScreenOnTimeText(1600000000L);
ArgumentCaptor<CharSequence> argumentCaptor = ArgumentCaptor.forClass(CharSequence.class);
verify(mScreenOnTimeTextPreference).setText(argumentCaptor.capture());
assertThat(argumentCaptor.getValue().toString()).isEqualTo("18 days, 12 hr, 27 min");
verify(mScreenOnTimeTextPreference).setVisible(true);
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.fuelgauge.batteryusage;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.spy;
import android.content.Context;
import com.android.settings.R;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class)
public final class TextViewPreferenceTest {
private Context mContext;
private TextViewPreference mTextViewPreference;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
mTextViewPreference = new TextViewPreference(mContext, /*attrs=*/ null);
}
@Test
public void constructor_returnExpectedResult() {
assertThat(mTextViewPreference.getLayoutResource()).isEqualTo(
R.layout.preference_text_view);
}
@Test
public void setText_returnExpectedResult() {
final String text = "TEST_TEXT";
mTextViewPreference.setText(text);
assertThat(mTextViewPreference.mText.toString()).isEqualTo(text);
}
}