diff --git a/res/layout/preference_text_view.xml b/res/layout/preference_text_view.xml new file mode 100644 index 00000000000..3d0b2a1d9c7 --- /dev/null +++ b/res/layout/preference_text_view.xml @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 1988d9dd688..bc5de68e965 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -5196,6 +5196,10 @@ Battery usage since last full charge Battery usage for %s + + Screen time since last full charge + + Screen time for %s Breakdown by apps diff --git a/res/xml/power_usage_advanced.xml b/res/xml/power_usage_advanced.xml index af6152abfd2..eaefe9df338 100644 --- a/res/xml/power_usage_advanced.xml +++ b/res/xml/power_usage_advanced.xml @@ -26,6 +26,18 @@ settings:controller= "com.android.settings.fuelgauge.batteryusage.BatteryChartPreferenceController" /> + + + + + + > mBatteryUsageMap; + @VisibleForTesting + Map> mScreenOnTimeMap; private boolean mIs24HourFormat; private boolean mHourlyChartVisible = true; @@ -108,6 +123,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll private BatteryChartViewModel mDailyViewModel; private List 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> 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 = diff --git a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java index c7cf70e3b97..7c4478e66c6 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java +++ b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageAdvanced.java @@ -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 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 */)); diff --git a/src/com/android/settings/fuelgauge/batteryusage/ScreenOnTimeController.java b/src/com/android/settings/fuelgauge/batteryusage/ScreenOnTimeController.java new file mode 100644 index 00000000000..e78285f6093 --- /dev/null +++ b/src/com/android/settings/fuelgauge/batteryusage/ScreenOnTimeController.java @@ -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; + } +} diff --git a/src/com/android/settings/fuelgauge/batteryusage/TextViewPreference.java b/src/com/android/settings/fuelgauge/batteryusage/TextViewPreference.java new file mode 100644 index 00000000000..22faabb3af0 --- /dev/null +++ b/src/com/android/settings/fuelgauge/batteryusage/TextViewPreference.java @@ -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(); + } +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ScreenOnTimeControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ScreenOnTimeControllerTest.java new file mode 100644 index 00000000000..0e15dcde0c8 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ScreenOnTimeControllerTest.java @@ -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 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); + } +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/TextViewPreferenceTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/TextViewPreferenceTest.java new file mode 100644 index 00000000000..ca7e9dd2879 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/TextViewPreferenceTest.java @@ -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); + } +}