diff --git a/res/layout/battery_chart_graph.xml b/res/layout/battery_chart_graph.xml index df481443e83..b95c6604be2 100644 --- a/res/layout/battery_chart_graph.xml +++ b/res/layout/battery_chart_graph.xml @@ -29,14 +29,24 @@ android:layout_marginVertical="16dp" android:textAppearance="?android:attr/textAppearanceSmall" android:textColor="?android:attr/textColorSecondary" - android:text="@string/battery_usage_chart_graph_hint" /> + android:text="@string/battery_usage_chart_graph_hint_last_full_charge" /> + + diff --git a/res/values/strings.xml b/res/values/strings.xml index c2f2272ef7f..455a2c39cc2 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -6751,10 +6751,16 @@ Show battery percentage in status bar + + Battery level since last full charge Battery level for past 24 hr + + App usage since last full charge App usage for past 24 hr + + System usage since last full charge System usage for past 24 hr diff --git a/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java b/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java index 99c630d9c3e..732163b6a36 100644 --- a/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppBatteryPreferenceController.java @@ -179,7 +179,7 @@ public class AppBatteryPreferenceController extends BasePreferenceController return null; } final BatteryDiffEntry entry = - BatteryChartPreferenceController.getBatteryLast24HrUsageData( + BatteryChartPreferenceController.getAppBatteryUsageData( mContext, mPackageName, mUserId); Log.d(TAG, "loadBatteryDiffEntries():\n" + entry); return entry; @@ -200,10 +200,10 @@ public class AppBatteryPreferenceController extends BasePreferenceController mBatteryPercent = Utils.formatPercentage( mBatteryDiffEntry.getPercentOfTotal(), /* round */ true); mPreference.setSummary(mContext.getString( - R.string.battery_summary_24hr, mBatteryPercent)); + R.string.battery_summary, mBatteryPercent)); } else { mPreference.setSummary( - mContext.getString(R.string.no_battery_summary_24hr)); + mContext.getString(R.string.no_battery_summary)); } } diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java index d096d49dc4e..db98a4c6376 100644 --- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java +++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java @@ -539,16 +539,13 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements return null; } if (totalTimeMs == 0) { - final int batteryWithoutUsageTime = consumedPower > 0 - ? R.string.battery_usage_without_time : R.string.battery_not_usage_24hr; - usageTimeSummary = getText(isChartGraphEnabled - ? batteryWithoutUsageTime : R.string.battery_not_usage); + usageTimeSummary = getText( + isChartGraphEnabled && consumedPower > 0 ? R.string.battery_usage_without_time + : R.string.battery_not_usage); } else if (slotTime == null) { - // Shows summary text with past 24 hr or full charge if slot time is null. - usageTimeSummary = isChartGraphEnabled - ? getAppPast24HrActiveSummary(foregroundTimeMs, backgroundTimeMs, totalTimeMs) - : getAppFullChargeActiveSummary( - foregroundTimeMs, backgroundTimeMs, totalTimeMs); + // Shows summary text with last full charge if slot time is null. + usageTimeSummary = getAppFullChargeActiveSummary( + foregroundTimeMs, backgroundTimeMs, totalTimeMs); } else { // Shows summary text with slot time. usageTimeSummary = getAppActiveSummaryWithSlotTime( diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java index d363308740a..88bec0d9d05 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java @@ -20,7 +20,6 @@ import android.app.settings.SettingsEnums; import android.content.Context; import android.content.res.Configuration; import android.graphics.drawable.Drawable; -import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -28,7 +27,9 @@ import android.text.TextUtils; import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.Log; +import android.view.View; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceGroup; @@ -53,8 +54,6 @@ import com.android.settingslib.utils.StringUtil; import com.android.settingslib.widget.FooterPreference; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -62,27 +61,23 @@ import java.util.Map; /** Controls the update for chart graph and the list items. */ public class BatteryChartPreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, LifecycleObserver, OnCreate, OnDestroy, - OnSaveInstanceState, BatteryChartView.OnSelectListener, OnResume, - ExpandDividerPreference.OnExpandListener { + OnSaveInstanceState, OnResume, ExpandDividerPreference.OnExpandListener { private static final String TAG = "BatteryChartPreferenceController"; private static final String KEY_FOOTER_PREF = "battery_graph_footer"; private static final String PACKAGE_NAME_NONE = "none"; - /** Desired battery history size for timestamp slots. */ - public static final int DESIRED_HISTORY_SIZE = 25; - private static final int CHART_LEVEL_ARRAY_SIZE = 13; - private static final int CHART_KEY_ARRAY_SIZE = DESIRED_HISTORY_SIZE; private static final long VALID_USAGE_TIME_DURATION = DateUtils.HOUR_IN_MILLIS * 2; private static final long VALID_DIFF_DURATION = DateUtils.MINUTE_IN_MILLIS * 3; // Keys for bundle instance to restore configurations. private static final String KEY_EXPAND_SYSTEM_INFO = "expand_system_info"; - private static final String KEY_CURRENT_TIME_SLOT = "current_time_slot"; + private static final String KEY_DAILY_CHART_INDEX = "daily_chart_index"; + private static final String KEY_HOURLY_CHART_INDEX = "hourly_chart_index"; private static int sUiMode = Configuration.UI_MODE_NIGHT_UNDEFINED; @VisibleForTesting - Map> mBatteryIndexedMap; + Map> mBatteryUsageMap; @VisibleForTesting Context mPrefContext; @@ -91,28 +86,34 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll @VisibleForTesting PreferenceGroup mAppListPrefGroup; @VisibleForTesting - BatteryChartView mBatteryChartView; - @VisibleForTesting ExpandDividerPreference mExpandDividerPreference; - @VisibleForTesting boolean mIsExpanded = false; - @VisibleForTesting - int[] mBatteryHistoryLevels; - @VisibleForTesting - long[] mBatteryHistoryKeys; - @VisibleForTesting - int mTrapezoidIndex = BatteryChartView.SELECTED_INDEX_INVALID; - private boolean mIs24HourFormat = false; + @VisibleForTesting + BatteryChartView mDailyChartView; + @VisibleForTesting + BatteryChartView mHourlyChartView; + + @VisibleForTesting + int mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL; + @VisibleForTesting + int mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL; + + private boolean mIs24HourFormat; private boolean mIsFooterPrefAdded = false; private PreferenceScreen mPreferenceScreen; private FooterPreference mFooterPreference; + // Daily view model only saves abbreviated day of week texts (e.g. MON). This field saves the + // full day of week texts (e.g. Monday), which is used in category title and battery detail + // page. + private List mDailyTimestampFullTexts; + private BatteryChartViewModel mDailyViewModel; + private List mHourlyViewModels; private final String mPreferenceKey; private final SettingsActivity mActivity; private final InstrumentedPreferenceFragment mFragment; - private final CharSequence[] mNotAllowShowEntryPackages; private final CharSequence[] mNotAllowShowSummaryPackages; private final MetricsFeatureProvider mMetricsFeatureProvider; private final Handler mHandler = new Handler(Looper.getMainLooper()); @@ -120,8 +121,6 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll // Preference cache to avoid create new instance each time. @VisibleForTesting final Map mPreferenceCache = new HashMap<>(); - @VisibleForTesting - final List mSystemEntries = new ArrayList<>(); public BatteryChartPreferenceController( Context context, String preferenceKey, @@ -134,10 +133,6 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll mIs24HourFormat = DateFormat.is24HourFormat(context); mMetricsFeatureProvider = FeatureFactory.getFactory(mContext).getMetricsFeatureProvider(); - mNotAllowShowEntryPackages = - FeatureFactory.getFactory(context) - .getPowerUsageFeatureProvider(context) - .getHideApplicationEntries(context); mNotAllowShowSummaryPackages = FeatureFactory.getFactory(context) .getPowerUsageFeatureProvider(context) @@ -152,12 +147,14 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll if (savedInstanceState == null) { return; } - mTrapezoidIndex = - savedInstanceState.getInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex); + mDailyChartIndex = + savedInstanceState.getInt(KEY_DAILY_CHART_INDEX, mDailyChartIndex); + mHourlyChartIndex = + savedInstanceState.getInt(KEY_HOURLY_CHART_INDEX, mHourlyChartIndex); mIsExpanded = savedInstanceState.getBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded); - Log.d(TAG, String.format("onCreate() slotIndex=%d isExpanded=%b", - mTrapezoidIndex, mIsExpanded)); + Log.d(TAG, String.format("onCreate() dailyIndex=%d hourlyIndex=%d isExpanded=%b", + mDailyChartIndex, mHourlyChartIndex, mIsExpanded)); } @Override @@ -179,10 +176,11 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll if (savedInstance == null) { return; } - savedInstance.putInt(KEY_CURRENT_TIME_SLOT, mTrapezoidIndex); + savedInstance.putInt(KEY_DAILY_CHART_INDEX, mDailyChartIndex); + savedInstance.putInt(KEY_HOURLY_CHART_INDEX, mHourlyChartIndex); savedInstance.putBoolean(KEY_EXPAND_SYSTEM_INFO, mIsExpanded); - Log.d(TAG, String.format("onSaveInstanceState() slotIndex=%d isExpanded=%b", - mTrapezoidIndex, mIsExpanded)); + Log.d(TAG, String.format("onSaveInstanceState() dailyIndex=%d hourlyIndex=%d isExpanded=%b", + mDailyChartIndex, mHourlyChartIndex, mIsExpanded)); } @Override @@ -204,8 +202,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll mPrefContext = screen.getContext(); mAppListPrefGroup = screen.findPreference(mPreferenceKey); mAppListPrefGroup.setOrderingAsAdded(false); - mAppListPrefGroup.setTitle( - mPrefContext.getString(R.string.battery_app_usage_for_past_24)); + mAppListPrefGroup.setTitle(mPrefContext.getString(R.string.battery_app_usage)); mFooterPreference = screen.findPreference(KEY_FOOTER_PREF); // Removes footer first until usage data is loaded to avoid flashing. if (mFooterPreference != null) { @@ -249,17 +246,6 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll return true; } - @Override - public void onSelect(int trapezoidIndex) { - Log.d(TAG, "onChartSelect:" + trapezoidIndex); - refreshUi(trapezoidIndex, /*isForce=*/ false); - mMetricsFeatureProvider.action( - mPrefContext, - trapezoidIndex == BatteryChartView.SELECTED_INDEX_ALL - ? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL - : SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT); - } - @Override public void onExpand(boolean isExpanded) { mIsExpanded = isExpanded; @@ -272,81 +258,120 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll void setBatteryHistoryMap( final Map> batteryHistoryMap) { - // Resets all battery history data relative variables. - if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) { - mBatteryIndexedMap = null; - mBatteryHistoryKeys = null; - mBatteryHistoryLevels = null; - addFooterPreferenceIfNeeded(false); + Log.d(TAG, "setBatteryHistoryMap() " + (batteryHistoryMap == null ? "null" + : ("size=" + batteryHistoryMap.size()))); + final BatteryLevelData batteryLevelData = + DataProcessor.getBatteryLevelData(mContext, mHandler, batteryHistoryMap, + batteryUsageMap -> { + mBatteryUsageMap = batteryUsageMap; + refreshUi(); + }); + Log.d(TAG, "getBatteryLevelData: " + batteryLevelData); + if (batteryLevelData == null) { + mDailyTimestampFullTexts = null; + mDailyViewModel = null; + mHourlyViewModels = null; + refreshUi(); return; } - mBatteryHistoryKeys = getBatteryHistoryKeys(batteryHistoryMap); - mBatteryHistoryLevels = new int[CHART_LEVEL_ARRAY_SIZE]; - for (int index = 0; index < CHART_LEVEL_ARRAY_SIZE; index++) { - final long timestamp = mBatteryHistoryKeys[index * 2]; - final Map entryMap = batteryHistoryMap.get(timestamp); - if (entryMap == null || entryMap.isEmpty()) { - Log.e(TAG, "abnormal entry list in the timestamp:" - + ConvertUtils.utcToLocalTime(mPrefContext, timestamp)); - continue; + mDailyTimestampFullTexts = generateTimestampDayOfWeekTexts( + mContext, batteryLevelData.getDailyBatteryLevels().getTimestamps(), + /* isAbbreviation= */ false); + mDailyViewModel = new BatteryChartViewModel( + batteryLevelData.getDailyBatteryLevels().getLevels(), + generateTimestampDayOfWeekTexts( + mContext, batteryLevelData.getDailyBatteryLevels().getTimestamps(), + /* isAbbreviation= */ true), + BatteryChartViewModel.AxisLabelPosition.CENTER_OF_TRAPEZOIDS); + mHourlyViewModels = new ArrayList<>(); + for (BatteryLevelData.PeriodBatteryLevelData hourlyBatteryLevelsPerDay : + batteryLevelData.getHourlyBatteryLevelsPerDay()) { + mHourlyViewModels.add(new BatteryChartViewModel( + hourlyBatteryLevelsPerDay.getLevels(), + generateTimestampHourTexts( + mContext, hourlyBatteryLevelsPerDay.getTimestamps()), + BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS)); + } + refreshUi(); + } + + void setBatteryChartView(@NonNull final BatteryChartView dailyChartView, + @NonNull final BatteryChartView hourlyChartView) { + if (mDailyChartView != dailyChartView || mHourlyChartView != hourlyChartView) { + mHandler.post(() -> setBatteryChartViewInner(dailyChartView, hourlyChartView)); + } + } + + private void setBatteryChartViewInner(@NonNull final BatteryChartView dailyChartView, + @NonNull final BatteryChartView hourlyChartView) { + mDailyChartView = dailyChartView; + mDailyChartView.setOnSelectListener(trapezoidIndex -> { + if (mDailyChartIndex == trapezoidIndex) { + return; } - // Averages the battery level in each time slot to avoid corner conditions. - float batteryLevelCounter = 0; - for (BatteryHistEntry entry : entryMap.values()) { - batteryLevelCounter += entry.mBatteryLevel; + Log.d(TAG, "onDailyChartSelect:" + trapezoidIndex); + mDailyChartIndex = trapezoidIndex; + mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL; + refreshUi(); + // TODO: Change to log daily data. + }); + mHourlyChartView = hourlyChartView; + mHourlyChartView.setOnSelectListener(trapezoidIndex -> { + if (mHourlyChartIndex == trapezoidIndex) { + return; } - mBatteryHistoryLevels[index] = - Math.round(batteryLevelCounter / entryMap.size()); - } - forceRefreshUi(); - Log.d(TAG, String.format( - "setBatteryHistoryMap() size=%d key=%s\nlevels=%s", - batteryHistoryMap.size(), - ConvertUtils.utcToLocalTime(mPrefContext, - mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1]), - Arrays.toString(mBatteryHistoryLevels))); - - // Loads item icon and label in the background. - new LoadAllItemsInfoTask(batteryHistoryMap).execute(); - } - - void setBatteryChartView(final BatteryChartView batteryChartView) { - if (mBatteryChartView != batteryChartView) { - mHandler.post(() -> setBatteryChartViewInner(batteryChartView)); - } - } - - private void setBatteryChartViewInner(final BatteryChartView batteryChartView) { - mBatteryChartView = batteryChartView; - mBatteryChartView.setOnSelectListener(this); - forceRefreshUi(); - } - - private void forceRefreshUi() { - final int refreshIndex = - mTrapezoidIndex == BatteryChartView.SELECTED_INDEX_INVALID - ? BatteryChartView.SELECTED_INDEX_ALL - : mTrapezoidIndex; - if (mBatteryChartView != null) { - mBatteryChartView.setLevels(mBatteryHistoryLevels); - mBatteryChartView.setSelectedIndex(refreshIndex); - setTimestampLabel(); - } - refreshUi(refreshIndex, /*isForce=*/ true); + Log.d(TAG, "onHourlyChartSelect:" + trapezoidIndex); + mHourlyChartIndex = trapezoidIndex; + refreshUi(); + mMetricsFeatureProvider.action( + mPrefContext, + trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL + ? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL + : SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT); + }); + refreshUi(); } @VisibleForTesting - boolean refreshUi(int trapezoidIndex, boolean isForce) { - // Invalid refresh condition. - if (mBatteryIndexedMap == null - || mBatteryChartView == null - || (mTrapezoidIndex == trapezoidIndex && !isForce)) { + boolean refreshUi() { + if (mDailyChartView == null || mHourlyChartView == null) { + // Chart views are not initialized. + return false; + } + if (mDailyViewModel == null || mHourlyViewModels == null) { + // Fail to get battery level data, show an empty hourly chart view. + mDailyChartView.setVisibility(View.GONE); + mHourlyChartView.setVisibility(View.VISIBLE); + mHourlyChartView.setViewModel(null); + removeAndCacheAllPrefs(); + addFooterPreferenceIfNeeded(false); + return false; + } + if (mBatteryUsageMap == null) { + // Battery usage data is not ready, wait for data ready to refresh UI. return false; } - Log.d(TAG, String.format("refreshUi: index=%d size=%d isForce:%b", - trapezoidIndex, mBatteryIndexedMap.size(), isForce)); - mTrapezoidIndex = trapezoidIndex; + if (isBatteryLevelDataInOneDay()) { + // Only 1 day data, hide the daily chart view. + mDailyChartView.setVisibility(View.GONE); + mDailyChartIndex = 0; + } else { + mDailyChartView.setVisibility(View.VISIBLE); + mDailyViewModel.setSelectedIndex(mDailyChartIndex); + mDailyChartView.setViewModel(mDailyViewModel); + } + + if (mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) { + // Multiple days are selected, hide the hourly chart view. + mHourlyChartView.setVisibility(View.GONE); + } else { + mHourlyChartView.setVisibility(View.VISIBLE); + final BatteryChartViewModel hourlyViewModel = mHourlyViewModels.get(mDailyChartIndex); + hourlyViewModel.setSelectedIndex(mHourlyChartIndex); + mHourlyChartView.setViewModel(hourlyViewModel); + } + mHandler.post(() -> { final long start = System.currentTimeMillis(); removeAndCacheAllPrefs(); @@ -359,43 +384,22 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll } private void addAllPreferences() { - final List entries = - mBatteryIndexedMap.get(Integer.valueOf(mTrapezoidIndex)); - addFooterPreferenceIfNeeded(entries != null && !entries.isEmpty()); - if (entries == null) { - Log.w(TAG, "cannot find BatteryDiffEntry for:" + mTrapezoidIndex); + final BatteryDiffData batteryDiffData = + mBatteryUsageMap.get(mDailyChartIndex).get(mHourlyChartIndex); + addFooterPreferenceIfNeeded(batteryDiffData != null + && (!batteryDiffData.getAppDiffEntryList().isEmpty() + || !batteryDiffData.getSystemDiffEntryList().isEmpty())); + if (batteryDiffData == null) { + Log.w(TAG, "cannot find BatteryDiffEntry for daily_index: " + mDailyChartIndex + + " hourly_index: " + mHourlyChartIndex); return; } - // Separates data into two groups and sort them individually. - final List appEntries = new ArrayList<>(); - mSystemEntries.clear(); - entries.forEach(entry -> { - final String packageName = entry.getPackageName(); - if (!isValidToShowEntry(packageName)) { - Log.w(TAG, "ignore showing item:" + packageName); - return; - } - if (entry.isSystemEntry()) { - mSystemEntries.add(entry); - } else { - appEntries.add(entry); - } - // Validates the usage time if users click a specific slot. - if (mTrapezoidIndex >= 0) { - validateUsageTime(entry); - } - }); - Collections.sort(appEntries, BatteryDiffEntry.COMPARATOR); - Collections.sort(mSystemEntries, BatteryDiffEntry.COMPARATOR); - Log.d(TAG, String.format("addAllPreferences() app=%d system=%d", - appEntries.size(), mSystemEntries.size())); - // Adds app entries to the list if it is not empty. - if (!appEntries.isEmpty()) { - addPreferenceToScreen(appEntries); + if (!batteryDiffData.getAppDiffEntryList().isEmpty()) { + addPreferenceToScreen(batteryDiffData.getAppDiffEntryList()); } // Adds the expabable divider if we have system entries data. - if (!mSystemEntries.isEmpty()) { + if (!batteryDiffData.getSystemDiffEntryList().isEmpty()) { if (mExpandDividerPreference == null) { mExpandDividerPreference = new ExpandDividerPreference(mPrefContext); mExpandDividerPreference.setOnExpandListener(this); @@ -469,11 +473,13 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll } private void refreshExpandUi() { + final List systemEntries = mBatteryUsageMap.get(mDailyChartIndex).get( + mHourlyChartIndex).getSystemDiffEntryList(); if (mIsExpanded) { - addPreferenceToScreen(mSystemEntries); + addPreferenceToScreen(systemEntries); } else { // Removes and recycles all system entries to hide all of them. - for (BatteryDiffEntry entry : mSystemEntries) { + for (BatteryDiffEntry entry : systemEntries) { final String prefKey = entry.mBatteryHistEntry.getKey(); final Preference pref = mAppListPrefGroup.findPreference(prefKey); if (pref != null) { @@ -499,11 +505,12 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll } private String getSlotInformation(boolean isApp, String slotInformation) { + // TODO: Updates the right slot information from daily and hourly chart selection. // Null means we show all information without a specific time slot. if (slotInformation == null) { return isApp - ? mPrefContext.getString(R.string.battery_app_usage_for_past_24) - : mPrefContext.getString(R.string.battery_system_usage_for_past_24); + ? mPrefContext.getString(R.string.battery_app_usage) + : mPrefContext.getString(R.string.battery_system_usage); } else { return isApp ? mPrefContext.getString(R.string.battery_app_usage_for, slotInformation) @@ -511,17 +518,33 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll } } - private String getSlotInformation() { - if (mTrapezoidIndex < 0) { + @VisibleForTesting + String getSlotInformation() { + if (mDailyTimestampFullTexts == null || mDailyViewModel == null + || mHourlyViewModels == null) { + // No data return null; } - final String fromHour = ConvertUtils.utcToLocalTimeHour(mPrefContext, - mBatteryHistoryKeys[mTrapezoidIndex * 2], mIs24HourFormat); - final String toHour = ConvertUtils.utcToLocalTimeHour(mPrefContext, - mBatteryHistoryKeys[(mTrapezoidIndex + 1) * 2], mIs24HourFormat); - return mIs24HourFormat - ? String.format("%s–%s", fromHour, toHour) - : String.format("%s – %s", fromHour, toHour); + if (isAllSelected()) { + return null; + } + + final String selectedDayText = mDailyTimestampFullTexts.get(mDailyChartIndex); + if (mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) { + return selectedDayText; + } + + final String fromHourText = mHourlyViewModels.get(mDailyChartIndex).texts().get( + mHourlyChartIndex); + final String toHourText = mHourlyViewModels.get(mDailyChartIndex).texts().get( + mHourlyChartIndex + 1); + final String selectedHourText = + String.format("%s%s%s", fromHourText, mIs24HourFormat ? "-" : " - ", toHourText); + if (isBatteryLevelDataInOneDay()) { + return selectedHourText; + } + + return String.format("%s %s", selectedDayText, selectedHourText); } @VisibleForTesting @@ -575,22 +598,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll @VisibleForTesting boolean isValidToShowSummary(String packageName) { - return !contains(packageName, mNotAllowShowSummaryPackages); - } - - @VisibleForTesting - boolean isValidToShowEntry(String packageName) { - return !contains(packageName, mNotAllowShowEntryPackages); - } - - @VisibleForTesting - void setTimestampLabel() { - if (mBatteryChartView == null || mBatteryHistoryKeys == null) { - return; - } - final long latestTimestamp = - mBatteryHistoryKeys[mBatteryHistoryKeys.length - 1]; - mBatteryChartView.setLatestTimestamp(latestTimestamp); + return !DataProcessor.contains(packageName, mNotAllowShowSummaryPackages); } private void addFooterPreferenceIfNeeded(boolean containAppItems) { @@ -605,60 +613,65 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll mHandler.post(() -> mPreferenceScreen.addPreference(mFooterPreference)); } - private static boolean contains(String target, CharSequence[] packageNames) { - if (target != null && packageNames != null) { - for (CharSequence packageName : packageNames) { - if (TextUtils.equals(target, packageName)) { - return true; - } - } - } - return false; + private boolean isBatteryLevelDataInOneDay() { + return mHourlyViewModels != null && mHourlyViewModels.size() == 1; } - @VisibleForTesting - static boolean validateUsageTime(BatteryDiffEntry entry) { - final long foregroundUsageTimeInMs = entry.mForegroundUsageTimeInMs; - final long backgroundUsageTimeInMs = entry.mBackgroundUsageTimeInMs; - final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs; - if (foregroundUsageTimeInMs > VALID_USAGE_TIME_DURATION - || backgroundUsageTimeInMs > VALID_USAGE_TIME_DURATION - || totalUsageTimeInMs > VALID_USAGE_TIME_DURATION) { - Log.e(TAG, "validateUsageTime() fail for\n" + entry); - return false; + private boolean isAllSelected() { + return (isBatteryLevelDataInOneDay() + || mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) + && mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL; + } + + private static List generateTimestampDayOfWeekTexts(@NonNull final Context context, + @NonNull final List timestamps, final boolean isAbbreviation) { + final ArrayList texts = new ArrayList<>(); + for (Long timestamp : timestamps) { + texts.add(ConvertUtils.utcToLocalTimeDayOfWeek(context, timestamp, isAbbreviation)); } - return true; + return texts; + } + + private static List generateTimestampHourTexts( + @NonNull final Context context, @NonNull final List timestamps) { + final boolean is24HourFormat = DateFormat.is24HourFormat(context); + final ArrayList texts = new ArrayList<>(); + for (Long timestamp : timestamps) { + texts.add(ConvertUtils.utcToLocalTimeHour(context, timestamp, is24HourFormat)); + } + return texts; } /** Used for {@link AppBatteryPreferenceController}. */ - public static List getBatteryLast24HrUsageData(Context context) { + public static List getAppBatteryUsageData(Context context) { final long start = System.currentTimeMillis(); final Map> batteryHistoryMap = FeatureFactory.getFactory(context) .getPowerUsageFeatureProvider(context) - .getBatteryHistory(context); + .getBatteryHistorySinceLastFullCharge(context); if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) { return null; } - Log.d(TAG, String.format("getBatteryLast24HrData() size=%d time=&d/ms", + Log.d(TAG, String.format("getBatterySinceLastFullChargeUsageData() size=%d time=%d/ms", batteryHistoryMap.size(), (System.currentTimeMillis() - start))); - final Map> batteryIndexedMap = - ConvertUtils.getIndexedUsageMap( - context, - /*timeSlotSize=*/ CHART_LEVEL_ARRAY_SIZE - 1, - getBatteryHistoryKeys(batteryHistoryMap), - batteryHistoryMap, - /*purgeLowPercentageAndFakeData=*/ true); - return batteryIndexedMap.get(BatteryChartView.SELECTED_INDEX_ALL); + + final Map> batteryUsageData = + DataProcessor.getBatteryUsageData(context, batteryHistoryMap); + return batteryUsageData == null + ? null + : batteryUsageData + .get(BatteryChartViewModel.SELECTED_INDEX_ALL) + .get(BatteryChartViewModel.SELECTED_INDEX_ALL) + .getAppDiffEntryList(); } /** Used for {@link AppBatteryPreferenceController}. */ - public static BatteryDiffEntry getBatteryLast24HrUsageData( + public static BatteryDiffEntry getAppBatteryUsageData( Context context, String packageName, int userId) { if (packageName == null) { return null; } - final List entries = getBatteryLast24HrUsageData(context); + final List entries = getAppBatteryUsageData(context); if (entries == null) { return null; } @@ -673,65 +686,4 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll } return null; } - - private static long[] getBatteryHistoryKeys( - final Map> batteryHistoryMap) { - final List batteryHistoryKeyList = - new ArrayList<>(batteryHistoryMap.keySet()); - Collections.sort(batteryHistoryKeyList); - final long[] batteryHistoryKeys = new long[CHART_KEY_ARRAY_SIZE]; - for (int index = 0; index < CHART_KEY_ARRAY_SIZE; index++) { - batteryHistoryKeys[index] = batteryHistoryKeyList.get(index); - } - return batteryHistoryKeys; - } - - // Loads all items icon and label in the background. - private final class LoadAllItemsInfoTask - extends AsyncTask>> { - - private long[] mBatteryHistoryKeysCache; - private Map> mBatteryHistoryMap; - - private LoadAllItemsInfoTask( - Map> batteryHistoryMap) { - this.mBatteryHistoryMap = batteryHistoryMap; - this.mBatteryHistoryKeysCache = mBatteryHistoryKeys; - } - - @Override - protected Map> doInBackground(Void... voids) { - if (mPrefContext == null || mBatteryHistoryKeysCache == null) { - return null; - } - final long startTime = System.currentTimeMillis(); - final Map> indexedUsageMap = - ConvertUtils.getIndexedUsageMap( - mPrefContext, /*timeSlotSize=*/ CHART_LEVEL_ARRAY_SIZE - 1, - mBatteryHistoryKeysCache, mBatteryHistoryMap, - /*purgeLowPercentageAndFakeData=*/ true); - // Pre-loads each BatteryDiffEntry relative icon and label for all slots. - for (List entries : indexedUsageMap.values()) { - entries.forEach(entry -> entry.loadLabelAndIcon()); - } - Log.d(TAG, String.format("execute LoadAllItemsInfoTask in %d/ms", - (System.currentTimeMillis() - startTime))); - return indexedUsageMap; - } - - @Override - protected void onPostExecute( - Map> indexedUsageMap) { - mBatteryHistoryMap = null; - mBatteryHistoryKeysCache = null; - if (indexedUsageMap == null) { - return; - } - // Posts results back to main thread to refresh UI. - mHandler.post(() -> { - mBatteryIndexedMap = indexedUsageMap; - forceRefreshUi(); - }); - } - } } diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java index 427388befcd..e668b37c24e 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartView.java @@ -18,6 +18,7 @@ package com.android.settings.fuelgauge.batteryusage; import static com.android.settings.Utils.formatPercentage; import static java.lang.Math.round; +import static java.util.Objects.requireNonNull; import android.accessibilityservice.AccessibilityServiceInfo; import android.content.Context; @@ -29,8 +30,6 @@ import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.os.Handler; -import android.text.format.DateFormat; -import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Log; import android.view.HapticFeedbackConstants; @@ -39,6 +38,7 @@ import android.view.View; import android.view.accessibility.AccessibilityManager; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.appcompat.widget.AppCompatImageView; @@ -46,7 +46,7 @@ import com.android.settings.R; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.Utils; -import java.time.Clock; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -58,36 +58,26 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick private static final List ACCESSIBILITY_SERVICE_NAMES = Arrays.asList("SwitchAccessService", "TalkBackService", "JustSpeakService"); - private static final int DEFAULT_TRAPEZOID_COUNT = 12; - private static final int DEFAULT_TIMESTAMP_COUNT = 4; - private static final int TIMESTAMP_GAPS_COUNT = DEFAULT_TIMESTAMP_COUNT - 1; private static final int DIVIDER_COLOR = Color.parseColor("#CDCCC5"); private static final long UPDATE_STATE_DELAYED_TIME = 500L; - /** Selects all trapezoid shapes. */ - public static final int SELECTED_INDEX_ALL = -1; - public static final int SELECTED_INDEX_INVALID = -2; - /** A callback listener for selected group index is updated. */ public interface OnSelectListener { /** The callback function for selected group index is updated. */ void onSelect(int trapezoidIndex); } + private BatteryChartViewModel mViewModel; + private int mDividerWidth; private int mDividerHeight; - private int mTrapezoidCount; private float mTrapezoidVOffset; private float mTrapezoidHOffset; private boolean mIsSlotsClickabled; private String[] mPercentages = getPercentages(); @VisibleForTesting - int mHoveredIndex = SELECTED_INDEX_INVALID; - @VisibleForTesting - int mSelectedIndex = SELECTED_INDEX_INVALID; - @VisibleForTesting - String[] mTimestamps; + int mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // Colors for drawing the trapezoid shape and dividers. private int mTrapezoidColor; @@ -98,25 +88,26 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick private final Rect mIndent = new Rect(); private final Rect[] mPercentageBounds = new Rect[]{new Rect(), new Rect(), new Rect()}; - // For drawing the timestamp information. - private final Rect[] mTimestampsBounds = - new Rect[]{new Rect(), new Rect(), new Rect(), new Rect()}; + // For drawing the axis label information. + private final List mAxisLabelsBounds = new ArrayList<>(); + @VisibleForTesting Handler mHandler = new Handler(); @VisibleForTesting final Runnable mUpdateClickableStateRun = () -> updateClickableState(); - private int[] mLevels; private Paint mTextPaint; private Paint mDividerPaint; private Paint mTrapezoidPaint; @VisibleForTesting Paint mTrapezoidCurvePaint = null; - private TrapezoidSlot[] mTrapezoidSlots; + @VisibleForTesting + TrapezoidSlot[] mTrapezoidSlots; // Records the location to calculate selected index. - private float mTouchUpEventX = Float.MIN_VALUE; + @VisibleForTesting + float mTouchUpEventX = Float.MIN_VALUE; private BatteryChartView.OnSelectListener mOnSelectListener; public BatteryChartView(Context context) { @@ -128,57 +119,25 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick initializeColors(context); // Registers the click event listener. setOnClickListener(this); - setSelectedIndex(SELECTED_INDEX_ALL); - setTrapezoidCount(DEFAULT_TRAPEZOID_COUNT); setClickable(false); - setLatestTimestamp(0); + requestLayout(); } - /** Sets the total trapezoid count for drawing. */ - public void setTrapezoidCount(int trapezoidCount) { - Log.i(TAG, "trapezoidCount:" + trapezoidCount); - mTrapezoidCount = trapezoidCount; - mTrapezoidSlots = new TrapezoidSlot[trapezoidCount]; - // Allocates the trapezoid slot array. - for (int index = 0; index < trapezoidCount; index++) { - mTrapezoidSlots[index] = new TrapezoidSlot(); - } - invalidate(); - } - - /** Sets all levels value to draw the trapezoid shape */ - public void setLevels(int[] levels) { - Log.d(TAG, "setLevels() " + (levels == null ? "null" : levels.length)); - if (levels == null) { - mLevels = null; - return; - } - // We should provide trapezoid count + 1 data to draw all trapezoids. - mLevels = levels.length == mTrapezoidCount + 1 ? levels : null; - setClickable(false); - invalidate(); - if (mLevels == null) { - return; - } - // Sets the chart is clickable if there is at least one valid item in it. - for (int index = 0; index < mLevels.length - 1; index++) { - if (mLevels[index] != 0 && mLevels[index + 1] != 0) { - setClickable(true); - break; - } - } - } - - /** Sets the selected group index to draw highlight effect. */ - public void setSelectedIndex(int index) { - if (mSelectedIndex != index) { - mSelectedIndex = index; + /** Sets the data model of this view. */ + public void setViewModel(BatteryChartViewModel viewModel) { + if (viewModel == null) { + mViewModel = null; invalidate(); - // Callbacks to the listener if we have. - if (mOnSelectListener != null) { - mOnSelectListener.onSelect(mSelectedIndex); - } + return; } + + Log.d(TAG, String.format("setViewModel(): size: %d, selectedIndex: %d.", + viewModel.size(), viewModel.selectedIndex())); + mViewModel = viewModel; + initializeAxisLabelsBounds(); + initializeTrapezoidSlots(viewModel.size() - 1); + setClickable(hasAnyValidTrapezoid(viewModel)); + requestLayout(); } /** Sets the callback to monitor the selected group index. */ @@ -195,29 +154,6 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick } else { mTextPaint = null; } - setVisibility(View.VISIBLE); - requestLayout(); - } - - /** Sets the latest timestamp for drawing into x-axis information. */ - public void setLatestTimestamp(long latestTimestamp) { - if (latestTimestamp == 0) { - latestTimestamp = Clock.systemUTC().millis(); - } - if (mTimestamps == null) { - mTimestamps = new String[DEFAULT_TIMESTAMP_COUNT]; - } - final long timeSlotOffset = - DateUtils.HOUR_IN_MILLIS * (/*total 24 hours*/ 24 / TIMESTAMP_GAPS_COUNT); - final boolean is24HourFormat = DateFormat.is24HourFormat(getContext()); - for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) { - mTimestamps[index] = - ConvertUtils.utcToLocalTimeHour( - getContext(), - latestTimestamp - (TIMESTAMP_GAPS_COUNT - index) - * timeSlotOffset, - is24HourFormat); - } requestLayout(); } @@ -226,6 +162,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick super.onMeasure(widthMeasureSpec, heightMeasureSpec); // Measures text bounds and updates indent configuration. if (mTextPaint != null) { + mTextPaint.setTextAlign(Paint.Align.LEFT); for (int index = 0; index < mPercentages.length; index++) { mTextPaint.getTextBounds( mPercentages[index], 0, mPercentages[index].length(), @@ -235,15 +172,14 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick mIndent.top = mPercentageBounds[0].height(); mIndent.right = mPercentageBounds[0].width() + mTextPadding; - if (mTimestamps != null) { - int maxHeight = 0; - for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) { - mTextPaint.getTextBounds( - mTimestamps[index], 0, mTimestamps[index].length(), - mTimestampsBounds[index]); - maxHeight = Math.max(maxHeight, mTimestampsBounds[index].height()); + if (mViewModel != null) { + int maxTop = 0; + for (int index = 0; index < mViewModel.size(); index++) { + final String text = mViewModel.texts().get(index); + mTextPaint.getTextBounds(text, 0, text.length(), mAxisLabelsBounds.get(index)); + maxTop = Math.max(maxTop, -mAxisLabelsBounds.get(index).top); } - mIndent.bottom = maxHeight + round(mTextPadding * 1.5f); + mIndent.bottom = maxTop + round(mTextPadding * 2f); } Log.d(TAG, "setIndent:" + mPercentageBounds[0]); } else { @@ -254,7 +190,12 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick @Override public void draw(Canvas canvas) { super.draw(canvas); + // Before mLevels initialized, the count of trapezoids is unknown. Only draws the + // horizontal percentages and dividers. drawHorizontalDividers(canvas); + if (mViewModel == null) { + return; + } drawVerticalDividers(canvas); drawTrapezoids(canvas); } @@ -294,7 +235,7 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick public void onHoverChanged(boolean hovered) { super.onHoverChanged(hovered); if (!hovered) { - mHoveredIndex = SELECTED_INDEX_INVALID; // reset + mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset invalidate(); } } @@ -307,15 +248,15 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick } final int trapezoidIndex = getTrapezoidIndex(mTouchUpEventX); // Ignores the click event if the level is zero. - if (trapezoidIndex == SELECTED_INDEX_INVALID - || !isValidToDraw(trapezoidIndex)) { + if (trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID + || !isValidToDraw(mViewModel, trapezoidIndex)) { return; } - // Selects all if users click the same trapezoid item two times. - if (trapezoidIndex == mSelectedIndex) { - setSelectedIndex(SELECTED_INDEX_ALL); - } else { - setSelectedIndex(trapezoidIndex); + if (mOnSelectListener != null) { + // Selects all if users click the same trapezoid item two times. + mOnSelectListener.onSelect( + trapezoidIndex == mViewModel.selectedIndex() + ? BatteryChartViewModel.SELECTED_INDEX_ALL : trapezoidIndex); } view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK); } @@ -364,8 +305,8 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick mTrapezoidCurvePaint.setStrokeWidth(mDividerWidth * 2); } else if (mIsSlotsClickabled) { mTrapezoidCurvePaint = null; - // Sets levels again to force update the click state. - setLevels(mLevels); + // Sets view model again to force update the click state. + setViewModel(mViewModel); } invalidate(); } @@ -380,6 +321,13 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick super.setClickable(clickable); } + private void initializeTrapezoidSlots(int count) { + mTrapezoidSlots = new TrapezoidSlot[count]; + for (int index = 0; index < mTrapezoidSlots.length; index++) { + mTrapezoidSlots[index] = new TrapezoidSlot(); + } + } + private void initializeColors(Context context) { setBackgroundColor(Color.TRANSPARENT); mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context); @@ -434,10 +382,10 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick private void drawPercentage(Canvas canvas, int index, float offsetY) { if (mTextPaint != null) { + mTextPaint.setTextAlign(Paint.Align.RIGHT); canvas.drawText( mPercentages[index], - getWidth() - mPercentageBounds[index].width() - - mPercentageBounds[index].left, + getWidth(), offsetY + mPercentageBounds[index].height() * .5f, mTextPaint); } @@ -445,9 +393,9 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick private void drawVerticalDividers(Canvas canvas) { final int width = getWidth() - mIndent.right; - final int dividerCount = mTrapezoidCount + 1; + final int dividerCount = mTrapezoidSlots.length + 1; final float dividerSpace = dividerCount * mDividerWidth; - final float unitWidth = (width - dividerSpace) / (float) mTrapezoidCount; + final float unitWidth = (width - dividerSpace) / (float) mTrapezoidSlots.length; final float bottomY = getHeight() - mIndent.bottom; final float startY = bottomY - mDividerHeight; final float trapezoidSlotOffset = mTrapezoidHOffset + mDividerWidth * .5f; @@ -463,66 +411,140 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick } startX = nextX; } - // Draws the timestamp slot information. - if (mTimestamps != null) { - final float[] xOffsets = new float[DEFAULT_TIMESTAMP_COUNT]; - final float baselineX = mDividerWidth * .5f; - final float offsetX = mDividerWidth + unitWidth; - final int slotBarOffset = (/*total 12 bars*/ 12) / TIMESTAMP_GAPS_COUNT; - for (int index = 0; index < DEFAULT_TIMESTAMP_COUNT; index++) { - xOffsets[index] = baselineX + index * offsetX * slotBarOffset; + // Draws the axis label slot information. + if (mViewModel != null) { + final float baselineY = getHeight() - mTextPadding * 1.5f; + Rect[] axisLabelDisplayAreas; + switch (mViewModel.axisLabelPosition()) { + case CENTER_OF_TRAPEZOIDS: + axisLabelDisplayAreas = getAxisLabelDisplayAreas( + /* size= */ mViewModel.size() - 1, + /* baselineX= */ mDividerWidth + unitWidth * .5f, + /* offsetX= */ mDividerWidth + unitWidth, + baselineY, + /* shiftFirstAndLast= */ false); + break; + case BETWEEN_TRAPEZOIDS: + default: + axisLabelDisplayAreas = getAxisLabelDisplayAreas( + /* size= */ mViewModel.size(), + /* baselineX= */ mDividerWidth * .5f, + /* offsetX= */ mDividerWidth + unitWidth, + baselineY, + /* shiftFirstAndLast= */ true); + break; } - drawTimestamp(canvas, xOffsets); + drawAxisLabels(canvas, axisLabelDisplayAreas, baselineY); } } - private void drawTimestamp(Canvas canvas, float[] xOffsets) { - // Draws the 1st timestamp info. - canvas.drawText( - mTimestamps[0], - xOffsets[0] - mTimestampsBounds[0].left, - getTimestampY(0), mTextPaint); - final int latestIndex = DEFAULT_TIMESTAMP_COUNT - 1; - // Draws the last timestamp info. - canvas.drawText( - mTimestamps[latestIndex], - xOffsets[latestIndex] - mTimestampsBounds[latestIndex].width() - - mTimestampsBounds[latestIndex].left, - getTimestampY(latestIndex), mTextPaint); - // Draws the rest of timestamp info since it is located in the center. - for (int index = 1; index <= DEFAULT_TIMESTAMP_COUNT - 2; index++) { - canvas.drawText( - mTimestamps[index], - xOffsets[index] - - (mTimestampsBounds[index].width() - mTimestampsBounds[index].left) - * .5f, - getTimestampY(index), mTextPaint); + /** Gets all the axis label texts displaying area positions if they are shown. */ + private Rect[] getAxisLabelDisplayAreas(final int size, final float baselineX, + final float offsetX, final float baselineY, final boolean shiftFirstAndLast) { + final Rect[] result = new Rect[size]; + for (int index = 0; index < result.length; index++) { + final float width = mAxisLabelsBounds.get(index).width(); + float middle = baselineX + index * offsetX; + if (shiftFirstAndLast) { + if (index == 0) { + middle += width * .5f; + } + if (index == size - 1) { + middle -= width * .5f; + } + } + final float left = middle - width * .5f; + final float right = left + width; + final float top = baselineY + mAxisLabelsBounds.get(index).top; + final float bottom = top + mAxisLabelsBounds.get(index).height(); + result[index] = new Rect(round(left), round(top), round(right), round(bottom)); + } + return result; + } + private void drawAxisLabels(Canvas canvas, final Rect[] displayAreas, final float baselineY) { + final int lastIndex = displayAreas.length - 1; + // Suppose first and last labels are always able to draw. + drawAxisLabelText(canvas, 0, displayAreas[0], baselineY); + drawAxisLabelText(canvas, lastIndex, displayAreas[lastIndex], baselineY); + drawAxisLabelsBetweenStartIndexAndEndIndex(canvas, displayAreas, 0, lastIndex, baselineY); + } + + /** + * Recursively draws axis labels between the start index and the end index. If the inner number + * can be exactly divided into 2 parts, check and draw the middle index label and then + * recursively draw the 2 parts. Otherwise, divide into 3 parts. Check and draw the middle two + * labels and then recursively draw the 3 parts. If there are any overlaps, skip drawing and go + * back to the uplevel of the recursion. + */ + private void drawAxisLabelsBetweenStartIndexAndEndIndex(Canvas canvas, + final Rect[] displayAreas, final int startIndex, final int endIndex, + final float baselineY) { + if (endIndex - startIndex <= 1) { + return; + } + if ((endIndex - startIndex) % 2 == 0) { + int middleIndex = (startIndex + endIndex) / 2; + if (hasOverlap(displayAreas, startIndex, middleIndex) + || hasOverlap(displayAreas, middleIndex, endIndex)) { + return; + } + drawAxisLabelText(canvas, middleIndex, displayAreas[middleIndex], baselineY); + drawAxisLabelsBetweenStartIndexAndEndIndex( + canvas, displayAreas, startIndex, middleIndex, baselineY); + drawAxisLabelsBetweenStartIndexAndEndIndex( + canvas, displayAreas, middleIndex, endIndex, baselineY); + } else { + int middleIndex1 = startIndex + round((endIndex - startIndex) / 3f); + int middleIndex2 = startIndex + round((endIndex - startIndex) * 2 / 3f); + if (hasOverlap(displayAreas, startIndex, middleIndex1) + || hasOverlap(displayAreas, middleIndex1, middleIndex2) + || hasOverlap(displayAreas, middleIndex2, endIndex)) { + return; + } + drawAxisLabelText(canvas, middleIndex1, displayAreas[middleIndex1], baselineY); + drawAxisLabelText(canvas, middleIndex2, displayAreas[middleIndex2], baselineY); + drawAxisLabelsBetweenStartIndexAndEndIndex( + canvas, displayAreas, startIndex, middleIndex1, baselineY); + drawAxisLabelsBetweenStartIndexAndEndIndex( + canvas, displayAreas, middleIndex1, middleIndex2, baselineY); + drawAxisLabelsBetweenStartIndexAndEndIndex( + canvas, displayAreas, middleIndex2, endIndex, baselineY); } } - private int getTimestampY(int index) { - return getHeight() - mTimestampsBounds[index].height() - + (mTimestampsBounds[index].height() + mTimestampsBounds[index].top) - + round(mTextPadding * 1.5f); + private boolean hasOverlap( + final Rect[] displayAreas, final int leftIndex, final int rightIndex) { + return displayAreas[leftIndex].right + mTextPadding * 2f > displayAreas[rightIndex].left; + } + + private void drawAxisLabelText( + Canvas canvas, final int index, final Rect displayArea, final float baselineY) { + mTextPaint.setTextAlign(Paint.Align.CENTER); + canvas.drawText( + mViewModel.texts().get(index), + displayArea.centerX(), + baselineY, + mTextPaint); } private void drawTrapezoids(Canvas canvas) { // Ignores invalid trapezoid data. - if (mLevels == null) { + if (mViewModel == null) { return; } final float trapezoidBottom = getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth - mTrapezoidVOffset; - final float availableSpace = trapezoidBottom - mDividerWidth * .5f - mIndent.top; + final float availableSpace = + trapezoidBottom - mDividerWidth * .5f - mIndent.top - mTrapezoidVOffset; final float unitHeight = availableSpace / 100f; // Draws all trapezoid shapes into the canvas. final Path trapezoidPath = new Path(); Path trapezoidCurvePath = null; - for (int index = 0; index < mTrapezoidCount; index++) { + for (int index = 0; index < mTrapezoidSlots.length; index++) { // Not draws the trapezoid for corner or not initialization cases. - if (!isValidToDraw(index)) { + if (!isValidToDraw(mViewModel, index)) { if (mTrapezoidCurvePaint != null && trapezoidCurvePath != null) { canvas.drawPath(trapezoidCurvePath, mTrapezoidCurvePaint); trapezoidCurvePath = null; @@ -530,17 +552,18 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick continue; } // Configures the trapezoid paint color. - final int trapezoidColor = - !mIsSlotsClickabled - ? mTrapezoidColor - : mSelectedIndex == index || mSelectedIndex == SELECTED_INDEX_ALL - ? mTrapezoidSolidColor : mTrapezoidColor; + final int trapezoidColor = mIsSlotsClickabled && (mViewModel.selectedIndex() == index + || mViewModel.selectedIndex() == BatteryChartViewModel.SELECTED_INDEX_ALL) + ? mTrapezoidSolidColor : mTrapezoidColor; final boolean isHoverState = - mIsSlotsClickabled && mHoveredIndex == index && isValidToDraw(mHoveredIndex); + mIsSlotsClickabled && mHoveredIndex == index + && isValidToDraw(mViewModel, mHoveredIndex); mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor); - final float leftTop = round(trapezoidBottom - mLevels[index] * unitHeight); - final float rightTop = round(trapezoidBottom - mLevels[index + 1] * unitHeight); + final float leftTop = round( + trapezoidBottom - requireNonNull(mViewModel.levels().get(index)) * unitHeight); + final float rightTop = round(trapezoidBottom + - requireNonNull(mViewModel.levels().get(index + 1)) * unitHeight); trapezoidPath.reset(); trapezoidPath.moveTo(mTrapezoidSlots[index].mLeft, trapezoidBottom); trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop); @@ -579,15 +602,37 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick return index; } } - return SELECTED_INDEX_INVALID; + return BatteryChartViewModel.SELECTED_INDEX_INVALID; } - private boolean isValidToDraw(int trapezoidIndex) { - return mLevels != null + private void initializeAxisLabelsBounds() { + mAxisLabelsBounds.clear(); + for (int i = 0; i < mViewModel.size(); i++) { + mAxisLabelsBounds.add(new Rect()); + } + } + + private static boolean isTrapezoidValid( + @NonNull BatteryChartViewModel viewModel, int trapezoidIndex) { + return viewModel.levels().get(trapezoidIndex) != null + && viewModel.levels().get(trapezoidIndex + 1) != null; + } + + private static boolean isValidToDraw(BatteryChartViewModel viewModel, int trapezoidIndex) { + return viewModel != null && trapezoidIndex >= 0 - && trapezoidIndex < mLevels.length - 1 - && mLevels[trapezoidIndex] != 0 - && mLevels[trapezoidIndex + 1] != 0; + && trapezoidIndex < viewModel.size() - 1 + && isTrapezoidValid(viewModel, trapezoidIndex); + } + + private static boolean hasAnyValidTrapezoid(@NonNull BatteryChartViewModel viewModel) { + // Sets the chart is clickable if there is at least one valid item in it. + for (int trapezoidIndex = 0; trapezoidIndex < viewModel.size() - 1; trapezoidIndex++) { + if (isTrapezoidValid(viewModel, trapezoidIndex)) { + return true; + } + } + return false; } private static String[] getPercentages() { @@ -621,7 +666,8 @@ public class BatteryChartView extends AppCompatImageView implements View.OnClick } // A container class for each trapezoid left and right location. - private static final class TrapezoidSlot { + @VisibleForTesting + static final class TrapezoidSlot { public float mLeft; public float mRight; diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewModel.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewModel.java new file mode 100644 index 00000000000..ac01bfd645b --- /dev/null +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewModel.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 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 androidx.annotation.NonNull; +import androidx.core.util.Preconditions; + +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +/** The view model of {@code BatteryChartView} */ +class BatteryChartViewModel { + private static final String TAG = "BatteryChartViewModel"; + + public static final int SELECTED_INDEX_ALL = -1; + public static final int SELECTED_INDEX_INVALID = -2; + + // We need at least 2 levels to draw a trapezoid. + private static final int MIN_LEVELS_DATA_SIZE = 2; + + enum AxisLabelPosition { + BETWEEN_TRAPEZOIDS, + CENTER_OF_TRAPEZOIDS, + } + + private final List mLevels; + private final List mTexts; + private final AxisLabelPosition mAxisLabelPosition; + private int mSelectedIndex = SELECTED_INDEX_ALL; + + BatteryChartViewModel( + @NonNull List levels, @NonNull List texts, + @NonNull AxisLabelPosition axisLabelPosition) { + Preconditions.checkArgument( + levels.size() == texts.size() && levels.size() >= MIN_LEVELS_DATA_SIZE, + String.format(Locale.ENGLISH, + "Invalid BatteryChartViewModel levels.size: %d, texts.size: %d.", + levels.size(), texts.size())); + mLevels = levels; + mTexts = texts; + mAxisLabelPosition = axisLabelPosition; + } + + public int size() { + return mLevels.size(); + } + + public List levels() { + return mLevels; + } + + public List texts() { + return mTexts; + } + + public AxisLabelPosition axisLabelPosition() { + return mAxisLabelPosition; + } + + public int selectedIndex() { + return mSelectedIndex; + } + + public void setSelectedIndex(int index) { + mSelectedIndex = index; + } + + @Override + public int hashCode() { + return Objects.hash(mLevels, mTexts, mSelectedIndex, mAxisLabelPosition); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (!(other instanceof BatteryChartViewModel)) { + return false; + } + final BatteryChartViewModel batteryChartViewModel = (BatteryChartViewModel) other; + return Objects.equals(mLevels, batteryChartViewModel.mLevels) + && Objects.equals(mTexts, batteryChartViewModel.mTexts) + && mAxisLabelPosition == batteryChartViewModel.mAxisLabelPosition + && mSelectedIndex == batteryChartViewModel.mSelectedIndex; + } + + @Override + public String toString() { + return String.format(Locale.ENGLISH, + "levels: %s,\ntexts: %s,\naxisLabelPosition: %s, selectedIndex: %d", + Objects.toString(mLevels), Objects.toString(mTexts), mAxisLabelPosition, + mSelectedIndex); + } +} diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java new file mode 100644 index 00000000000..b5d4dde883c --- /dev/null +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryDiffData.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2022 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 androidx.annotation.NonNull; + +import java.util.Collections; +import java.util.List; + +/** Wraps the battery usage diff data for each entry used for battery usage app list. */ +public class BatteryDiffData { + private final List mAppEntries; + private final List mSystemEntries; + + /** Constructor for the diff entries which already have totalConsumePower value. */ + public BatteryDiffData( + @NonNull List appDiffEntries, + @NonNull List systemDiffEntries) { + mAppEntries = appDiffEntries; + mSystemEntries = systemDiffEntries; + sortEntries(); + } + + /** Constructor for the diff entries which have not set totalConsumePower value. */ + public BatteryDiffData( + @NonNull List appDiffEntries, + @NonNull List systemDiffEntries, + final double totalConsumePower) { + mAppEntries = appDiffEntries; + mSystemEntries = systemDiffEntries; + setTotalConsumePowerForAllEntries(totalConsumePower); + sortEntries(); + } + + public List getAppDiffEntryList() { + return mAppEntries; + } + + public List getSystemDiffEntryList() { + return mSystemEntries; + } + + // Sets total consume power for each entry. + private void setTotalConsumePowerForAllEntries(final double totalConsumePower) { + mAppEntries.forEach(diffEntry -> diffEntry.setTotalConsumePower(totalConsumePower)); + mSystemEntries.forEach(diffEntry -> diffEntry.setTotalConsumePower(totalConsumePower)); + } + + // Sorts entries based on consumed percentage. + private void sortEntries() { + Collections.sort(mAppEntries, BatteryDiffEntry.COMPARATOR); + Collections.sort(mSystemEntries, BatteryDiffEntry.COMPARATOR); + } +} diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java index 34606a5a583..83b26150d39 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoader.java @@ -43,6 +43,6 @@ public class BatteryHistoryLoader public Map> loadInBackground() { final PowerUsageFeatureProvider powerUsageFeatureProvider = FeatureFactory.getFactory(mContext).getPowerUsageFeatureProvider(mContext); - return powerUsageFeatureProvider.getBatteryHistory(mContext); + return powerUsageFeatureProvider.getBatteryHistorySinceLastFullCharge(mContext); } } diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java index e125d17d6a3..71fd26ce95c 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryPreference.java @@ -49,7 +49,8 @@ public class BatteryHistoryPreference extends Preference { private TextView mSummaryView; private CharSequence mSummaryContent; - private BatteryChartView mBatteryChartView; + private BatteryChartView mDailyChartView; + private BatteryChartView mHourlyChartView; private BatteryChartPreferenceController mChartPreferenceController; public BatteryHistoryPreference(Context context, AttributeSet attrs) { @@ -92,8 +93,8 @@ public class BatteryHistoryPreference extends Preference { void setChartPreferenceController(BatteryChartPreferenceController controller) { mChartPreferenceController = controller; - if (mBatteryChartView != null) { - mChartPreferenceController.setBatteryChartView(mBatteryChartView); + if (mDailyChartView != null && mHourlyChartView != null) { + mChartPreferenceController.setBatteryChartView(mDailyChartView, mHourlyChartView); } } @@ -105,11 +106,14 @@ public class BatteryHistoryPreference extends Preference { return; } if (mIsChartGraphEnabled) { - mBatteryChartView = (BatteryChartView) view.findViewById(R.id.battery_chart); - mBatteryChartView.setCompanionTextView( + mDailyChartView = (BatteryChartView) view.findViewById(R.id.daily_battery_chart); + mDailyChartView.setCompanionTextView( + (TextView) view.findViewById(R.id.companion_text)); + mHourlyChartView = (BatteryChartView) view.findViewById(R.id.hourly_battery_chart); + mHourlyChartView.setCompanionTextView( (TextView) view.findViewById(R.id.companion_text)); if (mChartPreferenceController != null) { - mChartPreferenceController.setBatteryChartView(mBatteryChartView); + mChartPreferenceController.setBatteryChartView(mDailyChartView, mHourlyChartView); } } else { final TextView chargeView = (TextView) view.findViewById(R.id.charge); diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java new file mode 100644 index 00000000000..4ff9eeba9b2 --- /dev/null +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryLevelData.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 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 androidx.annotation.NonNull; +import androidx.core.util.Preconditions; + +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +/** Wraps the battery timestamp and level data used for battery usage chart. */ +public final class BatteryLevelData { + /** A container for the battery timestamp and level data. */ + public static final class PeriodBatteryLevelData { + // The length of mTimestamps and mLevels must be the same. mLevels[index] might be null when + // there is no level data for the corresponding timestamp. + private final List mTimestamps; + private final List mLevels; + + public PeriodBatteryLevelData( + @NonNull List timestamps, @NonNull List levels) { + Preconditions.checkArgument(timestamps.size() == levels.size(), + /* errorMessage= */ "Timestamp: " + timestamps.size() + ", Level: " + + levels.size()); + mTimestamps = timestamps; + mLevels = levels; + } + + public List getTimestamps() { + return mTimestamps; + } + + public List getLevels() { + return mLevels; + } + + @Override + public String toString() { + return String.format(Locale.ENGLISH, "timestamps: %s; levels: %s", + Objects.toString(mTimestamps), Objects.toString(mLevels)); + } + } + + /** + * There could be 2 cases for the daily battery levels: + * 1) length is 2: The usage data is within 1 day. Only contains start and end data, such as + * data of 2022-01-01 06:00 and 2022-01-01 16:00. + * 2) length > 2: The usage data is more than 1 days. The data should be the start, end and 0am + * data of every day between the start and end, such as data of 2022-01-01 06:00, + * 2022-01-02 00:00, 2022-01-03 00:00 and 2022-01-03 08:00. + */ + private final PeriodBatteryLevelData mDailyBatteryLevels; + // The size of hourly data must be the size of daily data - 1. + private final List mHourlyBatteryLevelsPerDay; + + public BatteryLevelData( + @NonNull PeriodBatteryLevelData dailyBatteryLevels, + @NonNull List hourlyBatteryLevelsPerDay) { + final long dailySize = dailyBatteryLevels.getTimestamps().size(); + final long hourlySize = hourlyBatteryLevelsPerDay.size(); + Preconditions.checkArgument(hourlySize == dailySize - 1, + /* errorMessage= */ "DailySize: " + dailySize + ", HourlySize: " + hourlySize); + mDailyBatteryLevels = dailyBatteryLevels; + mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay; + } + + public PeriodBatteryLevelData getDailyBatteryLevels() { + return mDailyBatteryLevels; + } + + public List getHourlyBatteryLevelsPerDay() { + return mHourlyBatteryLevelsPerDay; + } + + @Override + public String toString() { + return String.format(Locale.ENGLISH, + "dailyBatteryLevels: %s; hourlyBatteryLevelsPerDay: %s", + Objects.toString(mDailyBatteryLevels), + Objects.toString(mHourlyBatteryLevelsPerDay)); + } +} + diff --git a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java index 168fe0f5383..f04658d83a4 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java +++ b/src/com/android/settings/fuelgauge/batteryusage/ConvertUtils.java @@ -140,7 +140,7 @@ public final class ConvertUtils { /** Converts UTC timestamp to local time hour data. */ public static String utcToLocalTimeHour( - Context context, long timestamp, boolean is24HourFormat) { + final Context context, final long timestamp, final boolean is24HourFormat) { final Locale locale = getLocale(context); // e.g. for 12-hour format: 9 pm // e.g. for 24-hour format: 09:00 @@ -149,6 +149,15 @@ public final class ConvertUtils { return DateFormat.format(pattern, timestamp).toString().toLowerCase(locale); } + /** Converts UTC timestamp to local time day of week data. */ + public static String utcToLocalTimeDayOfWeek( + final Context context, final long timestamp, final boolean isAbbreviation) { + final Locale locale = getLocale(context); + final String pattern = DateFormat.getBestDateTimePattern(locale, + isAbbreviation ? "E" : "EEEE"); + return DateFormat.format(pattern, timestamp).toString(); + } + /** Gets indexed battery usage data for each corresponding time slot. */ public static Map> getIndexedUsageMap( final Context context, @@ -267,7 +276,7 @@ public final class ConvertUtils { diffEntry.setTotalConsumePower(totalConsumePower); } } - insert24HoursData(BatteryChartView.SELECTED_INDEX_ALL, resultMap); + insert24HoursData(BatteryChartViewModel.SELECTED_INDEX_ALL, resultMap); resolveMultiUsersData(context, resultMap); if (purgeLowPercentageAndFakeData) { purgeLowPercentageAndFakeData(context, resultMap); diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java new file mode 100644 index 00000000000..125f879abff --- /dev/null +++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java @@ -0,0 +1,1058 @@ +/* + * Copyright (C) 2022 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.android.settings.fuelgauge.batteryusage.ConvertUtils.utcToLocalTime; + +import android.content.ContentValues; +import android.content.Context; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.util.ArraySet; +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.Utils; +import com.android.settings.fuelgauge.BatteryUtils; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.fuelgauge.BatteryStatus; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A utility class to process data loaded from database and make the data easy to use for battery + * usage UI. + */ +public final class DataProcessor { + private static final boolean DEBUG = false; + private static final String TAG = "DataProcessor"; + private static final int MIN_DAILY_DATA_SIZE = 2; + private static final int MIN_TIMESTAMP_DATA_SIZE = 2; + private static final int MAX_DIFF_SECONDS_OF_UPPER_TIMESTAMP = 5; + // Maximum total time value for each hourly slot cumulative data at most 2 hours. + private static final float TOTAL_HOURLY_TIME_THRESHOLD = DateUtils.HOUR_IN_MILLIS * 2; + private static final Map EMPTY_BATTERY_MAP = new HashMap<>(); + private static final BatteryHistEntry EMPTY_BATTERY_HIST_ENTRY = + new BatteryHistEntry(new ContentValues()); + + @VisibleForTesting + static final double PERCENTAGE_OF_TOTAL_THRESHOLD = 1f; + @VisibleForTesting + static final int SELECTED_INDEX_ALL = BatteryChartViewModel.SELECTED_INDEX_ALL; + + /** A fake package name to represent no BatteryEntry data. */ + public static final String FAKE_PACKAGE_NAME = "fake_package"; + + /** A callback listener when battery usage loading async task is executed. */ + public interface UsageMapAsyncResponse { + /** The callback function when batteryUsageMap is loaded. */ + void onBatteryUsageMapLoaded( + Map> batteryUsageMap); + } + + private DataProcessor() { + } + + /** + * @return Returns battery level data and start async task to compute battery diff usage data + * and load app labels + icons. + * Returns null if the input is invalid or not having at least 2 hours data. + */ + @Nullable + public static BatteryLevelData getBatteryLevelData( + Context context, + @Nullable Handler handler, + @Nullable final Map> batteryHistoryMap, + final UsageMapAsyncResponse asyncResponseDelegate) { + if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) { + Log.d(TAG, "getBatteryLevelData() returns null"); + return null; + } + handler = handler != null ? handler : new Handler(Looper.getMainLooper()); + // Process raw history map data into hourly timestamps. + final Map> processedBatteryHistoryMap = + getHistoryMapWithExpectedTimestamps(context, batteryHistoryMap); + // Wrap and processed history map into easy-to-use format for UI rendering. + final BatteryLevelData batteryLevelData = + getLevelDataThroughProcessedHistoryMap(context, processedBatteryHistoryMap); + + // Start the async task to compute diff usage data and load labels and icons. + if (batteryLevelData != null) { + new ComputeUsageMapAndLoadItemsTask( + context, + handler, + asyncResponseDelegate, + batteryLevelData.getHourlyBatteryLevelsPerDay(), + processedBatteryHistoryMap).execute(); + } + + return batteryLevelData; + } + + /** + * @return Returns battery usage data of different entries. + * Returns null if the input is invalid or there is no enough data. + */ + @Nullable + public static Map> getBatteryUsageData( + Context context, + @Nullable final Map> batteryHistoryMap) { + if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) { + Log.d(TAG, "getBatteryLevelData() returns null"); + return null; + } + // Process raw history map data into hourly timestamps. + final Map> processedBatteryHistoryMap = + getHistoryMapWithExpectedTimestamps(context, batteryHistoryMap); + // Wrap and processed history map into easy-to-use format for UI rendering. + final BatteryLevelData batteryLevelData = + getLevelDataThroughProcessedHistoryMap(context, processedBatteryHistoryMap); + return batteryLevelData == null + ? null + : getBatteryUsageMap( + context, + batteryLevelData.getHourlyBatteryLevelsPerDay(), + processedBatteryHistoryMap); + } + + /** + * @return Returns whether the target is in the CharSequence array. + */ + public static boolean contains(String target, CharSequence[] packageNames) { + if (target != null && packageNames != null) { + for (CharSequence packageName : packageNames) { + if (TextUtils.equals(target, packageName)) { + return true; + } + } + } + return false; + } + + /** + * @return Returns the processed history map which has interpolated to every hour data. + * The start and end timestamp must be the even hours. + * The keys of processed history map should contain every hour between the start and end + * timestamp. If there's no data in some key, the value will be the empty hashmap. + */ + @VisibleForTesting + static Map> getHistoryMapWithExpectedTimestamps( + Context context, + final Map> batteryHistoryMap) { + final long startTime = System.currentTimeMillis(); + final List rawTimestampList = new ArrayList<>(batteryHistoryMap.keySet()); + final Map> resultMap = new HashMap(); + if (rawTimestampList.isEmpty()) { + Log.d(TAG, "empty batteryHistoryMap in getHistoryMapWithExpectedTimestamps()"); + return resultMap; + } + Collections.sort(rawTimestampList); + final List expectedTimestampList = getTimestampSlots(rawTimestampList); + final boolean isFromFullCharge = + isFromFullCharge(batteryHistoryMap.get(rawTimestampList.get(0))); + interpolateHistory( + context, rawTimestampList, expectedTimestampList, isFromFullCharge, + batteryHistoryMap, resultMap); + Log.d(TAG, String.format("getHistoryMapWithExpectedTimestamps() size=%d in %d/ms", + resultMap.size(), (System.currentTimeMillis() - startTime))); + return resultMap; + } + + @VisibleForTesting + @Nullable + static BatteryLevelData getLevelDataThroughProcessedHistoryMap( + Context context, + final Map> processedBatteryHistoryMap) { + final List timestampList = new ArrayList<>(processedBatteryHistoryMap.keySet()); + Collections.sort(timestampList); + final List dailyTimestamps = getDailyTimestamps(timestampList); + // There should be at least the start and end timestamps. Otherwise, return null to not show + // data in usage chart. + if (dailyTimestamps.size() < MIN_DAILY_DATA_SIZE) { + return null; + } + + final List> hourlyTimestamps = getHourlyTimestamps(dailyTimestamps); + final BatteryLevelData.PeriodBatteryLevelData dailyLevelData = + getPeriodBatteryLevelData(context, processedBatteryHistoryMap, dailyTimestamps); + final List hourlyLevelData = + getHourlyPeriodBatteryLevelData( + context, processedBatteryHistoryMap, hourlyTimestamps); + return new BatteryLevelData(dailyLevelData, hourlyLevelData); + } + + /** + * Computes expected timestamp slots for last full charge, which will return hourly timestamps + * between start and end two even hour values. + */ + @VisibleForTesting + static List getTimestampSlots(final List rawTimestampList) { + final List timestampSlots = new ArrayList<>(); + final int rawTimestampListSize = rawTimestampList.size(); + // If timestamp number is smaller than 2, the following computation is not necessary. + if (rawTimestampListSize < MIN_TIMESTAMP_DATA_SIZE) { + return timestampSlots; + } + final long rawStartTimestamp = rawTimestampList.get(0); + final long rawEndTimestamp = rawTimestampList.get(rawTimestampListSize - 1); + // No matter the start is from last full charge or 6 days ago, use the nearest even hour. + final long startTimestamp = getNearestEvenHourTimestamp(rawStartTimestamp); + // Use the even hour before the raw end timestamp as the end. + final long endTimestamp = getLastEvenHourBeforeTimestamp(rawEndTimestamp); + // If the start timestamp is later or equal the end one, return the empty list. + if (startTimestamp >= endTimestamp) { + return timestampSlots; + } + for (long timestamp = startTimestamp; timestamp <= endTimestamp; + timestamp += DateUtils.HOUR_IN_MILLIS) { + timestampSlots.add(timestamp); + } + return timestampSlots; + } + + /** + * Computes expected daily timestamp slots. + * + * The valid result should be composed of 3 parts: + * 1) start timestamp + * 2) every 00:00 timestamp (default timezone) between the start and end + * 3) end timestamp + * Otherwise, returns an empty list. + */ + @VisibleForTesting + static List getDailyTimestamps(final List timestampList) { + final List dailyTimestampList = new ArrayList<>(); + // If timestamp number is smaller than 2, the following computation is not necessary. + if (timestampList.size() < MIN_TIMESTAMP_DATA_SIZE) { + return dailyTimestampList; + } + final long startTime = timestampList.get(0); + final long endTime = timestampList.get(timestampList.size() - 1); + long nextDay = getTimestampOfNextDay(startTime); + dailyTimestampList.add(startTime); + while (nextDay < endTime) { + dailyTimestampList.add(nextDay); + nextDay += DateUtils.DAY_IN_MILLIS; + } + dailyTimestampList.add(endTime); + return dailyTimestampList; + } + + @VisibleForTesting + static boolean isFromFullCharge(@Nullable final Map entryList) { + if (entryList == null) { + Log.d(TAG, "entryList is null in isFromFullCharge()"); + return false; + } + final List entryKeys = new ArrayList<>(entryList.keySet()); + if (entryKeys.isEmpty()) { + Log.d(TAG, "empty entryList in isFromFullCharge()"); + return false; + } + // The hist entries in the same timestamp should have same battery status and level. + // Checking the first one should be enough. + final BatteryHistEntry firstHistEntry = entryList.get(entryKeys.get(0)); + return BatteryStatus.isCharged(firstHistEntry.mBatteryStatus, firstHistEntry.mBatteryLevel); + } + + @VisibleForTesting + static long[] findNearestTimestamp(final List timestamps, final long target) { + final long[] results = new long[] {Long.MIN_VALUE, Long.MAX_VALUE}; + // Searches the nearest lower and upper timestamp value. + timestamps.forEach(timestamp -> { + if (timestamp <= target && timestamp > results[0]) { + results[0] = timestamp; + } + if (timestamp >= target && timestamp < results[1]) { + results[1] = timestamp; + } + }); + // Uses zero value to represent invalid searching result. + results[0] = results[0] == Long.MIN_VALUE ? 0 : results[0]; + results[1] = results[1] == Long.MAX_VALUE ? 0 : results[1]; + return results; + } + + /** + * @return Returns the timestamp for 00:00 1 day after the given timestamp based on local + * timezone. + */ + @VisibleForTesting + static long getTimestampOfNextDay(long timestamp) { + return getTimestampWithDayDiff(timestamp, /*dayDiff=*/ 1); + } + + /** + * Returns whether currentSlot will be used in daily chart. + */ + @VisibleForTesting + static boolean isForDailyChart(final boolean isStartOrEnd, final long currentSlot) { + // The start and end timestamps will always be used in daily chart. + if (isStartOrEnd) { + return true; + } + + // The timestamps for 00:00 will be used in daily chart. + final long startOfTheDay = getTimestampWithDayDiff(currentSlot, /*dayDiff=*/ 0); + return currentSlot == startOfTheDay; + } + + /** + * @return Returns the indexed battery usage data for each corresponding time slot. + * + * There could be 2 cases of the returned value: + * 1) null: empty or invalid data. + * 2) non-null: must be a 2d map and composed by 3 parts: + * 1 - [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL] + * 2 - [0][SELECTED_INDEX_ALL] ~ [maxDailyIndex][SELECTED_INDEX_ALL] + * 3 - [0][0] ~ [maxDailyIndex][maxHourlyIndex] + */ + @VisibleForTesting + @Nullable + static Map> getBatteryUsageMap( + final Context context, + final List hourlyBatteryLevelsPerDay, + final Map> batteryHistoryMap) { + if (batteryHistoryMap.isEmpty()) { + return null; + } + final Map> resultMap = new HashMap<>(); + // Insert diff data from [0][0] to [maxDailyIndex][maxHourlyIndex]. + insertHourlyUsageDiffData( + context, hourlyBatteryLevelsPerDay, batteryHistoryMap, resultMap); + // Insert diff data from [0][SELECTED_INDEX_ALL] to [maxDailyIndex][SELECTED_INDEX_ALL]. + insertDailyUsageDiffData(hourlyBatteryLevelsPerDay, resultMap); + // Insert diff data [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL]. + insertAllUsageDiffData(resultMap); + purgeLowPercentageAndFakeData(context, resultMap); + if (!isUsageMapValid(resultMap, hourlyBatteryLevelsPerDay)) { + return null; + } + return resultMap; + } + + /** + * Interpolates history map based on expected timestamp slots and processes the corner case when + * the expected start timestamp is earlier than what we have. + */ + private static void interpolateHistory( + Context context, + final List rawTimestampList, + final List expectedTimestampSlots, + final boolean isFromFullCharge, + final Map> batteryHistoryMap, + final Map> resultMap) { + if (rawTimestampList.isEmpty() || expectedTimestampSlots.isEmpty()) { + return; + } + final long expectedStartTimestamp = expectedTimestampSlots.get(0); + final long rawStartTimestamp = rawTimestampList.get(0); + int startIndex = 0; + // If the expected start timestamp is full charge or earlier than what we have, use the + // first data of what we have directly. This should be OK because the expected start + // timestamp is the nearest even hour of the raw start timestamp, their time diff is no + // more than 1 hour. + if (isFromFullCharge || expectedStartTimestamp < rawStartTimestamp) { + startIndex = 1; + resultMap.put(expectedStartTimestamp, batteryHistoryMap.get(rawStartTimestamp)); + } + final int expectedTimestampSlotsSize = expectedTimestampSlots.size(); + for (int index = startIndex; index < expectedTimestampSlotsSize; index++) { + final long currentSlot = expectedTimestampSlots.get(index); + final boolean isStartOrEnd = index == 0 || index == expectedTimestampSlotsSize - 1; + interpolateHistoryForSlot( + context, currentSlot, rawTimestampList, batteryHistoryMap, resultMap, + isStartOrEnd); + } + } + + private static void interpolateHistoryForSlot( + Context context, + final long currentSlot, + final List rawTimestampList, + final Map> batteryHistoryMap, + final Map> resultMap, + final boolean isStartOrEnd) { + final long[] nearestTimestamps = findNearestTimestamp(rawTimestampList, currentSlot); + final long lowerTimestamp = nearestTimestamps[0]; + final long upperTimestamp = nearestTimestamps[1]; + // Case 1: upper timestamp is zero since scheduler is delayed! + if (upperTimestamp == 0) { + log(context, "job scheduler is delayed", currentSlot, null); + resultMap.put(currentSlot, new HashMap<>()); + return; + } + // Case 2: upper timestamp is closed to the current timestamp. + if ((upperTimestamp - currentSlot) + < MAX_DIFF_SECONDS_OF_UPPER_TIMESTAMP * DateUtils.SECOND_IN_MILLIS) { + log(context, "force align into the nearest slot", currentSlot, null); + resultMap.put(currentSlot, batteryHistoryMap.get(upperTimestamp)); + return; + } + // Case 3: lower timestamp is zero before starting to collect data. + if (lowerTimestamp == 0) { + log(context, "no lower timestamp slot data", currentSlot, null); + resultMap.put(currentSlot, new HashMap<>()); + return; + } + interpolateHistoryForSlot(context, + currentSlot, lowerTimestamp, upperTimestamp, batteryHistoryMap, resultMap, + isStartOrEnd); + } + + private static void interpolateHistoryForSlot( + Context context, + final long currentSlot, + final long lowerTimestamp, + final long upperTimestamp, + final Map> batteryHistoryMap, + final Map> resultMap, + final boolean isStartOrEnd) { + final Map lowerEntryDataMap = + batteryHistoryMap.get(lowerTimestamp); + final Map upperEntryDataMap = + batteryHistoryMap.get(upperTimestamp); + // Verifies whether the lower data is valid to use or not by checking boot time. + final BatteryHistEntry upperEntryDataFirstEntry = + upperEntryDataMap.values().stream().findFirst().get(); + final long upperEntryDataBootTimestamp = + upperEntryDataFirstEntry.mTimestamp - upperEntryDataFirstEntry.mBootTimestamp; + // Lower data is captured before upper data corresponding device is booting. + // Skips the booting-specific logics and always does interpolation for daily chart level + // data. + if (lowerTimestamp < upperEntryDataBootTimestamp + && !isForDailyChart(isStartOrEnd, currentSlot)) { + // Provides an opportunity to force align the slot directly. + if ((upperTimestamp - currentSlot) < 10 * DateUtils.MINUTE_IN_MILLIS) { + log(context, "force align into the nearest slot", currentSlot, null); + resultMap.put(currentSlot, upperEntryDataMap); + } else { + log(context, "in the different booting section", currentSlot, null); + resultMap.put(currentSlot, new HashMap<>()); + } + return; + } + log(context, "apply interpolation arithmetic", currentSlot, null); + final Map newHistEntryMap = new HashMap<>(); + final double timestampLength = upperTimestamp - lowerTimestamp; + final double timestampDiff = currentSlot - lowerTimestamp; + // Applies interpolation arithmetic for each BatteryHistEntry. + for (String entryKey : upperEntryDataMap.keySet()) { + final BatteryHistEntry lowerEntry = lowerEntryDataMap.get(entryKey); + final BatteryHistEntry upperEntry = upperEntryDataMap.get(entryKey); + // Checks whether there is any abnormal battery reset conditions. + if (lowerEntry != null) { + final boolean invalidForegroundUsageTime = + lowerEntry.mForegroundUsageTimeInMs > upperEntry.mForegroundUsageTimeInMs; + final boolean invalidBackgroundUsageTime = + lowerEntry.mBackgroundUsageTimeInMs > upperEntry.mBackgroundUsageTimeInMs; + if (invalidForegroundUsageTime || invalidBackgroundUsageTime) { + newHistEntryMap.put(entryKey, upperEntry); + log(context, "abnormal reset condition is found", currentSlot, upperEntry); + continue; + } + } + final BatteryHistEntry newEntry = + BatteryHistEntry.interpolate( + currentSlot, + upperTimestamp, + /*ratio=*/ timestampDiff / timestampLength, + lowerEntry, + upperEntry); + newHistEntryMap.put(entryKey, newEntry); + if (lowerEntry == null) { + log(context, "cannot find lower entry data", currentSlot, upperEntry); + continue; + } + } + resultMap.put(currentSlot, newHistEntryMap); + } + + /** + * @return Returns the nearest even hour timestamp of the given timestamp. + */ + private static long getNearestEvenHourTimestamp(long rawTimestamp) { + // If raw hour is even, the nearest even hour should be the even hour before raw + // start. The hour doesn't need to change and just set the minutes and seconds to 0. + // Otherwise, the nearest even hour should be raw hour + 1. + // For example, the nearest hour of 14:30:50 should be 14:00:00. While the nearest + // hour of 15:30:50 should be 16:00:00. + return getEvenHourTimestamp(rawTimestamp, /*addHourOfDay*/ 1); + } + + /** + * @return Returns the last even hour timestamp before the given timestamp. + */ + private static long getLastEvenHourBeforeTimestamp(long rawTimestamp) { + // If raw hour is even, the hour doesn't need to change as well. + // Otherwise, the even hour before raw end should be raw hour - 1. + // For example, the even hour before 14:30:50 should be 14:00:00. While the even + // hour before 15:30:50 should be 14:00:00. + return getEvenHourTimestamp(rawTimestamp, /*addHourOfDay*/ -1); + } + + private static long getEvenHourTimestamp(long rawTimestamp, int addHourOfDay) { + final Calendar evenHourCalendar = Calendar.getInstance(); + evenHourCalendar.setTimeInMillis(rawTimestamp); + // Before computing the evenHourCalendar, record raw hour based on local timezone. + final int rawHour = evenHourCalendar.get(Calendar.HOUR_OF_DAY); + if (rawHour % 2 != 0) { + evenHourCalendar.add(Calendar.HOUR_OF_DAY, addHourOfDay); + } + evenHourCalendar.set(Calendar.MINUTE, 0); + evenHourCalendar.set(Calendar.SECOND, 0); + evenHourCalendar.set(Calendar.MILLISECOND, 0); + return evenHourCalendar.getTimeInMillis(); + } + + private static List> getHourlyTimestamps(final List dailyTimestamps) { + final List> hourlyTimestamps = new ArrayList<>(); + if (dailyTimestamps.size() < MIN_DAILY_DATA_SIZE) { + return hourlyTimestamps; + } + + for (int dailyStartIndex = 0; dailyStartIndex < dailyTimestamps.size() - 1; + dailyStartIndex++) { + long currentTimestamp = dailyTimestamps.get(dailyStartIndex); + final long dailyEndTimestamp = dailyTimestamps.get(dailyStartIndex + 1); + final List hourlyTimestampsPerDay = new ArrayList<>(); + while (currentTimestamp <= dailyEndTimestamp) { + hourlyTimestampsPerDay.add(currentTimestamp); + currentTimestamp += 2 * DateUtils.HOUR_IN_MILLIS; + } + hourlyTimestamps.add(hourlyTimestampsPerDay); + } + return hourlyTimestamps; + } + + private static List getHourlyPeriodBatteryLevelData( + Context context, + final Map> processedBatteryHistoryMap, + final List> timestamps) { + final List levelData = new ArrayList<>(); + timestamps.forEach( + timestampList -> levelData.add( + getPeriodBatteryLevelData( + context, processedBatteryHistoryMap, timestampList))); + return levelData; + } + + private static BatteryLevelData.PeriodBatteryLevelData getPeriodBatteryLevelData( + Context context, + final Map> processedBatteryHistoryMap, + final List timestamps) { + final List levels = new ArrayList<>(); + timestamps.forEach( + timestamp -> levels.add(getLevel(context, processedBatteryHistoryMap, timestamp))); + return new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels); + } + + private static Integer getLevel( + Context context, + final Map> processedBatteryHistoryMap, + final long timestamp) { + final Map entryMap = processedBatteryHistoryMap.get(timestamp); + if (entryMap == null || entryMap.isEmpty()) { + Log.e(TAG, "abnormal entry list in the timestamp:" + + utcToLocalTime(context, timestamp)); + return null; + } + // Averages the battery level in each time slot to avoid corner conditions. + float batteryLevelCounter = 0; + for (BatteryHistEntry entry : entryMap.values()) { + batteryLevelCounter += entry.mBatteryLevel; + } + return Math.round(batteryLevelCounter / entryMap.size()); + } + + private static void insertHourlyUsageDiffData( + Context context, + final List hourlyBatteryLevelsPerDay, + final Map> batteryHistoryMap, + final Map> resultMap) { + final int currentUserId = context.getUserId(); + final UserHandle userHandle = + Utils.getManagedProfile(context.getSystemService(UserManager.class)); + final int workProfileUserId = + userHandle != null ? userHandle.getIdentifier() : Integer.MIN_VALUE; + // Each time slot usage diff data = + // Math.abs(timestamp[i+2] data - timestamp[i+1] data) + + // Math.abs(timestamp[i+1] data - timestamp[i] data); + // since we want to aggregate every two hours data into a single time slot. + for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) { + final Map dailyDiffMap = new HashMap<>(); + resultMap.put(dailyIndex, dailyDiffMap); + if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) { + continue; + } + final List timestamps = hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps(); + for (int hourlyIndex = 0; hourlyIndex < timestamps.size() - 1; hourlyIndex++) { + final BatteryDiffData hourlyBatteryDiffData = + insertHourlyUsageDiffDataPerSlot( + context, + currentUserId, + workProfileUserId, + hourlyIndex, + timestamps, + batteryHistoryMap); + dailyDiffMap.put(hourlyIndex, hourlyBatteryDiffData); + } + } + } + + private static void insertDailyUsageDiffData( + final List hourlyBatteryLevelsPerDay, + final Map> resultMap) { + for (int index = 0; index < hourlyBatteryLevelsPerDay.size(); index++) { + Map dailyUsageMap = resultMap.get(index); + if (dailyUsageMap == null) { + dailyUsageMap = new HashMap<>(); + resultMap.put(index, dailyUsageMap); + } + dailyUsageMap.put( + SELECTED_INDEX_ALL, + getAccumulatedUsageDiffData(dailyUsageMap.values())); + } + } + + private static void insertAllUsageDiffData( + final Map> resultMap) { + final List diffDataList = new ArrayList<>(); + resultMap.keySet().forEach( + key -> diffDataList.add(resultMap.get(key).get(SELECTED_INDEX_ALL))); + final Map allUsageMap = new HashMap<>(); + allUsageMap.put(SELECTED_INDEX_ALL, getAccumulatedUsageDiffData(diffDataList)); + resultMap.put(SELECTED_INDEX_ALL, allUsageMap); + } + + @Nullable + private static BatteryDiffData insertHourlyUsageDiffDataPerSlot( + Context context, + final int currentUserId, + final int workProfileUserId, + final int currentIndex, + final List timestamps, + final Map> batteryHistoryMap) { + final List appEntries = new ArrayList<>(); + final List systemEntries = new ArrayList<>(); + + final Long currentTimestamp = timestamps.get(currentIndex); + final Long nextTimestamp = currentTimestamp + DateUtils.HOUR_IN_MILLIS; + final Long nextTwoTimestamp = nextTimestamp + DateUtils.HOUR_IN_MILLIS; + // Fetches BatteryHistEntry data from corresponding time slot. + final Map currentBatteryHistMap = + batteryHistoryMap.getOrDefault(currentTimestamp, EMPTY_BATTERY_MAP); + final Map nextBatteryHistMap = + batteryHistoryMap.getOrDefault(nextTimestamp, EMPTY_BATTERY_MAP); + final Map nextTwoBatteryHistMap = + batteryHistoryMap.getOrDefault(nextTwoTimestamp, EMPTY_BATTERY_MAP); + // We should not get the empty list since we have at least one fake data to record + // the battery level and status in each time slot, the empty list is used to + // represent there is no enough data to apply interpolation arithmetic. + if (currentBatteryHistMap.isEmpty() + || nextBatteryHistMap.isEmpty() + || nextTwoBatteryHistMap.isEmpty()) { + return null; + } + + // Collects all keys in these three time slot records as all populations. + final Set allBatteryHistEntryKeys = new ArraySet<>(); + allBatteryHistEntryKeys.addAll(currentBatteryHistMap.keySet()); + allBatteryHistEntryKeys.addAll(nextBatteryHistMap.keySet()); + allBatteryHistEntryKeys.addAll(nextTwoBatteryHistMap.keySet()); + + double totalConsumePower = 0.0; + double consumePowerFromOtherUsers = 0f; + // Calculates all packages diff usage data in a specific time slot. + for (String key : allBatteryHistEntryKeys) { + final BatteryHistEntry currentEntry = + currentBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY); + final BatteryHistEntry nextEntry = + nextBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY); + final BatteryHistEntry nextTwoEntry = + nextTwoBatteryHistMap.getOrDefault(key, EMPTY_BATTERY_HIST_ENTRY); + // Cumulative values is a specific time slot for a specific app. + long foregroundUsageTimeInMs = + getDiffValue( + currentEntry.mForegroundUsageTimeInMs, + nextEntry.mForegroundUsageTimeInMs, + nextTwoEntry.mForegroundUsageTimeInMs); + long backgroundUsageTimeInMs = + getDiffValue( + currentEntry.mBackgroundUsageTimeInMs, + nextEntry.mBackgroundUsageTimeInMs, + nextTwoEntry.mBackgroundUsageTimeInMs); + double consumePower = + getDiffValue( + currentEntry.mConsumePower, + nextEntry.mConsumePower, + nextTwoEntry.mConsumePower); + // Excludes entry since we don't have enough data to calculate. + if (foregroundUsageTimeInMs == 0 + && backgroundUsageTimeInMs == 0 + && consumePower == 0) { + continue; + } + final BatteryHistEntry selectedBatteryEntry = + selectBatteryHistEntry(currentEntry, nextEntry, nextTwoEntry); + if (selectedBatteryEntry == null) { + continue; + } + // Forces refine the cumulative value since it may introduce deviation error since we + // will apply the interpolation arithmetic. + final float totalUsageTimeInMs = + foregroundUsageTimeInMs + backgroundUsageTimeInMs; + if (totalUsageTimeInMs > TOTAL_HOURLY_TIME_THRESHOLD) { + final float ratio = TOTAL_HOURLY_TIME_THRESHOLD / totalUsageTimeInMs; + if (DEBUG) { + Log.w(TAG, String.format("abnormal usage time %d|%d for:\n%s", + Duration.ofMillis(foregroundUsageTimeInMs).getSeconds(), + Duration.ofMillis(backgroundUsageTimeInMs).getSeconds(), + currentEntry)); + } + foregroundUsageTimeInMs = + Math.round(foregroundUsageTimeInMs * ratio); + backgroundUsageTimeInMs = + Math.round(backgroundUsageTimeInMs * ratio); + consumePower = consumePower * ratio; + } + totalConsumePower += consumePower; + + final boolean isFromOtherUsers = isConsumedFromOtherUsers( + currentUserId, workProfileUserId, selectedBatteryEntry); + if (isFromOtherUsers) { + consumePowerFromOtherUsers += consumePower; + } else { + final BatteryDiffEntry currentBatteryDiffEntry = new BatteryDiffEntry( + context, + foregroundUsageTimeInMs, + backgroundUsageTimeInMs, + consumePower, + selectedBatteryEntry); + if (currentBatteryDiffEntry.isSystemEntry()) { + systemEntries.add(currentBatteryDiffEntry); + } else { + appEntries.add(currentBatteryDiffEntry); + } + } + } + if (consumePowerFromOtherUsers != 0) { + systemEntries.add(createOtherUsersEntry(context, consumePowerFromOtherUsers)); + } + + // If there is no data, return null instead of empty item. + if (appEntries.isEmpty() && systemEntries.isEmpty()) { + return null; + } + + final BatteryDiffData resultDiffData = + new BatteryDiffData(appEntries, systemEntries, totalConsumePower); + return resultDiffData; + } + + private static boolean isConsumedFromOtherUsers( + final int currentUserId, + final int workProfileUserId, + final BatteryHistEntry batteryHistEntry) { + return batteryHistEntry.mConsumerType == ConvertUtils.CONSUMER_TYPE_UID_BATTERY + && batteryHistEntry.mUserId != currentUserId + && batteryHistEntry.mUserId != workProfileUserId; + } + + @Nullable + private static BatteryDiffData getAccumulatedUsageDiffData( + final Collection diffEntryListData) { + double totalConsumePower = 0f; + final Map diffEntryMap = new HashMap<>(); + final List appEntries = new ArrayList<>(); + final List systemEntries = new ArrayList<>(); + + for (BatteryDiffData diffEntryList : diffEntryListData) { + if (diffEntryList == null) { + continue; + } + for (BatteryDiffEntry entry : diffEntryList.getAppDiffEntryList()) { + computeUsageDiffDataPerEntry(entry, diffEntryMap); + totalConsumePower += entry.mConsumePower; + } + for (BatteryDiffEntry entry : diffEntryList.getSystemDiffEntryList()) { + computeUsageDiffDataPerEntry(entry, diffEntryMap); + totalConsumePower += entry.mConsumePower; + } + } + + final Collection diffEntryList = diffEntryMap.values(); + for (BatteryDiffEntry entry : diffEntryList) { + // Sets total daily consume power data into all BatteryDiffEntry. + entry.setTotalConsumePower(totalConsumePower); + if (entry.isSystemEntry()) { + systemEntries.add(entry); + } else { + appEntries.add(entry); + } + } + + return diffEntryList.isEmpty() ? null : new BatteryDiffData(appEntries, systemEntries); + } + + private static void computeUsageDiffDataPerEntry( + final BatteryDiffEntry entry, + final Map diffEntryMap) { + final String key = entry.mBatteryHistEntry.getKey(); + final BatteryDiffEntry oldBatteryDiffEntry = diffEntryMap.get(key); + // Creates new BatteryDiffEntry if we don't have it. + if (oldBatteryDiffEntry == null) { + diffEntryMap.put(key, entry.clone()); + } else { + // Sums up some field data into the existing one. + oldBatteryDiffEntry.mForegroundUsageTimeInMs += + entry.mForegroundUsageTimeInMs; + oldBatteryDiffEntry.mBackgroundUsageTimeInMs += + entry.mBackgroundUsageTimeInMs; + oldBatteryDiffEntry.mConsumePower += entry.mConsumePower; + } + } + + // Removes low percentage data and fake usage data, which will be zero value. + private static void purgeLowPercentageAndFakeData( + final Context context, + final Map> resultMap) { + final Set backgroundUsageTimeHideList = + FeatureFactory.getFactory(context) + .getPowerUsageFeatureProvider(context) + .getHideBackgroundUsageTimeSet(context); + final CharSequence[] notAllowShowEntryPackages = + FeatureFactory.getFactory(context) + .getPowerUsageFeatureProvider(context) + .getHideApplicationEntries(context); + resultMap.keySet().forEach(dailyKey -> { + final Map dailyUsageMap = resultMap.get(dailyKey); + dailyUsageMap.values().forEach(diffEntryLists -> { + if (diffEntryLists == null) { + return; + } + purgeLowPercentageAndFakeData( + diffEntryLists.getAppDiffEntryList(), backgroundUsageTimeHideList, + notAllowShowEntryPackages); + purgeLowPercentageAndFakeData( + diffEntryLists.getSystemDiffEntryList(), backgroundUsageTimeHideList, + notAllowShowEntryPackages); + }); + }); + } + + private static void purgeLowPercentageAndFakeData( + final List entries, + final Set backgroundUsageTimeHideList, + final CharSequence[] notAllowShowEntryPackages) { + final Iterator iterator = entries.iterator(); + while (iterator.hasNext()) { + final BatteryDiffEntry entry = iterator.next(); + final String packageName = entry.getPackageName(); + if (entry.getPercentOfTotal() < PERCENTAGE_OF_TOTAL_THRESHOLD + || FAKE_PACKAGE_NAME.equals(packageName) + || contains(packageName, notAllowShowEntryPackages)) { + iterator.remove(); + } + if (packageName != null + && !backgroundUsageTimeHideList.isEmpty() + && contains(packageName, backgroundUsageTimeHideList)) { + entry.mBackgroundUsageTimeInMs = 0; + } + } + } + + private static boolean isUsageMapValid( + final Map> batteryUsageMap, + final List hourlyBatteryLevelsPerDay) { + if (batteryUsageMap.get(SELECTED_INDEX_ALL) == null + || !batteryUsageMap.get(SELECTED_INDEX_ALL).containsKey(SELECTED_INDEX_ALL)) { + Log.e(TAG, "no [SELECTED_INDEX_ALL][SELECTED_INDEX_ALL] in batteryUsageMap"); + return false; + } + for (int dailyIndex = 0; dailyIndex < hourlyBatteryLevelsPerDay.size(); dailyIndex++) { + if (batteryUsageMap.get(dailyIndex) == null + || !batteryUsageMap.get(dailyIndex).containsKey(SELECTED_INDEX_ALL)) { + Log.e(TAG, "no [" + dailyIndex + "][SELECTED_INDEX_ALL] in batteryUsageMap, " + + "daily size is: " + hourlyBatteryLevelsPerDay.size()); + return false; + } + if (hourlyBatteryLevelsPerDay.get(dailyIndex) == null) { + continue; + } + final List timestamps = hourlyBatteryLevelsPerDay.get(dailyIndex).getTimestamps(); + // Length of hourly usage map should be the length of hourly level data - 1. + for (int hourlyIndex = 0; hourlyIndex < timestamps.size() - 1; hourlyIndex++) { + if (!batteryUsageMap.get(dailyIndex).containsKey(hourlyIndex)) { + Log.e(TAG, "no [" + dailyIndex + "][" + hourlyIndex + "] in batteryUsageMap, " + + "hourly size is: " + (timestamps.size() - 1)); + return false; + } + } + } + return true; + } + + private static long getTimestampWithDayDiff(final long timestamp, final int dayDiff) { + final Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(timestamp); + calendar.add(Calendar.DAY_OF_YEAR, dayDiff); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + return calendar.getTimeInMillis(); + } + + private static boolean contains(String target, Set packageNames) { + if (target != null && packageNames != null) { + for (CharSequence packageName : packageNames) { + if (TextUtils.equals(target, packageName)) { + return true; + } + } + } + return false; + } + + private static long getDiffValue(long v1, long v2, long v3) { + return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0); + } + + private static double getDiffValue(double v1, double v2, double v3) { + return (v2 > v1 ? v2 - v1 : 0) + (v3 > v2 ? v3 - v2 : 0); + } + + @Nullable + private static BatteryHistEntry selectBatteryHistEntry( + final BatteryHistEntry... batteryHistEntries) { + for (BatteryHistEntry entry : batteryHistEntries) { + if (entry != null && entry != EMPTY_BATTERY_HIST_ENTRY) { + return entry; + } + } + return null; + } + + private static BatteryDiffEntry createOtherUsersEntry( + Context context, final double consumePower) { + final ContentValues values = new ContentValues(); + values.put(BatteryHistEntry.KEY_UID, BatteryUtils.UID_OTHER_USERS); + values.put(BatteryHistEntry.KEY_USER_ID, BatteryUtils.UID_OTHER_USERS); + values.put(BatteryHistEntry.KEY_CONSUMER_TYPE, ConvertUtils.CONSUMER_TYPE_UID_BATTERY); + // We will show the percentage for the "other users" item only, the aggregated + // running time information is useless for users to identify individual apps. + final BatteryDiffEntry batteryDiffEntry = new BatteryDiffEntry( + context, + /*foregroundUsageTimeInMs=*/ 0, + /*backgroundUsageTimeInMs=*/ 0, + consumePower, + new BatteryHistEntry(values)); + return batteryDiffEntry; + } + + private static void log(Context context, final String content, final long timestamp, + final BatteryHistEntry entry) { + if (DEBUG) { + Log.d(TAG, String.format(entry != null ? "%s %s:\n%s" : "%s %s:%s", + utcToLocalTime(context, timestamp), content, entry)); + } + } + + // Compute diff map and loads all items (icon and label) in the background. + private static final class ComputeUsageMapAndLoadItemsTask + extends AsyncTask>> { + + private Context mApplicationContext; + private Handler mHandler; + private UsageMapAsyncResponse mAsyncResponseDelegate; + private List mHourlyBatteryLevelsPerDay; + private Map> mBatteryHistoryMap; + + private ComputeUsageMapAndLoadItemsTask( + Context context, + Handler handler, + final UsageMapAsyncResponse asyncResponseDelegate, + final List hourlyBatteryLevelsPerDay, + final Map> batteryHistoryMap) { + mApplicationContext = context.getApplicationContext(); + mHandler = handler; + mAsyncResponseDelegate = asyncResponseDelegate; + mHourlyBatteryLevelsPerDay = hourlyBatteryLevelsPerDay; + mBatteryHistoryMap = batteryHistoryMap; + } + + @Override + protected Map> doInBackground(Void... voids) { + if (mApplicationContext == null + || mHandler == null + || mAsyncResponseDelegate == null + || mBatteryHistoryMap == null + || mHourlyBatteryLevelsPerDay == null) { + Log.e(TAG, "invalid input for ComputeUsageMapAndLoadItemsTask()"); + return null; + } + final long startTime = System.currentTimeMillis(); + final Map> batteryUsageMap = + getBatteryUsageMap( + mApplicationContext, mHourlyBatteryLevelsPerDay, mBatteryHistoryMap); + if (batteryUsageMap != null) { + // Pre-loads each BatteryDiffEntry relative icon and label for all slots. + final BatteryDiffData batteryUsageMapForAll = + batteryUsageMap.get(SELECTED_INDEX_ALL).get(SELECTED_INDEX_ALL); + if (batteryUsageMapForAll != null) { + batteryUsageMapForAll.getAppDiffEntryList().forEach( + entry -> entry.loadLabelAndIcon()); + batteryUsageMapForAll.getSystemDiffEntryList().forEach( + entry -> entry.loadLabelAndIcon()); + } + } + Log.d(TAG, String.format("execute ComputeUsageMapAndLoadItemsTask in %d/ms", + (System.currentTimeMillis() - startTime))); + return batteryUsageMap; + } + + @Override + protected void onPostExecute( + final Map> batteryUsageMap) { + mApplicationContext = null; + mHourlyBatteryLevelsPerDay = null; + mBatteryHistoryMap = null; + // Post results back to main thread to refresh UI. + if (mHandler != null && mAsyncResponseDelegate != null) { + mHandler.post(() -> { + mAsyncResponseDelegate.onBatteryUsageMapLoaded(batteryUsageMap); + }); + } + } + } +} diff --git a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummary.java b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummary.java index 405d855d3d8..bca32a78f8f 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummary.java +++ b/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummary.java @@ -259,10 +259,7 @@ public class PowerUsageSummary extends PowerUsageBase implements @VisibleForTesting void initPreference() { mBatteryUsagePreference = findPreference(KEY_BATTERY_USAGE); - mBatteryUsagePreference.setSummary( - mPowerFeatureProvider.isChartGraphEnabled(getContext()) - ? getString(R.string.advanced_battery_preference_summary_with_hours) - : getString(R.string.advanced_battery_preference_summary)); + mBatteryUsagePreference.setSummary(getString(R.string.advanced_battery_preference_summary)); mHelpPreference = findPreference(KEY_BATTERY_ERROR); mHelpPreference.setVisible(false); diff --git a/tests/robotests/src/com/android/settings/applications/appinfo/AppBatteryPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/applications/appinfo/AppBatteryPreferenceControllerTest.java index a75663b2f64..c95a50955d8 100644 --- a/tests/robotests/src/com/android/settings/applications/appinfo/AppBatteryPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/applications/appinfo/AppBatteryPreferenceControllerTest.java @@ -162,7 +162,8 @@ public class AppBatteryPreferenceControllerTest { mController.updateBatteryWithDiffEntry(); - assertThat(mBatteryPreference.getSummary()).isEqualTo("No battery use for past 24 hours"); + assertThat(mBatteryPreference.getSummary().toString()).isEqualTo( + "No battery use since last full charge"); } @Test @@ -175,7 +176,8 @@ public class AppBatteryPreferenceControllerTest { mController.updateBatteryWithDiffEntry(); - assertThat(mBatteryPreference.getSummary()).isEqualTo("60% use for past 24 hours"); + assertThat(mBatteryPreference.getSummary().toString()).isEqualTo( + "60% use since last full charge"); } @Test diff --git a/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java b/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java index cba20197d0e..5db76b1797b 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetailTest.java @@ -232,7 +232,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testGetPreferenceScreenResId_returnNewLayout() { + public void setPreferenceScreenResId_returnNewLayout() { assertThat(mFragment.getPreferenceScreenResId()).isEqualTo(R.xml.power_usage_detail); } @@ -252,7 +252,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitHeader_HasAppEntry_BuildByAppEntry() { + public void initHeader_HasAppEntry_BuildByAppEntry() { ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider", new InstantAppDataProvider() { @Override @@ -269,7 +269,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitHeader_HasAppEntry_InstantApp() { + public void initHeader_HasAppEntry_InstantApp() { ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider", new InstantAppDataProvider() { @Override @@ -286,7 +286,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitHeader_noUsageTimeAndGraphDisabled_hasCorrectSummary() { + public void initHeader_noUsageTimeAndGraphDisabled_hasCorrectSummary() { when(mFeatureFactory.powerUsageFeatureProvider.isChartGraphEnabled(mContext)) .thenReturn(false); @@ -304,7 +304,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitHeader_bgTwoMinFgZeroAndGraphDisabled_hasCorrectSummary() { + public void initHeader_bgTwoMinFgZeroAndGraphDisabled_hasCorrectSummary() { when(mFeatureFactory.powerUsageFeatureProvider.isChartGraphEnabled(mContext)) .thenReturn(false); @@ -324,7 +324,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitHeader_bgLessThanAMinFgZeroAndGraphDisabled_hasCorrectSummary() { + public void initHeader_bgLessThanAMinFgZeroAndGraphDisabled_hasCorrectSummary() { when(mFeatureFactory.powerUsageFeatureProvider.isChartGraphEnabled(mContext)) .thenReturn(false); @@ -345,7 +345,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitHeader_totalUsageLessThanAMinAndGraphDisabled_hasCorrectSummary() { + public void initHeader_totalUsageLessThanAMinAndGraphDisabled_hasCorrectSummary() { when(mFeatureFactory.powerUsageFeatureProvider.isChartGraphEnabled(mContext)) .thenReturn(false); @@ -367,7 +367,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitHeader_TotalAMinutesBgLessThanAMinAndGraphDisabled_hasCorrectSummary() { + public void initHeader_TotalAMinutesBgLessThanAMinAndGraphDisabled_hasCorrectSummary() { when(mFeatureFactory.powerUsageFeatureProvider.isChartGraphEnabled(mContext)) .thenReturn(false); @@ -387,7 +387,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitHeader_TotalAMinBackgroundZeroAndGraphDisabled_hasCorrectSummary() { + public void initHeader_TotalAMinBackgroundZeroAndGraphDisabled_hasCorrectSummary() { when(mFeatureFactory.powerUsageFeatureProvider.isChartGraphEnabled(mContext)) .thenReturn(false); final long backgroundTimeZero = 0; @@ -406,7 +406,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitHeader_fgTwoMinBgFourMinAndGraphDisabled_hasCorrectSummary() { + public void initHeader_fgTwoMinBgFourMinAndGraphDisabled_hasCorrectSummary() { when(mFeatureFactory.powerUsageFeatureProvider.isChartGraphEnabled(mContext)) .thenReturn(false); final long backgroundTimeFourMinute = 240000; @@ -424,7 +424,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitHeader_noUsageTime_hasCorrectSummary() { + public void initHeader_noUsageTime_hasCorrectSummary() { Bundle bundle = new Bundle(2); bundle.putLong(AdvancedPowerUsageDetail.EXTRA_BACKGROUND_TIME, /* value */ 0); bundle.putLong(AdvancedPowerUsageDetail.EXTRA_FOREGROUND_TIME, /* value */ 0); @@ -435,11 +435,11 @@ public class AdvancedPowerUsageDetailTest { ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); verify(mEntityHeaderController).setSummary(captor.capture()); assertThat(captor.getValue().toString()) - .isEqualTo("No usage for past 24 hr"); + .isEqualTo("No usage from last full charge"); } @Test - public void testInitHeader_noUsageTimeButConsumedPower_hasEmptySummary() { + public void initHeader_noUsageTimeButConsumedPower_hasEmptySummary() { Bundle bundle = new Bundle(3); bundle.putLong(AdvancedPowerUsageDetail.EXTRA_BACKGROUND_TIME, /* value */ 0); bundle.putLong(AdvancedPowerUsageDetail.EXTRA_FOREGROUND_TIME, /* value */ 0); @@ -454,7 +454,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitHeader_backgroundTwoMinForegroundZero_hasCorrectSummary() { + public void initHeader_backgroundTwoMinForegroundZero_hasCorrectSummary() { final long backgroundTimeTwoMinutes = 120000; final long foregroundTimeZero = 0; Bundle bundle = new Bundle(2); @@ -467,11 +467,11 @@ public class AdvancedPowerUsageDetailTest { ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); verify(mEntityHeaderController).setSummary(captor.capture()); assertThat(captor.getValue().toString()) - .isEqualTo("2 min background for past 24 hr"); + .isEqualTo("2 min background from last full charge"); } @Test - public void testInitHeader_backgroundLessThanAMinForegroundZero_hasCorrectSummary() { + public void initHeader_backgroundLessThanAMinForegroundZero_hasCorrectSummary() { final long backgroundTimeLessThanAMinute = 59999; final long foregroundTimeZero = 0; Bundle bundle = new Bundle(2); @@ -485,11 +485,11 @@ public class AdvancedPowerUsageDetailTest { ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); verify(mEntityHeaderController).setSummary(captor.capture()); assertThat(captor.getValue().toString()) - .isEqualTo("Background less than a minute for past 24 hr"); + .isEqualTo("Background less than a minute from last full charge"); } @Test - public void testInitHeader_totalUsageLessThanAMin_hasCorrectSummary() { + public void initHeader_totalUsageLessThanAMin_hasCorrectSummary() { final long backgroundTimeLessThanHalfMinute = 20000; final long foregroundTimeLessThanHalfMinute = 20000; Bundle bundle = new Bundle(2); @@ -504,11 +504,11 @@ public class AdvancedPowerUsageDetailTest { ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); verify(mEntityHeaderController).setSummary(captor.capture()); assertThat(captor.getValue().toString()) - .isEqualTo("Total less than a minute for past 24 hr"); + .isEqualTo("Total less than a minute from last full charge"); } @Test - public void testInitHeader_TotalAMinutesBackgroundLessThanAMin_hasCorrectSummary() { + public void initHeader_TotalAMinutesBackgroundLessThanAMin_hasCorrectSummary() { final long backgroundTimeZero = 59999; final long foregroundTimeTwoMinutes = 1; Bundle bundle = new Bundle(2); @@ -521,11 +521,11 @@ public class AdvancedPowerUsageDetailTest { ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); verify(mEntityHeaderController).setSummary(captor.capture()); assertThat(captor.getValue().toString()) - .isEqualTo("1 min total • background less than a minute\nfor past 24 hr"); + .isEqualTo("1 min total • background less than a minute\nfrom last full charge"); } @Test - public void testInitHeader_TotalAMinBackgroundZero_hasCorrectSummary() { + public void initHeader_TotalAMinBackgroundZero_hasCorrectSummary() { final long backgroundTimeZero = 0; final long foregroundTimeAMinutes = 60000; Bundle bundle = new Bundle(2); @@ -538,11 +538,11 @@ public class AdvancedPowerUsageDetailTest { ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); verify(mEntityHeaderController).setSummary(captor.capture()); assertThat(captor.getValue().toString()) - .isEqualTo("1 min total for past 24 hr"); + .isEqualTo("1 min total from last full charge"); } @Test - public void testInitHeader_foregroundTwoMinBackgroundFourMin_hasCorrectSummary() { + public void initHeader_foregroundTwoMinBackgroundFourMin_hasCorrectSummary() { final long backgroundTimeFourMinute = 240000; final long foregroundTimeTwoMinutes = 120000; Bundle bundle = new Bundle(2); @@ -555,11 +555,11 @@ public class AdvancedPowerUsageDetailTest { ArgumentCaptor captor = ArgumentCaptor.forClass(CharSequence.class); verify(mEntityHeaderController).setSummary(captor.capture()); assertThat(captor.getValue().toString()) - .isEqualTo("6 min total • 4 min background\nfor past 24 hr"); + .isEqualTo("6 min total • 4 min background\nfrom last full charge"); } @Test - public void testInitHeader_totalUsageLessThanAMinWithSlotTime_hasCorrectSummary() { + public void initHeader_totalUsageLessThanAMinWithSlotTime_hasCorrectSummary() { final long backgroundTimeLessThanHalfMinute = 20000; final long foregroundTimeLessThanHalfMinute = 20000; Bundle bundle = new Bundle(3); @@ -579,7 +579,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitHeader_TotalAMinBackgroundLessThanAMinWithSlotTime_hasCorrectSummary() { + public void initHeader_TotalAMinBackgroundLessThanAMinWithSlotTime_hasCorrectSummary() { final long backgroundTimeZero = 59999; final long foregroundTimeTwoMinutes = 1; Bundle bundle = new Bundle(3); @@ -597,7 +597,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitHeader_TotalAMinBackgroundZeroWithSlotTime_hasCorrectSummary() { + public void initHeader_TotalAMinBackgroundZeroWithSlotTime_hasCorrectSummary() { final long backgroundTimeZero = 0; final long foregroundTimeAMinutes = 60000; Bundle bundle = new Bundle(3); @@ -615,7 +615,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitHeader_foregroundTwoMinBackgroundFourMinWithSlotTime_hasCorrectSummary() { + public void initHeader_foregroundTwoMinBackgroundFourMinWithSlotTime_hasCorrectSummary() { final long backgroundTimeFourMinute = 240000; final long foregroundTimeTwoMinutes = 120000; Bundle bundle = new Bundle(3); @@ -633,7 +633,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitHeader_systemUidWithChartIsDisabled_nullSummary() { + public void initHeader_systemUidWithChartIsDisabled_nullSummary() { Bundle bundle = new Bundle(3); bundle.putLong(AdvancedPowerUsageDetail.EXTRA_BACKGROUND_TIME, 240000); bundle.putLong(AdvancedPowerUsageDetail.EXTRA_FOREGROUND_TIME, 120000); @@ -650,7 +650,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitHeader_systemUidWithChartIsEnabled_notNullSummary() { + public void initHeader_systemUidWithChartIsEnabled_notNullSummary() { Bundle bundle = new Bundle(3); bundle.putLong(AdvancedPowerUsageDetail.EXTRA_BACKGROUND_TIME, 240000); bundle.putLong(AdvancedPowerUsageDetail.EXTRA_FOREGROUND_TIME, 120000); @@ -665,21 +665,21 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testStartBatteryDetailPage_hasBasicData() { + public void startBatteryDetailPage_hasBasicData() { AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, mFragment, mBatteryEntry, USAGE_PERCENT, /*isValidToShowSummary=*/ true); assertThat(mBundle.getInt(AdvancedPowerUsageDetail.EXTRA_UID)).isEqualTo(UID); assertThat(mBundle.getLong(AdvancedPowerUsageDetail.EXTRA_BACKGROUND_TIME)) - .isEqualTo(BACKGROUND_TIME_MS); + .isEqualTo(BACKGROUND_TIME_MS); assertThat(mBundle.getLong(AdvancedPowerUsageDetail.EXTRA_FOREGROUND_TIME)) - .isEqualTo(FOREGROUND_TIME_MS); + .isEqualTo(FOREGROUND_TIME_MS); assertThat(mBundle.getString(AdvancedPowerUsageDetail.EXTRA_POWER_USAGE_PERCENT)) - .isEqualTo(USAGE_PERCENT); + .isEqualTo(USAGE_PERCENT); } @Test - public void testStartBatteryDetailPage_invalidToShowSummary_noFGBDData() { + public void startBatteryDetailPage_invalidToShowSummary_noFGBDData() { AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, mFragment, mBatteryEntry, USAGE_PERCENT, /*isValidToShowSummary=*/ false); @@ -693,7 +693,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testStartBatteryDetailPage_NormalApp() { + public void startBatteryDetailPage_NormalApp() { when(mBatteryEntry.getDefaultPackageName()).thenReturn(PACKAGE_NAME[0]); AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, mFragment, @@ -704,7 +704,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testStartBatteryDetailPage_SystemApp() { + public void startBatteryDetailPage_SystemApp() { when(mBatteryEntry.getDefaultPackageName()).thenReturn(null); AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, mFragment, @@ -716,7 +716,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testStartBatteryDetailPage_WorkApp() { + public void startBatteryDetailPage_WorkApp() { final int appUid = 1010019; doReturn(appUid).when(mBatteryEntry).getUid(); @@ -727,7 +727,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testStartBatteryDetailPage_typeUser_startByCurrentUser() { + public void startBatteryDetailPage_typeUser_startByCurrentUser() { when(mBatteryEntry.isUserEntry()).thenReturn(true); final int currentUser = 20; @@ -739,7 +739,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testStartBatteryDetailPage_noBatteryUsage_hasBasicData() { + public void startBatteryDetailPage_noBatteryUsage_hasBasicData() { final ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, mFragment, PACKAGE_NAME[0]); @@ -747,16 +747,16 @@ public class AdvancedPowerUsageDetailTest { verify(mActivity).startActivity(captor.capture()); assertThat(captor.getValue().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS) - .getString(AdvancedPowerUsageDetail.EXTRA_PACKAGE_NAME)) - .isEqualTo(PACKAGE_NAME[0]); + .getString(AdvancedPowerUsageDetail.EXTRA_PACKAGE_NAME)) + .isEqualTo(PACKAGE_NAME[0]); assertThat(captor.getValue().getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS) - .getString(AdvancedPowerUsageDetail.EXTRA_POWER_USAGE_PERCENT)) - .isEqualTo("0%"); + .getString(AdvancedPowerUsageDetail.EXTRA_POWER_USAGE_PERCENT)) + .isEqualTo("0%"); } @Test - public void testStartBatteryDetailPage_batteryEntryNotExisted_extractUidFromPackageName() throws + public void startBatteryDetailPage_batteryEntryNotExisted_extractUidFromPackageName() throws PackageManager.NameNotFoundException { doReturn(UID).when(mPackageManager).getPackageUid(PACKAGE_NAME[0], 0 /* no flag */); @@ -796,7 +796,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitPreferenceForTriState_isSystemOrDefaultApp_hasCorrectString() { + public void initPreferenceForTriState_isSystemOrDefaultApp_hasCorrectString() { when(mBatteryOptimizeUtils.isValidPackageName()).thenReturn(true); when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(true); @@ -807,7 +807,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testInitPreferenceForTriState_hasCorrectString() { + public void initPreferenceForTriState_hasCorrectString() { when(mBatteryOptimizeUtils.isValidPackageName()).thenReturn(true); when(mBatteryOptimizeUtils.isSystemOrDefaultApp()).thenReturn(false); @@ -818,7 +818,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testOnRadioButtonClicked_clickOptimizePref_optimizePreferenceChecked() { + public void onRadioButtonClicked_clickOptimizePref_optimizePreferenceChecked() { mOptimizePreference.setKey(KEY_PREF_OPTIMIZED); mRestrictedPreference.setKey(KEY_PREF_RESTRICTED); mUnrestrictedPreference.setKey(KEY_PREF_UNRESTRICTED); @@ -830,7 +830,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testOnPause_optimizationModeChanged_logPreference() { + public void onPause_optimizationModeChanged_logPreference() { final int mode = BatteryOptimizeUtils.MODE_RESTRICTED; mFragment.mOptimizationMode = mode; when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(mode); @@ -849,7 +849,7 @@ public class AdvancedPowerUsageDetailTest { } @Test - public void testOnPause_optimizationModeIsNotChanged_notInvokeLogging() { + public void onPause_optimizationModeIsNotChanged_notInvokeLogging() { final int mode = BatteryOptimizeUtils.MODE_OPTIMIZED; mFragment.mOptimizationMode = mode; when(mBatteryOptimizeUtils.getAppOptimizationMode()).thenReturn(mode); diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java index ec982264e60..016287e2f87 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceControllerTest.java @@ -18,23 +18,24 @@ package com.android.settings.fuelgauge.batteryusage; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.app.settings.SettingsEnums; import android.content.ContentValues; import android.content.Context; -import android.content.pm.PackageManager; -import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.LocaleList; import android.text.format.DateUtils; +import android.view.View; +import android.widget.LinearLayout; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; @@ -58,15 +59,15 @@ import org.robolectric.RuntimeEnvironment; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.TimeZone; @RunWith(RobolectricTestRunner.class) public final class BatteryChartPreferenceControllerTest { private static final String PREF_KEY = "pref_key"; private static final String PREF_SUMMARY = "fake preference summary"; - private static final int DESIRED_HISTORY_SIZE = - BatteryChartPreferenceController.DESIRED_HISTORY_SIZE; @Mock private InstrumentedPreferenceFragment mFragment; @@ -79,11 +80,15 @@ public final class BatteryChartPreferenceControllerTest { @Mock private BatteryHistEntry mBatteryHistEntry; @Mock - private BatteryChartView mBatteryChartView; + private BatteryChartView mDailyChartView; + @Mock + private BatteryChartView mHourlyChartView; @Mock private PowerGaugePreference mPowerGaugePreference; @Mock private BatteryUtils mBatteryUtils; + @Mock + private LinearLayout.LayoutParams mLayoutParams; private Context mContext; private FakeFeatureFactory mFeatureFactory; @@ -96,6 +101,7 @@ public final class BatteryChartPreferenceControllerTest { MockitoAnnotations.initMocks(this); Locale.setDefault(new Locale("en_US")); org.robolectric.shadows.ShadowSettings.set24HourTimeFormat(false); + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); mFeatureFactory = FakeFeatureFactory.setupForTest(); mMetricsFeatureProvider = mFeatureFactory.metricsFeatureProvider; mContext = spy(RuntimeEnvironment.application); @@ -108,10 +114,12 @@ public final class BatteryChartPreferenceControllerTest { doReturn(new String[]{"com.android.gms.persistent"}) .when(mFeatureFactory.powerUsageFeatureProvider) .getHideApplicationEntries(mContext); + doReturn(mLayoutParams).when(mDailyChartView).getLayoutParams(); mBatteryChartPreferenceController = createController(); mBatteryChartPreferenceController.mPrefContext = mContext; mBatteryChartPreferenceController.mAppListPrefGroup = mAppListGroup; - mBatteryChartPreferenceController.mBatteryChartView = mBatteryChartView; + mBatteryChartPreferenceController.mDailyChartView = mDailyChartView; + mBatteryChartPreferenceController.mHourlyChartView = mHourlyChartView; mBatteryDiffEntry = new BatteryDiffEntry( mContext, /*foregroundUsageTimeInMs=*/ 1, @@ -123,12 +131,10 @@ public final class BatteryChartPreferenceControllerTest { BatteryDiffEntry.sResourceCache.put( "fakeBatteryDiffEntryKey", new BatteryEntry.NameAndIcon("fakeName", /*icon=*/ null, /*iconId=*/ 1)); - mBatteryChartPreferenceController.setBatteryHistoryMap( - createBatteryHistoryMap()); } @Test - public void testOnDestroy_activityIsChanging_clearBatteryEntryCache() { + public void onDestroy_activityIsChanging_clearBatteryEntryCache() { doReturn(true).when(mSettingsActivity).isChangingConfigurations(); // Ensures the testing environment is correct. assertThat(BatteryDiffEntry.sResourceCache).hasSize(1); @@ -138,7 +144,7 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testOnDestroy_activityIsNotChanging_notClearBatteryEntryCache() { + public void onDestroy_activityIsNotChanging_notClearBatteryEntryCache() { doReturn(false).when(mSettingsActivity).isChangingConfigurations(); // Ensures the testing environment is correct. assertThat(BatteryDiffEntry.sResourceCache).hasSize(1); @@ -148,7 +154,7 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testOnDestroy_clearPreferenceCache() { + public void onDestroy_clearPreferenceCache() { // Ensures the testing environment is correct. mBatteryChartPreferenceController.mPreferenceCache.put( PREF_KEY, mPowerGaugePreference); @@ -160,113 +166,135 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testOnDestroy_removeAllPreferenceFromPreferenceGroup() { + public void onDestroy_removeAllPreferenceFromPreferenceGroup() { mBatteryChartPreferenceController.onDestroy(); verify(mAppListGroup).removeAll(); } @Test - public void testSetBatteryHistoryMap_createExpectedKeysAndLevels() { - mBatteryChartPreferenceController.setBatteryHistoryMap( - createBatteryHistoryMap()); + public void setBatteryChartViewModel_6Hours() { + mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6)); - // Verifies the created battery keys array. - for (int index = 0; index < DESIRED_HISTORY_SIZE; index++) { - assertThat(mBatteryChartPreferenceController.mBatteryHistoryKeys[index]) - // These values is are calculated by hand from createBatteryHistoryMap(). - .isEqualTo(index + 1); - } - // Verifies the created battery levels array. - for (int index = 0; index < 13; index++) { - assertThat(mBatteryChartPreferenceController.mBatteryHistoryLevels[index]) - // These values is are calculated by hand from createBatteryHistoryMap(). - .isEqualTo(100 - index * 2); - } - assertThat(mBatteryChartPreferenceController.mBatteryIndexedMap).hasSize(13); + verify(mDailyChartView, atLeastOnce()).setVisibility(View.GONE); + verify(mHourlyChartView, atLeastOnce()).setVisibility(View.VISIBLE); + verify(mHourlyChartView).setViewModel(new BatteryChartViewModel( + List.of(100, 97, 95), + List.of("8 am", "10 am", "12 pm"), + BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS)); } @Test - public void testSetBatteryHistoryMap_largeSize_createExpectedKeysAndLevels() { - mBatteryChartPreferenceController.setBatteryHistoryMap( - createBatteryHistoryMap()); + public void setBatteryChartViewModel_60Hours() { + BatteryChartViewModel expectedDailyViewModel = new BatteryChartViewModel( + List.of(100, 83, 59, 41), + List.of("Sat", "Sun", "Mon", "Mon"), + BatteryChartViewModel.AxisLabelPosition.CENTER_OF_TRAPEZOIDS); - // Verifies the created battery keys array. - for (int index = 0; index < DESIRED_HISTORY_SIZE; index++) { - assertThat(mBatteryChartPreferenceController.mBatteryHistoryKeys[index]) - // These values is are calculated by hand from createBatteryHistoryMap(). - .isEqualTo(index + 1); - } - // Verifies the created battery levels array. - for (int index = 0; index < 13; index++) { - assertThat(mBatteryChartPreferenceController.mBatteryHistoryLevels[index]) - // These values is are calculated by hand from createBatteryHistoryMap(). - .isEqualTo(100 - index * 2); - } - assertThat(mBatteryChartPreferenceController.mBatteryIndexedMap).hasSize(13); + mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(60)); + + verify(mDailyChartView, atLeastOnce()).setVisibility(View.VISIBLE); + verify(mHourlyChartView, atLeastOnce()).setVisibility(View.GONE); + verify(mDailyChartView).setViewModel(expectedDailyViewModel); + + reset(mDailyChartView); + reset(mHourlyChartView); + doReturn(mLayoutParams).when(mDailyChartView).getLayoutParams(); + mBatteryChartPreferenceController.mDailyChartIndex = 0; + mBatteryChartPreferenceController.refreshUi(); + verify(mDailyChartView).setVisibility(View.VISIBLE); + verify(mHourlyChartView).setVisibility(View.VISIBLE); + + expectedDailyViewModel.setSelectedIndex(0); + verify(mDailyChartView).setViewModel(expectedDailyViewModel); + verify(mHourlyChartView).setViewModel(new BatteryChartViewModel( + List.of(100, 97, 95, 93, 91, 89, 87, 85, 83), + List.of("8 am", "10 am", "12 pm", "2 pm", "4 pm", "6 pm", "8 pm", "10 pm", + "12 am"), + BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS)); + + reset(mDailyChartView); + reset(mHourlyChartView); + doReturn(mLayoutParams).when(mDailyChartView).getLayoutParams(); + mBatteryChartPreferenceController.mDailyChartIndex = 1; + mBatteryChartPreferenceController.mHourlyChartIndex = 6; + mBatteryChartPreferenceController.refreshUi(); + verify(mDailyChartView).setVisibility(View.VISIBLE); + verify(mHourlyChartView).setVisibility(View.VISIBLE); + expectedDailyViewModel.setSelectedIndex(1); + verify(mDailyChartView).setViewModel(expectedDailyViewModel); + BatteryChartViewModel expectedHourlyViewModel = new BatteryChartViewModel( + List.of(83, 81, 79, 77, 75, 73, 71, 69, 67, 65, 63, 61, 59), + List.of("12 am", "2 am", "4 am", "6 am", "8 am", "10 am", "12 pm", "2 pm", + "4 pm", "6 pm", "8 pm", "10 pm", "12 am"), + BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS); + expectedHourlyViewModel.setSelectedIndex(6); + verify(mHourlyChartView).setViewModel(expectedHourlyViewModel); + + reset(mDailyChartView); + reset(mHourlyChartView); + doReturn(mLayoutParams).when(mDailyChartView).getLayoutParams(); + mBatteryChartPreferenceController.mDailyChartIndex = 2; + mBatteryChartPreferenceController.mHourlyChartIndex = + BatteryChartViewModel.SELECTED_INDEX_ALL; + mBatteryChartPreferenceController.refreshUi(); + verify(mDailyChartView).setVisibility(View.VISIBLE); + verify(mHourlyChartView).setVisibility(View.VISIBLE); + expectedDailyViewModel.setSelectedIndex(2); + verify(mDailyChartView).setViewModel(expectedDailyViewModel); + verify(mHourlyChartView).setViewModel(new BatteryChartViewModel( + List.of(59, 57, 55, 53, 51, 49, 47, 45, 43, 41), + List.of("12 am", "2 am", "4 am", "6 am", "8 am", "10 am", "12 pm", "2 pm", + "4 pm", "6 pm"), + BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS)); } @Test - public void testRefreshUi_batteryIndexedMapIsNull_ignoreRefresh() { + public void refreshUi_normalCase_returnTrue() { + mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6)); + assertThat(mBatteryChartPreferenceController.refreshUi()).isTrue(); + } + + @Test + public void refreshUi_batteryIndexedMapIsNull_ignoreRefresh() { mBatteryChartPreferenceController.setBatteryHistoryMap(null); - assertThat(mBatteryChartPreferenceController.refreshUi( - /*trapezoidIndex=*/ 1, /*isForce=*/ false)).isFalse(); + assertThat(mBatteryChartPreferenceController.refreshUi()).isFalse(); } @Test - public void testRefreshUi_batteryChartViewIsNull_ignoreRefresh() { - mBatteryChartPreferenceController.mBatteryChartView = null; - assertThat(mBatteryChartPreferenceController.refreshUi( - /*trapezoidIndex=*/ 1, /*isForce=*/ false)).isFalse(); + public void refreshUi_dailyChartViewIsNull_ignoreRefresh() { + mBatteryChartPreferenceController.mDailyChartView = null; + assertThat(mBatteryChartPreferenceController.refreshUi()).isFalse(); } @Test - public void testRefreshUi_trapezoidIndexIsNotChanged_ignoreRefresh() { - final int trapezoidIndex = 1; - mBatteryChartPreferenceController.mTrapezoidIndex = trapezoidIndex; - assertThat(mBatteryChartPreferenceController.refreshUi( - trapezoidIndex, /*isForce=*/ false)).isFalse(); + public void refreshUi_hourlyChartViewIsNull_ignoreRefresh() { + mBatteryChartPreferenceController.mHourlyChartView = null; + assertThat(mBatteryChartPreferenceController.refreshUi()).isFalse(); } @Test - public void testRefreshUi_forceUpdate_refreshUi() { - final int trapezoidIndex = 1; - mBatteryChartPreferenceController.mTrapezoidIndex = trapezoidIndex; - assertThat(mBatteryChartPreferenceController.refreshUi( - trapezoidIndex, /*isForce=*/ true)).isTrue(); - } - - @Test - public void testForceRefreshUi_updateTrapezoidIndexIntoSelectAll() { - mBatteryChartPreferenceController.mTrapezoidIndex = - BatteryChartView.SELECTED_INDEX_INVALID; - mBatteryChartPreferenceController.setBatteryHistoryMap( - createBatteryHistoryMap()); - - assertThat(mBatteryChartPreferenceController.mTrapezoidIndex) - .isEqualTo(BatteryChartView.SELECTED_INDEX_ALL); - } - - @Test - public void testRemoveAndCacheAllPrefs_emptyContent_ignoreRemoveAll() { - final int trapezoidIndex = 1; + public void removeAndCacheAllPrefs_emptyContent_ignoreRemoveAll() { + mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6)); + mBatteryChartPreferenceController.mBatteryUsageMap = createBatteryUsageMap(); doReturn(0).when(mAppListGroup).getPreferenceCount(); - mBatteryChartPreferenceController.refreshUi( - trapezoidIndex, /*isForce=*/ true); + mBatteryChartPreferenceController.refreshUi(); verify(mAppListGroup, never()).removeAll(); } @Test - public void testRemoveAndCacheAllPrefs_buildCacheAndRemoveAllPreference() { - final int trapezoidIndex = 1; + public void removeAndCacheAllPrefs_buildCacheAndRemoveAllPreference() { + mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6)); + mBatteryChartPreferenceController.mBatteryUsageMap = createBatteryUsageMap(); doReturn(1).when(mAppListGroup).getPreferenceCount(); doReturn(mPowerGaugePreference).when(mAppListGroup).getPreference(0); + doReturn(PREF_KEY).when(mBatteryHistEntry).getKey(); doReturn(PREF_KEY).when(mPowerGaugePreference).getKey(); + doReturn(mPowerGaugePreference).when(mAppListGroup).findPreference(PREF_KEY); // Ensures the testing data is correct. assertThat(mBatteryChartPreferenceController.mPreferenceCache).isEmpty(); - mBatteryChartPreferenceController.refreshUi( - trapezoidIndex, /*isForce=*/ true); + mBatteryChartPreferenceController.refreshUi(); assertThat(mBatteryChartPreferenceController.mPreferenceCache.get(PREF_KEY)) .isEqualTo(mPowerGaugePreference); @@ -274,14 +302,14 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testAddPreferenceToScreen_emptyContent_ignoreAddPreference() { + public void addPreferenceToScreen_emptyContent_ignoreAddPreference() { mBatteryChartPreferenceController.addPreferenceToScreen( new ArrayList()); verify(mAppListGroup, never()).addPreference(any()); } @Test - public void testAddPreferenceToScreen_addPreferenceIntoScreen() { + public void addPreferenceToScreen_addPreferenceIntoScreen() { final String appLabel = "fake app label"; doReturn(1).when(mAppListGroup).getPreferenceCount(); doReturn(mDrawable).when(mBatteryDiffEntry).getAppIcon(); @@ -310,7 +338,7 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testAddPreferenceToScreen_alreadyInScreen_notAddPreferenceAgain() { + public void addPreferenceToScreen_alreadyInScreen_notAddPreferenceAgain() { final String appLabel = "fake app label"; doReturn(1).when(mAppListGroup).getPreferenceCount(); doReturn(mDrawable).when(mBatteryDiffEntry).getAppIcon(); @@ -325,7 +353,7 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testHandlePreferenceTreeiClick_notPowerGaugePreference_returnFalse() { + public void handlePreferenceTreeClick_notPowerGaugePreference_returnFalse() { assertThat(mBatteryChartPreferenceController.handlePreferenceTreeClick(mAppListGroup)) .isFalse(); @@ -336,7 +364,7 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testHandlePreferenceTreeClick_forAppEntry_returnTrue() { + public void handlePreferenceTreeClick_forAppEntry_returnTrue() { doReturn(false).when(mBatteryHistEntry).isAppEntry(); doReturn(mBatteryDiffEntry).when(mPowerGaugePreference).getBatteryDiffEntry(); @@ -352,7 +380,7 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testHandlePreferenceTreeClick_forSystemEntry_returnTrue() { + public void handlePreferenceTreeClick_forSystemEntry_returnTrue() { mBatteryChartPreferenceController.mBatteryUtils = mBatteryUtils; doReturn(true).when(mBatteryHistEntry).isAppEntry(); doReturn(mBatteryDiffEntry).when(mPowerGaugePreference).getBatteryDiffEntry(); @@ -369,7 +397,7 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testSetPreferenceSummary_setNullContentIfTotalUsageTimeIsZero() { + public void setPreferenceSummary_setNullContentIfTotalUsageTimeIsZero() { final PowerGaugePreference pref = new PowerGaugePreference(mContext); pref.setSummary(PREF_SUMMARY); @@ -381,7 +409,7 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testSetPreferenceSummary_setBackgroundUsageTimeOnly() { + public void setPreferenceSummary_setBackgroundUsageTimeOnly() { final PowerGaugePreference pref = new PowerGaugePreference(mContext); pref.setSummary(PREF_SUMMARY); @@ -393,7 +421,7 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testSetPreferenceSummary_setTotalUsageTimeLessThanAMinute() { + public void setPreferenceSummary_setTotalUsageTimeLessThanAMinute() { final PowerGaugePreference pref = new PowerGaugePreference(mContext); pref.setSummary(PREF_SUMMARY); @@ -405,7 +433,7 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testSetPreferenceSummary_setTotalTimeIfBackgroundTimeLessThanAMinute() { + public void setPreferenceSummary_setTotalTimeIfBackgroundTimeLessThanAMinute() { final PowerGaugePreference pref = new PowerGaugePreference(mContext); pref.setSummary(PREF_SUMMARY); @@ -418,7 +446,7 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testSetPreferenceSummary_setTotalAndBackgroundUsageTime() { + public void setPreferenceSummary_setTotalAndBackgroundUsageTime() { final PowerGaugePreference pref = new PowerGaugePreference(mContext); pref.setSummary(PREF_SUMMARY); @@ -430,7 +458,7 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testSetPreferenceSummary_notAllowShownPackage_setSummayAsNull() { + public void setPreferenceSummary_notAllowShownPackage_setSummayAsNull() { final PowerGaugePreference pref = new PowerGaugePreference(mContext); pref.setSummary(PREF_SUMMARY); final BatteryDiffEntry batteryDiffEntry = @@ -445,36 +473,9 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testValidateUsageTime_returnTrueIfBatteryDiffEntryIsValid() { - assertThat(BatteryChartPreferenceController.validateUsageTime( - createBatteryDiffEntry( - /*foregroundUsageTimeInMs=*/ DateUtils.MINUTE_IN_MILLIS, - /*backgroundUsageTimeInMs=*/ DateUtils.MINUTE_IN_MILLIS))) - .isTrue(); - } - - @Test - public void testValidateUsageTime_foregroundTimeExceedThreshold_returnFalse() { - assertThat(BatteryChartPreferenceController.validateUsageTime( - createBatteryDiffEntry( - /*foregroundUsageTimeInMs=*/ DateUtils.HOUR_IN_MILLIS * 3, - /*backgroundUsageTimeInMs=*/ 0))) - .isFalse(); - } - - @Test - public void testValidateUsageTime_backgroundTimeExceedThreshold_returnFalse() { - assertThat(BatteryChartPreferenceController.validateUsageTime( - createBatteryDiffEntry( - /*foregroundUsageTimeInMs=*/ 0, - /*backgroundUsageTimeInMs=*/ DateUtils.HOUR_IN_MILLIS * 3))) - .isFalse(); - } - - @Test - public void testOnExpand_expandedIsTrue_addSystemEntriesToPreferenceGroup() { + public void onExpand_expandedIsTrue_addSystemEntriesToPreferenceGroup() { doReturn(1).when(mAppListGroup).getPreferenceCount(); - mBatteryChartPreferenceController.mSystemEntries.add(mBatteryDiffEntry); + mBatteryChartPreferenceController.mBatteryUsageMap = createBatteryUsageMap(); doReturn("label").when(mBatteryDiffEntry).getAppLabel(); doReturn(mDrawable).when(mBatteryDiffEntry).getAppIcon(); doReturn(PREF_KEY).when(mBatteryHistEntry).getKey(); @@ -493,10 +494,10 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testOnExpand_expandedIsFalse_removeSystemEntriesFromPreferenceGroup() { + public void onExpand_expandedIsFalse_removeSystemEntriesFromPreferenceGroup() { doReturn(PREF_KEY).when(mBatteryHistEntry).getKey(); doReturn(mPowerGaugePreference).when(mAppListGroup).findPreference(PREF_KEY); - mBatteryChartPreferenceController.mSystemEntries.add(mBatteryDiffEntry); + mBatteryChartPreferenceController.mBatteryUsageMap = createBatteryUsageMap(); // Verifies the cache is empty first. assertThat(mBatteryChartPreferenceController.mPreferenceCache).isEmpty(); @@ -513,57 +514,17 @@ public final class BatteryChartPreferenceControllerTest { } @Test - public void testOnSelect_selectSpecificTimeSlot_logMetric() { - mBatteryChartPreferenceController.onSelect(1 /*slot index*/); - - verify(mMetricsFeatureProvider) - .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT); - } - - @Test - public void testOnSelect_selectAll_logMetric() { - mBatteryChartPreferenceController.onSelect( - BatteryChartView.SELECTED_INDEX_ALL /*slot index*/); - - verify(mMetricsFeatureProvider) - .action(mContext, SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL); - } - - @Test - public void testRefreshCategoryTitle_setHourIntoBothTitleTextView() { - mBatteryChartPreferenceController = createController(); - setUpBatteryHistoryKeys(); - mBatteryChartPreferenceController.mAppListPrefGroup = - spy(new PreferenceCategory(mContext)); - mBatteryChartPreferenceController.mExpandDividerPreference = - spy(new ExpandDividerPreference(mContext)); - // Simulates select the first slot. - mBatteryChartPreferenceController.mTrapezoidIndex = 0; - - mBatteryChartPreferenceController.refreshCategoryTitle(); - - ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - // Verifies the title in the preference group. - verify(mBatteryChartPreferenceController.mAppListPrefGroup) - .setTitle(captor.capture()); - assertThat(captor.getValue()).isNotEqualTo("App usage for past 24 hr"); - // Verifies the title in the expandable divider. - captor = ArgumentCaptor.forClass(String.class); - verify(mBatteryChartPreferenceController.mExpandDividerPreference) - .setTitle(captor.capture()); - assertThat(captor.getValue()).isNotEqualTo("System usage for past 24 hr"); - } - - @Test - public void testRefreshCategoryTitle_setLast24HrIntoBothTitleTextView() { + public void refreshCategoryTitle_setLastFullChargeIntoBothTitleTextView() { mBatteryChartPreferenceController = createController(); mBatteryChartPreferenceController.mAppListPrefGroup = spy(new PreferenceCategory(mContext)); mBatteryChartPreferenceController.mExpandDividerPreference = spy(new ExpandDividerPreference(mContext)); // Simulates select all condition. - mBatteryChartPreferenceController.mTrapezoidIndex = - BatteryChartView.SELECTED_INDEX_ALL; + mBatteryChartPreferenceController.mDailyChartIndex = + BatteryChartViewModel.SELECTED_INDEX_ALL; + mBatteryChartPreferenceController.mHourlyChartIndex = + BatteryChartViewModel.SELECTED_INDEX_ALL; mBatteryChartPreferenceController.refreshCategoryTitle(); @@ -572,76 +533,93 @@ public final class BatteryChartPreferenceControllerTest { verify(mBatteryChartPreferenceController.mAppListPrefGroup) .setTitle(captor.capture()); assertThat(captor.getValue()) - .isEqualTo("App usage for past 24 hr"); + .isEqualTo("App usage since last full charge"); // Verifies the title in the expandable divider. captor = ArgumentCaptor.forClass(String.class); verify(mBatteryChartPreferenceController.mExpandDividerPreference) .setTitle(captor.capture()); assertThat(captor.getValue()) - .isEqualTo("System usage for past 24 hr"); + .isEqualTo("System usage since last full charge"); } @Test - public void testSetTimestampLabel_nullBatteryHistoryKeys_ignore() { - mBatteryChartPreferenceController = createController(); - mBatteryChartPreferenceController.mBatteryHistoryKeys = null; - mBatteryChartPreferenceController.mBatteryChartView = - spy(new BatteryChartView(mContext)); - mBatteryChartPreferenceController.setTimestampLabel(); + public void selectedSlotText_selectAllDaysAllHours_returnNull() { + mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(60)); + mBatteryChartPreferenceController.mDailyChartIndex = + BatteryChartViewModel.SELECTED_INDEX_ALL; + mBatteryChartPreferenceController.mHourlyChartIndex = + BatteryChartViewModel.SELECTED_INDEX_ALL; - verify(mBatteryChartPreferenceController.mBatteryChartView, never()) - .setLatestTimestamp(anyLong()); + assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo(null); } @Test - public void testSetTimestampLabel_setExpectedTimestampData() { - mBatteryChartPreferenceController = createController(); - mBatteryChartPreferenceController.mBatteryChartView = - spy(new BatteryChartView(mContext)); - setUpBatteryHistoryKeys(); + public void selectedSlotText_onlyOneDayDataSelectAllHours_returnNull() { + mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6)); + mBatteryChartPreferenceController.mDailyChartIndex = 0; + mBatteryChartPreferenceController.mHourlyChartIndex = + BatteryChartViewModel.SELECTED_INDEX_ALL; - mBatteryChartPreferenceController.setTimestampLabel(); - - verify(mBatteryChartPreferenceController.mBatteryChartView) - .setLatestTimestamp(1619247636826L); + assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo(null); } @Test - public void testSetTimestampLabel_withoutValidTimestamp_setExpectedTimestampData() { - mBatteryChartPreferenceController = createController(); - mBatteryChartPreferenceController.mBatteryChartView = - spy(new BatteryChartView(mContext)); - mBatteryChartPreferenceController.mBatteryHistoryKeys = new long[]{0L}; + public void selectedSlotText_selectADayAllHours_onlyDayText() { + mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(60)); + mBatteryChartPreferenceController.mDailyChartIndex = 1; + mBatteryChartPreferenceController.mHourlyChartIndex = + BatteryChartViewModel.SELECTED_INDEX_ALL; - mBatteryChartPreferenceController.setTimestampLabel(); - - verify(mBatteryChartPreferenceController.mBatteryChartView) - .setLatestTimestamp(anyLong()); + assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo("Sunday"); } @Test - public void testOnSaveInstanceState_restoreSelectedIndexAndExpandState() { - final int expectedIndex = 1; + public void selectedSlotText_onlyOneDayDataSelectAnHour_onlyHourText() { + mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(6)); + mBatteryChartPreferenceController.mDailyChartIndex = 0; + mBatteryChartPreferenceController.mHourlyChartIndex = 1; + + assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo( + "10 am - 12 pm"); + } + + @Test + public void selectedSlotText_SelectADayAnHour_dayAndHourText() { + mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(60)); + mBatteryChartPreferenceController.mDailyChartIndex = 1; + mBatteryChartPreferenceController.mHourlyChartIndex = 8; + + assertThat(mBatteryChartPreferenceController.getSlotInformation()).isEqualTo( + "Sunday 4 pm - 6 pm"); + } + + @Test + public void onSaveInstanceState_restoreSelectedIndexAndExpandState() { + final int expectedDailyIndex = 1; + final int expectedHourlyIndex = 2; final boolean isExpanded = true; final Bundle bundle = new Bundle(); - mBatteryChartPreferenceController.mTrapezoidIndex = expectedIndex; + mBatteryChartPreferenceController.mDailyChartIndex = expectedDailyIndex; + mBatteryChartPreferenceController.mHourlyChartIndex = expectedHourlyIndex; mBatteryChartPreferenceController.mIsExpanded = isExpanded; mBatteryChartPreferenceController.onSaveInstanceState(bundle); // Replaces the original controller with other values. - mBatteryChartPreferenceController.mTrapezoidIndex = -1; + mBatteryChartPreferenceController.mDailyChartIndex = -1; + mBatteryChartPreferenceController.mHourlyChartIndex = -1; mBatteryChartPreferenceController.mIsExpanded = false; mBatteryChartPreferenceController.onCreate(bundle); - mBatteryChartPreferenceController.setBatteryHistoryMap( - createBatteryHistoryMap()); + mBatteryChartPreferenceController.setBatteryHistoryMap(createBatteryHistoryMap(25)); - assertThat(mBatteryChartPreferenceController.mTrapezoidIndex) - .isEqualTo(expectedIndex); + assertThat(mBatteryChartPreferenceController.mDailyChartIndex) + .isEqualTo(expectedDailyIndex); + assertThat(mBatteryChartPreferenceController.mHourlyChartIndex) + .isEqualTo(expectedHourlyIndex); assertThat(mBatteryChartPreferenceController.mIsExpanded).isTrue(); } @Test - public void testIsValidToShowSummary_returnExpectedResult() { + public void isValidToShowSummary_returnExpectedResult() { assertThat(mBatteryChartPreferenceController .isValidToShowSummary("com.google.android.apps.scone")) .isTrue(); @@ -652,31 +630,42 @@ public final class BatteryChartPreferenceControllerTest { .isFalse(); } - @Test - public void testIsValidToShowEntry_returnExpectedResult() { - assertThat(mBatteryChartPreferenceController - .isValidToShowEntry("com.google.android.apps.scone")) - .isTrue(); - - // Verifies the items which are defined in the array list. - assertThat(mBatteryChartPreferenceController - .isValidToShowEntry("com.android.gms.persistent")) - .isFalse(); + private static Long generateTimestamp(int index) { + // "2021-04-23 07:00:00 UTC" + index hours + return 1619247600000L + index * DateUtils.HOUR_IN_MILLIS; } - private static Map> createBatteryHistoryMap() { + private static Map> createBatteryHistoryMap( + int numOfHours) { final Map> batteryHistoryMap = new HashMap<>(); - for (int index = 0; index < DESIRED_HISTORY_SIZE; index++) { + for (int index = 0; index < numOfHours; index++) { final ContentValues values = new ContentValues(); values.put("batteryLevel", Integer.valueOf(100 - index)); + values.put("consumePower", Integer.valueOf(100 - index)); final BatteryHistEntry entry = new BatteryHistEntry(values); final Map entryMap = new HashMap<>(); entryMap.put("fake_entry_key" + index, entry); - batteryHistoryMap.put(Long.valueOf(index + 1), entryMap); + batteryHistoryMap.put(generateTimestamp(index), entryMap); } return batteryHistoryMap; } + private Map> createBatteryUsageMap() { + final int selectedAll = BatteryChartViewModel.SELECTED_INDEX_ALL; + return Map.of( + selectedAll, Map.of( + selectedAll, new BatteryDiffData( + Arrays.asList(mBatteryDiffEntry), + Arrays.asList(mBatteryDiffEntry))), + 0, Map.of( + selectedAll, new BatteryDiffData( + Arrays.asList(mBatteryDiffEntry), + Arrays.asList(mBatteryDiffEntry)), + 0, new BatteryDiffData( + Arrays.asList(mBatteryDiffEntry), + Arrays.asList(mBatteryDiffEntry)))); + } + private BatteryDiffEntry createBatteryDiffEntry( long foregroundUsageTimeInMs, long backgroundUsageTimeInMs) { return new BatteryDiffEntry( @@ -684,13 +673,6 @@ public final class BatteryChartPreferenceControllerTest { /*consumePower=*/ 0, mBatteryHistEntry); } - private void setUpBatteryHistoryKeys() { - mBatteryChartPreferenceController.mBatteryHistoryKeys = - new long[]{1619196786769L, 0L, 1619247636826L}; - ConvertUtils.utcToLocalTimeHour( - mContext, /*timestamp=*/ 0, /*is24HourFormat=*/ false); - } - private BatteryChartPreferenceController createController() { final BatteryChartPreferenceController controller = new BatteryChartPreferenceController( diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewTest.java index a2d8ca95c07..8a430875614 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryChartViewTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.when; import android.accessibilityservice.AccessibilityServiceInfo; import android.content.Context; import android.os.LocaleList; +import android.view.View; import android.view.accessibility.AccessibilityManager; import com.android.settings.fuelgauge.PowerUsageFeatureProvider; @@ -41,6 +42,7 @@ import org.robolectric.RuntimeEnvironment; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Locale; @RunWith(RobolectricTestRunner.class) @@ -55,6 +57,8 @@ public final class BatteryChartViewTest { private AccessibilityServiceInfo mMockAccessibilityServiceInfo; @Mock private AccessibilityManager mMockAccessibilityManager; + @Mock + private View mMockView; @Before public void setUp() { @@ -74,13 +78,13 @@ public final class BatteryChartViewTest { } @Test - public void testIsAccessibilityEnabled_disable_returnFalse() { + public void isAccessibilityEnabled_disable_returnFalse() { doReturn(false).when(mMockAccessibilityManager).isEnabled(); assertThat(BatteryChartView.isAccessibilityEnabled(mContext)).isFalse(); } @Test - public void testIsAccessibilityEnabled_emptyInfo_returnFalse() { + public void isAccessibilityEnabled_emptyInfo_returnFalse() { doReturn(true).when(mMockAccessibilityManager).isEnabled(); doReturn(new ArrayList()) .when(mMockAccessibilityManager) @@ -90,68 +94,70 @@ public final class BatteryChartViewTest { } @Test - public void testIsAccessibilityEnabled_validServiceId_returnTrue() { + public void isAccessibilityEnabled_validServiceId_returnTrue() { doReturn(true).when(mMockAccessibilityManager).isEnabled(); assertThat(BatteryChartView.isAccessibilityEnabled(mContext)).isTrue(); } @Test - public void testSetSelectedIndex_invokesCallback() { + public void onClick_invokesCallback() { + final int originalSelectedIndex = 2; + BatteryChartViewModel batteryChartViewModel = new BatteryChartViewModel( + List.of(90, 80, 70, 60), List.of("", "", "", ""), + BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS); + batteryChartViewModel.setSelectedIndex(originalSelectedIndex); + mBatteryChartView.setViewModel(batteryChartViewModel); + for (int i = 0; i < mBatteryChartView.mTrapezoidSlots.length; i++) { + mBatteryChartView.mTrapezoidSlots[i] = new BatteryChartView.TrapezoidSlot(); + mBatteryChartView.mTrapezoidSlots[i].mLeft = i; + mBatteryChartView.mTrapezoidSlots[i].mRight = i + 0.5f; + } final int[] selectedIndex = new int[1]; - final int expectedIndex = 2; - mBatteryChartView.mSelectedIndex = 1; mBatteryChartView.setOnSelectListener( trapezoidIndex -> { selectedIndex[0] = trapezoidIndex; }); - mBatteryChartView.setSelectedIndex(expectedIndex); + // Verify onClick() a different index 1. + mBatteryChartView.mTouchUpEventX = 1; + selectedIndex[0] = Integer.MIN_VALUE; + mBatteryChartView.onClick(mMockView); + assertThat(selectedIndex[0]).isEqualTo(1); - assertThat(mBatteryChartView.mSelectedIndex) - .isEqualTo(expectedIndex); - assertThat(selectedIndex[0]).isEqualTo(expectedIndex); + // Verify onClick() the same index 2. + mBatteryChartView.mTouchUpEventX = 2; + selectedIndex[0] = Integer.MIN_VALUE; + mBatteryChartView.onClick(mMockView); + assertThat(selectedIndex[0]).isEqualTo(BatteryChartViewModel.SELECTED_INDEX_ALL); } @Test - public void testSetSelectedIndex_sameIndex_notInvokesCallback() { - final int[] selectedIndex = new int[1]; - final int expectedIndex = 1; - mBatteryChartView.mSelectedIndex = expectedIndex; - mBatteryChartView.setOnSelectListener( - trapezoidIndex -> { - selectedIndex[0] = trapezoidIndex; - }); - - mBatteryChartView.setSelectedIndex(expectedIndex); - - assertThat(selectedIndex[0]).isNotEqualTo(expectedIndex); - } - - @Test - public void testClickable_isChartGraphSlotsEnabledIsFalse_notClickable() { + public void clickable_isChartGraphSlotsEnabledIsFalse_notClickable() { mBatteryChartView.setClickableForce(true); when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext)) .thenReturn(false); mBatteryChartView.onAttachedToWindow(); + assertThat(mBatteryChartView.isClickable()).isFalse(); assertThat(mBatteryChartView.mTrapezoidCurvePaint).isNotNull(); } @Test - public void testClickable_accessibilityIsDisabled_clickable() { + public void clickable_accessibilityIsDisabled_clickable() { mBatteryChartView.setClickableForce(true); when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext)) .thenReturn(true); doReturn(false).when(mMockAccessibilityManager).isEnabled(); mBatteryChartView.onAttachedToWindow(); + assertThat(mBatteryChartView.isClickable()).isTrue(); assertThat(mBatteryChartView.mTrapezoidCurvePaint).isNull(); } @Test - public void testClickable_accessibilityIsEnabledWithoutValidId_clickable() { + public void clickable_accessibilityIsEnabledWithoutValidId_clickable() { mBatteryChartView.setClickableForce(true); when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext)) .thenReturn(true); @@ -161,30 +167,34 @@ public final class BatteryChartViewTest { .getEnabledAccessibilityServiceList(anyInt()); mBatteryChartView.onAttachedToWindow(); + assertThat(mBatteryChartView.isClickable()).isTrue(); assertThat(mBatteryChartView.mTrapezoidCurvePaint).isNull(); } @Test - public void testClickable_accessibilityIsEnabledWithValidId_notClickable() { + public void clickable_accessibilityIsEnabledWithValidId_notClickable() { mBatteryChartView.setClickableForce(true); when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext)) .thenReturn(true); doReturn(true).when(mMockAccessibilityManager).isEnabled(); mBatteryChartView.onAttachedToWindow(); + assertThat(mBatteryChartView.isClickable()).isFalse(); assertThat(mBatteryChartView.mTrapezoidCurvePaint).isNotNull(); } @Test - public void testClickable_restoreFromNonClickableState() { - final int[] levels = new int[13]; - for (int index = 0; index < levels.length; index++) { - levels[index] = index + 1; + public void clickable_restoreFromNonClickableState() { + final List levels = new ArrayList(); + final List texts = new ArrayList(); + for (int index = 0; index < 13; index++) { + levels.add(index + 1); + texts.add(""); } - mBatteryChartView.setTrapezoidCount(12); - mBatteryChartView.setLevels(levels); + mBatteryChartView.setViewModel(new BatteryChartViewModel(levels, texts, + BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS)); mBatteryChartView.setClickableForce(true); when(mPowerUsageFeatureProvider.isChartGraphSlotsEnabled(mContext)) .thenReturn(true); @@ -201,14 +211,14 @@ public final class BatteryChartViewTest { } @Test - public void testOnAttachedToWindow_addAccessibilityStateChangeListener() { + public void onAttachedToWindow_addAccessibilityStateChangeListener() { mBatteryChartView.onAttachedToWindow(); verify(mMockAccessibilityManager) .addAccessibilityStateChangeListener(mBatteryChartView); } @Test - public void testOnDetachedFromWindow_removeAccessibilityStateChangeListener() { + public void onDetachedFromWindow_removeAccessibilityStateChangeListener() { mBatteryChartView.onAttachedToWindow(); mBatteryChartView.mHandler.postDelayed( mBatteryChartView.mUpdateClickableStateRun, 1000); @@ -223,7 +233,7 @@ public final class BatteryChartViewTest { } @Test - public void testOnAccessibilityStateChanged_postUpdateStateRunnable() { + public void onAccessibilityStateChanged_postUpdateStateRunnable() { mBatteryChartView.mHandler = spy(mBatteryChartView.mHandler); mBatteryChartView.onAccessibilityStateChanged(/*enabled=*/ true); diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoaderTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoaderTest.java index 98a44de81b8..57178578169 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoaderTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/BatteryHistoryLoaderTest.java @@ -53,7 +53,7 @@ public final class BatteryHistoryLoaderTest { public void testLoadIBackground_returnsMapFromPowerFeatureProvider() { final Map> batteryHistoryMap = new HashMap<>(); doReturn(batteryHistoryMap).when(mFeatureFactory.powerUsageFeatureProvider) - .getBatteryHistory(mContext); + .getBatteryHistorySinceLastFullCharge(mContext); assertThat(mBatteryHistoryLoader.loadInBackground()) .isSameInstanceAs(batteryHistoryMap); diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java index c1f981539c1..c9bac030ac8 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/ConvertUtilsTest.java @@ -26,6 +26,7 @@ import android.os.BatteryManager; import android.os.BatteryUsageStats; import android.os.LocaleList; import android.os.UserHandle; +import android.text.format.DateUtils; import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.fuelgauge.PowerUsageFeatureProvider; @@ -39,8 +40,8 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import java.util.Arrays; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -173,7 +174,8 @@ public final class ConvertUtilsTest { public void getIndexedUsageMap_returnsExpectedResult() { // Creates the fake testing data. final int timeSlotSize = 2; - final long[] batteryHistoryKeys = new long[]{101L, 102L, 103L, 104L, 105L}; + final long[] batteryHistoryKeys = new long[]{generateTimestamp(0), generateTimestamp(1), + generateTimestamp(2), generateTimestamp(3), generateTimestamp(4)}; final Map> batteryHistoryMap = new HashMap<>(); final BatteryHistEntry fakeEntry = createBatteryHistEntry( @@ -270,11 +272,11 @@ public final class ConvertUtilsTest { for (int index = 0; index < remainingSize; index++) { batteryHistoryMap.put(105L + index + 1, new HashMap<>()); } - when(mPowerUsageFeatureProvider.getBatteryHistory(mContext)) + when(mPowerUsageFeatureProvider.getBatteryHistorySinceLastFullCharge(mContext)) .thenReturn(batteryHistoryMap); final List batteryDiffEntryList = - BatteryChartPreferenceController.getBatteryLast24HrUsageData(mContext); + BatteryChartPreferenceController.getAppBatteryUsageData(mContext); assertThat(batteryDiffEntryList).isNotEmpty(); final BatteryDiffEntry resultEntry = batteryDiffEntryList.get(0); @@ -472,4 +474,9 @@ public final class ConvertUtilsTest { assertThat(entry.mForegroundUsageTimeInMs).isEqualTo(foregroundUsageTimeInMs); assertThat(entry.mBackgroundUsageTimeInMs).isEqualTo(backgroundUsageTimeInMs); } + + private static Long generateTimestamp(int index) { + // "2021-04-23 07:00:00 UTC" + index hours + return 1619247600000L + index * DateUtils.HOUR_IN_MILLIS; + } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java new file mode 100644 index 00000000000..883b0e7db91 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/DataProcessorTest.java @@ -0,0 +1,950 @@ +/* + * Copyright (C) 2022 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 static org.mockito.Mockito.when; + +import android.content.ContentValues; +import android.content.Context; +import android.text.format.DateUtils; + +import com.android.settings.fuelgauge.BatteryUtils; +import com.android.settings.fuelgauge.PowerUsageFeatureProvider; +import com.android.settings.testutils.FakeFeatureFactory; + +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; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +@RunWith(RobolectricTestRunner.class) +public class DataProcessorTest { + private static final String FAKE_ENTRY_KEY = "fake_entry_key"; + + private Context mContext; + + private FakeFeatureFactory mFeatureFactory; + private PowerUsageFeatureProvider mPowerUsageFeatureProvider; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + TimeZone.setDefault(TimeZone.getTimeZone("GMT+8")); + + mContext = spy(RuntimeEnvironment.application); + mFeatureFactory = FakeFeatureFactory.setupForTest(); + mPowerUsageFeatureProvider = mFeatureFactory.powerUsageFeatureProvider; + } + + @Test + public void getBatteryLevelData_emptyHistoryMap_returnNull() { + assertThat(DataProcessor.getBatteryLevelData( + mContext, + /*handler=*/ null, + /*batteryHistoryMap=*/ null, + /*asyncResponseDelegate=*/ null)) + .isNull(); + assertThat(DataProcessor.getBatteryLevelData( + mContext, /*handler=*/ null, new HashMap<>(), /*asyncResponseDelegate=*/ null)) + .isNull(); + } + + @Test + public void getBatteryLevelData_notEnoughData_returnNull() { + // The timestamps are within 1 hour. + final long[] timestamps = {1000000L, 2000000L, 3000000L}; + final int[] levels = {100, 99, 98}; + final Map> batteryHistoryMap = + createHistoryMap(timestamps, levels); + + assertThat(DataProcessor.getBatteryLevelData( + mContext, /*handler=*/ null, batteryHistoryMap, /*asyncResponseDelegate=*/ null)) + .isNull(); + } + + @Test + public void getBatteryLevelData_returnExpectedResult() { + // Timezone GMT+8: 2022-01-01 00:00:00, 2022-01-01 01:00:00, 2022-01-01 02:00:00 + final long[] timestamps = {1640966400000L, 1640970000000L, 1640973600000L}; + final int[] levels = {100, 99, 98}; + final Map> batteryHistoryMap = + createHistoryMap(timestamps, levels); + + final BatteryLevelData resultData = + DataProcessor.getBatteryLevelData( + mContext, + /*handler=*/ null, + batteryHistoryMap, + /*asyncResponseDelegate=*/ null); + + final List expectedDailyTimestamps = List.of(timestamps[0], timestamps[2]); + final List expectedDailyLevels = List.of(levels[0], levels[2]); + final List> expectedHourlyTimestamps = List.of(expectedDailyTimestamps); + final List> expectedHourlyLevels = List.of(expectedDailyLevels); + verifyExpectedBatteryLevelData( + resultData, + expectedDailyTimestamps, + expectedDailyLevels, + expectedHourlyTimestamps, + expectedHourlyLevels); + } + + @Test + public void getHistoryMapWithExpectedTimestamps_emptyHistoryMap_returnEmptyMap() { + assertThat(DataProcessor + .getHistoryMapWithExpectedTimestamps(mContext, new HashMap<>())) + .isEmpty(); + } + + @Test + public void getHistoryMapWithExpectedTimestamps_returnExpectedMap() { + // Timezone GMT+8 + final long[] timestamps = { + 1640966700000L, // 2022-01-01 00:05:00 + 1640970180000L, // 2022-01-01 01:03:00 + 1640973840000L, // 2022-01-01 02:04:00 + 1640978100000L, // 2022-01-01 03:15:00 + 1640981400000L // 2022-01-01 04:10:00 + }; + final int[] levels = {100, 94, 90, 82, 50}; + final Map> batteryHistoryMap = + createHistoryMap(timestamps, levels); + + final Map> resultMap = + DataProcessor.getHistoryMapWithExpectedTimestamps(mContext, batteryHistoryMap); + + // Timezone GMT+8 + final long[] expectedTimestamps = { + 1640966400000L, // 2022-01-01 00:00:00 + 1640970000000L, // 2022-01-01 01:00:00 + 1640973600000L, // 2022-01-01 02:00:00 + 1640977200000L, // 2022-01-01 03:00:00 + 1640980800000L // 2022-01-01 04:00:00 + }; + final int[] expectedLevels = {100, 94, 90, 84, 56}; + assertThat(resultMap).hasSize(expectedLevels.length); + for (int index = 0; index < expectedLevels.length; index++) { + assertThat(resultMap.get(expectedTimestamps[index]).get(FAKE_ENTRY_KEY).mBatteryLevel) + .isEqualTo(expectedLevels[index]); + } + } + + @Test + public void getLevelDataThroughProcessedHistoryMap_notEnoughData_returnNull() { + final long[] timestamps = {100L}; + final int[] levels = {100}; + final Map> batteryHistoryMap = + createHistoryMap(timestamps, levels); + + assertThat( + DataProcessor.getLevelDataThroughProcessedHistoryMap(mContext, batteryHistoryMap)) + .isNull(); + } + + @Test + public void getLevelDataThroughProcessedHistoryMap_OneDayData_returnExpectedResult() { + // Timezone GMT+8 + final long[] timestamps = { + 1640966400000L, // 2022-01-01 00:00:00 + 1640970000000L, // 2022-01-01 01:00:00 + 1640973600000L, // 2022-01-01 02:00:00 + 1640977200000L, // 2022-01-01 03:00:00 + 1640980800000L // 2022-01-01 04:00:00 + }; + final int[] levels = {100, 94, 90, 82, 50}; + final Map> batteryHistoryMap = + createHistoryMap(timestamps, levels); + + final BatteryLevelData resultData = + DataProcessor.getLevelDataThroughProcessedHistoryMap(mContext, batteryHistoryMap); + + final List expectedDailyTimestamps = List.of(timestamps[0], timestamps[4]); + final List expectedDailyLevels = List.of(levels[0], levels[4]); + final List> expectedHourlyTimestamps = List.of( + List.of(timestamps[0], timestamps[2], timestamps[4]) + ); + final List> expectedHourlyLevels = List.of( + List.of(levels[0], levels[2], levels[4]) + ); + verifyExpectedBatteryLevelData( + resultData, + expectedDailyTimestamps, + expectedDailyLevels, + expectedHourlyTimestamps, + expectedHourlyLevels); + } + + @Test + public void getLevelDataThroughProcessedHistoryMap_MultipleDaysData_returnExpectedResult() { + // Timezone GMT+8 + final long[] timestamps = { + 1641038400000L, // 2022-01-01 20:00:00 + 1641060000000L, // 2022-01-02 02:00:00 + 1641067200000L, // 2022-01-02 04:00:00 + 1641081600000L, // 2022-01-02 08:00:00 + }; + final int[] levels = {100, 94, 90, 82}; + final Map> batteryHistoryMap = + createHistoryMap(timestamps, levels); + + final BatteryLevelData resultData = + DataProcessor.getLevelDataThroughProcessedHistoryMap(mContext, batteryHistoryMap); + + final List expectedDailyTimestamps = List.of( + 1641038400000L, // 2022-01-01 20:00:00 + 1641052800000L, // 2022-01-02 00:00:00 + 1641081600000L // 2022-01-02 08:00:00 + ); + final List expectedDailyLevels = new ArrayList<>(); + expectedDailyLevels.add(100); + expectedDailyLevels.add(null); + expectedDailyLevels.add(82); + final List> expectedHourlyTimestamps = List.of( + List.of( + 1641038400000L, // 2022-01-01 20:00:00 + 1641045600000L, // 2022-01-01 22:00:00 + 1641052800000L // 2022-01-02 00:00:00 + ), + List.of( + 1641052800000L, // 2022-01-02 00:00:00 + 1641060000000L, // 2022-01-02 02:00:00 + 1641067200000L, // 2022-01-02 04:00:00 + 1641074400000L, // 2022-01-02 06:00:00 + 1641081600000L // 2022-01-02 08:00:00 + ) + ); + final List expectedHourlyLevels1 = new ArrayList<>(); + expectedHourlyLevels1.add(100); + expectedHourlyLevels1.add(null); + expectedHourlyLevels1.add(null); + final List expectedHourlyLevels2 = new ArrayList<>(); + expectedHourlyLevels2.add(null); + expectedHourlyLevels2.add(94); + expectedHourlyLevels2.add(90); + expectedHourlyLevels2.add(null); + expectedHourlyLevels2.add(82); + final List> expectedHourlyLevels = List.of( + expectedHourlyLevels1, + expectedHourlyLevels2 + ); + verifyExpectedBatteryLevelData( + resultData, + expectedDailyTimestamps, + expectedDailyLevels, + expectedHourlyTimestamps, + expectedHourlyLevels); + } + + @Test + public void getTimestampSlots_emptyRawList_returnEmptyList() { + final List resultList = + DataProcessor.getTimestampSlots(new ArrayList<>()); + assertThat(resultList).isEmpty(); + } + + @Test + public void getTimestampSlots_startWithEvenHour_returnExpectedResult() { + final Calendar startCalendar = Calendar.getInstance(); + startCalendar.set(2022, 6, 5, 6, 30, 50); // 2022-07-05 06:30:50 + final Calendar endCalendar = Calendar.getInstance(); + endCalendar.set(2022, 6, 5, 22, 30, 50); // 2022-07-05 22:30:50 + + final Calendar expectedStartCalendar = Calendar.getInstance(); + expectedStartCalendar.set(2022, 6, 5, 6, 0, 0); // 2022-07-05 06:00:00 + final Calendar expectedEndCalendar = Calendar.getInstance(); + expectedEndCalendar.set(2022, 6, 5, 22, 0, 0); // 2022-07-05 22:00:00 + verifyExpectedTimestampSlots( + startCalendar, endCalendar, expectedStartCalendar, expectedEndCalendar); + } + + @Test + public void getTimestampSlots_startWithOddHour_returnExpectedResult() { + final Calendar startCalendar = Calendar.getInstance(); + startCalendar.set(2022, 6, 5, 5, 0, 50); // 2022-07-05 05:00:50 + final Calendar endCalendar = Calendar.getInstance(); + endCalendar.set(2022, 6, 6, 21, 00, 50); // 2022-07-06 21:00:50 + + final Calendar expectedStartCalendar = Calendar.getInstance(); + expectedStartCalendar.set(2022, 6, 5, 6, 00, 00); // 2022-07-05 06:00:00 + final Calendar expectedEndCalendar = Calendar.getInstance(); + expectedEndCalendar.set(2022, 6, 6, 20, 00, 00); // 2022-07-06 20:00:00 + verifyExpectedTimestampSlots( + startCalendar, endCalendar, expectedStartCalendar, expectedEndCalendar); + } + + @Test + public void getDailyTimestamps_notEnoughData_returnEmptyList() { + assertThat(DataProcessor.getDailyTimestamps(new ArrayList<>())).isEmpty(); + assertThat(DataProcessor.getDailyTimestamps(List.of(100L))).isEmpty(); + } + + @Test + public void getDailyTimestamps_OneDayData_returnExpectedList() { + // Timezone GMT+8 + final List timestamps = List.of( + 1640966400000L, // 2022-01-01 00:00:00 + 1640970000000L, // 2022-01-01 01:00:00 + 1640973600000L, // 2022-01-01 02:00:00 + 1640977200000L, // 2022-01-01 03:00:00 + 1640980800000L // 2022-01-01 04:00:00 + ); + + final List expectedTimestamps = List.of( + 1640966400000L, // 2022-01-01 00:00:00 + 1640980800000L // 2022-01-01 04:00:00 + ); + assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps); + } + + @Test + public void getDailyTimestamps_MultipleDaysData_returnExpectedList() { + // Timezone GMT+8 + final List timestamps = List.of( + 1640988000000L, // 2022-01-01 06:00:00 + 1641060000000L, // 2022-01-02 02:00:00 + 1641160800000L, // 2022-01-03 06:00:00 + 1641254400000L // 2022-01-04 08:00:00 + ); + + final List expectedTimestamps = List.of( + 1640988000000L, // 2022-01-01 06:00:00 + 1641052800000L, // 2022-01-02 00:00:00 + 1641139200000L, // 2022-01-03 00:00:00 + 1641225600000L, // 2022-01-04 00:00:00 + 1641254400000L // 2022-01-04 08:00:00 + ); + assertThat(DataProcessor.getDailyTimestamps(timestamps)).isEqualTo(expectedTimestamps); + } + + @Test + public void isFromFullCharge_emptyData_returnFalse() { + assertThat(DataProcessor.isFromFullCharge(null)).isFalse(); + assertThat(DataProcessor.isFromFullCharge(new HashMap<>())).isFalse(); + } + + @Test + public void isFromFullCharge_notChargedData_returnFalse() { + final Map entryMap = new HashMap<>(); + final ContentValues values = new ContentValues(); + values.put("batteryLevel", 98); + final BatteryHistEntry entry = new BatteryHistEntry(values); + entryMap.put(FAKE_ENTRY_KEY, entry); + + assertThat(DataProcessor.isFromFullCharge(entryMap)).isFalse(); + } + + @Test + public void isFromFullCharge_chargedData_returnTrue() { + final Map entryMap = new HashMap<>(); + final ContentValues values = new ContentValues(); + values.put("batteryLevel", 100); + final BatteryHistEntry entry = new BatteryHistEntry(values); + entryMap.put(FAKE_ENTRY_KEY, entry); + + assertThat(DataProcessor.isFromFullCharge(entryMap)).isTrue(); + } + + @Test + public void findNearestTimestamp_returnExpectedResult() { + long[] results = DataProcessor.findNearestTimestamp( + Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 15L); + assertThat(results).isEqualTo(new long[] {10L, 20L}); + + results = DataProcessor.findNearestTimestamp( + Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 10L); + assertThat(results).isEqualTo(new long[] {10L, 10L}); + + results = DataProcessor.findNearestTimestamp( + Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 5L); + assertThat(results).isEqualTo(new long[] {0L, 10L}); + + results = DataProcessor.findNearestTimestamp( + Arrays.asList(10L, 20L, 30L, 40L), /*target=*/ 50L); + assertThat(results).isEqualTo(new long[] {40L, 0L}); + } + + @Test + public void getTimestampOfNextDay_returnExpectedResult() { + // 2021-02-28 06:00:00 => 2021-03-01 00:00:00 + assertThat(DataProcessor.getTimestampOfNextDay(1614463200000L)) + .isEqualTo(1614528000000L); + // 2021-12-31 16:00:00 => 2022-01-01 00:00:00 + assertThat(DataProcessor.getTimestampOfNextDay(1640937600000L)) + .isEqualTo(1640966400000L); + } + + @Test + public void isForDailyChart_returnExpectedResult() { + assertThat(DataProcessor.isForDailyChart(/*isStartOrEnd=*/ true, 0L)).isTrue(); + // 2022-01-01 00:00:00 + assertThat(DataProcessor.isForDailyChart(/*isStartOrEnd=*/ false, 1640966400000L)) + .isTrue(); + // 2022-01-01 01:00:05 + assertThat(DataProcessor.isForDailyChart(/*isStartOrEnd=*/ false, 1640970005000L)) + .isFalse(); + } + + @Test + public void getBatteryUsageMap_emptyHistoryMap_returnNull() { + final List hourlyBatteryLevelsPerDay = + new ArrayList<>(); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(new ArrayList<>(), new ArrayList<>())); + + assertThat(DataProcessor.getBatteryUsageMap( + mContext, hourlyBatteryLevelsPerDay, new HashMap<>())).isNull(); + } + + @Test + public void getBatteryUsageMap_returnsExpectedResult() { + final long[] batteryHistoryKeys = new long[]{ + 1641045600000L, // 2022-01-01 22:00:00 + 1641049200000L, // 2022-01-01 23:00:00 + 1641052800000L, // 2022-01-02 00:00:00 + 1641056400000L, // 2022-01-02 01:00:00 + 1641060000000L, // 2022-01-02 02:00:00 + }; + final Map> batteryHistoryMap = new HashMap<>(); + final int currentUserId = mContext.getUserId(); + final BatteryHistEntry fakeEntry = createBatteryHistEntry( + ConvertUtils.FAKE_PACKAGE_NAME, "fake_label", /*consumePower=*/ 0, /*uid=*/ 0L, + currentUserId, ConvertUtils.CONSUMER_TYPE_UID_BATTERY, + /*foregroundUsageTimeInMs=*/ 0L, /*backgroundUsageTimeInMs=*/ 0L); + // Adds the index = 0 data. + Map entryMap = new HashMap<>(); + BatteryHistEntry entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 5.0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + entryMap.put(fakeEntry.getKey(), fakeEntry); + batteryHistoryMap.put(batteryHistoryKeys[0], entryMap); + // Adds the index = 1 data. + entryMap = new HashMap<>(); + entryMap.put(fakeEntry.getKey(), fakeEntry); + batteryHistoryMap.put(batteryHistoryKeys[1], entryMap); + // Adds the index = 2 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 20.0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 15L, + 25L); + entryMap.put(entry.getKey(), entry); + entryMap.put(fakeEntry.getKey(), fakeEntry); + batteryHistoryMap.put(batteryHistoryKeys[2], entryMap); + // Adds the index = 3 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 40.0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 25L, + /*backgroundUsageTimeInMs=*/ 35L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 10.0, /*uid=*/ 3L, currentUserId, + ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY, /*foregroundUsageTimeInMs=*/ 40L, + /*backgroundUsageTimeInMs=*/ 50L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package3", "label3", /*consumePower=*/ 15.0, /*uid=*/ 4L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 5L, + /*backgroundUsageTimeInMs=*/ 5L); + entryMap.put(entry.getKey(), entry); + entryMap.put(fakeEntry.getKey(), fakeEntry); + batteryHistoryMap.put(batteryHistoryKeys[3], entryMap); + // Adds the index = 4 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 40.0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 30L, + /*backgroundUsageTimeInMs=*/ 40L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 20.0, /*uid=*/ 3L, currentUserId, + ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY, /*foregroundUsageTimeInMs=*/ 50L, + /*backgroundUsageTimeInMs=*/ 60L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package3", "label3", /*consumePower=*/ 40.0, /*uid=*/ 4L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 5L, + /*backgroundUsageTimeInMs=*/ 5L); + entryMap.put(entry.getKey(), entry); + entryMap.put(fakeEntry.getKey(), fakeEntry); + batteryHistoryMap.put(batteryHistoryKeys[4], entryMap); + final List hourlyBatteryLevelsPerDay = + new ArrayList<>(); + // Adds the day 1 data. + List timestamps = + List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]); + final List levels = List.of(100, 100); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels)); + // Adds the day 2 data. + timestamps = List.of(batteryHistoryKeys[2], batteryHistoryKeys[4]); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels)); + + final Map> resultMap = + DataProcessor.getBatteryUsageMap( + mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap); + + BatteryDiffData resultDiffData = + resultMap + .get(DataProcessor.SELECTED_INDEX_ALL) + .get(DataProcessor.SELECTED_INDEX_ALL); + assertBatteryDiffEntry( + resultDiffData.getAppDiffEntryList().get(0), currentUserId, /*uid=*/ 2L, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 40.0, + /*foregroundUsageTimeInMs=*/ 30, /*backgroundUsageTimeInMs=*/ 40); + assertBatteryDiffEntry( + resultDiffData.getAppDiffEntryList().get(1), currentUserId, /*uid=*/ 4L, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 40.0, + /*foregroundUsageTimeInMs=*/ 5, /*backgroundUsageTimeInMs=*/ 5); + assertBatteryDiffEntry( + resultDiffData.getSystemDiffEntryList().get(0), currentUserId, /*uid=*/ 3L, + ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY, /*consumePercentage=*/ 20.0, + /*foregroundUsageTimeInMs=*/ 50, /*backgroundUsageTimeInMs=*/ 60); + resultDiffData = resultMap.get(0).get(DataProcessor.SELECTED_INDEX_ALL); + assertBatteryDiffEntry( + resultDiffData.getAppDiffEntryList().get(0), currentUserId, /*uid=*/ 2L, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 100.0, + /*foregroundUsageTimeInMs=*/ 15, /*backgroundUsageTimeInMs=*/ 25); + resultDiffData = resultMap.get(1).get(DataProcessor.SELECTED_INDEX_ALL); + assertBatteryDiffEntry( + resultDiffData.getAppDiffEntryList().get(0), currentUserId, /*uid=*/ 4L, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 50.0, + /*foregroundUsageTimeInMs=*/ 5, /*backgroundUsageTimeInMs=*/ 5); + assertBatteryDiffEntry( + resultDiffData.getAppDiffEntryList().get(1), currentUserId, /*uid=*/ 2L, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 25.0, + /*foregroundUsageTimeInMs=*/ 15, /*backgroundUsageTimeInMs=*/ 15); + assertBatteryDiffEntry( + resultDiffData.getSystemDiffEntryList().get(0), currentUserId, /*uid=*/ 3L, + ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY, /*consumePercentage=*/ 25.0, + /*foregroundUsageTimeInMs=*/ 50, /*backgroundUsageTimeInMs=*/ 60); + } + + @Test + public void getBatteryUsageMap_multipleUsers_returnsExpectedResult() { + final long[] batteryHistoryKeys = new long[]{ + 1641052800000L, // 2022-01-02 00:00:00 + 1641056400000L, // 2022-01-02 01:00:00 + 1641060000000L // 2022-01-02 02:00:00 + }; + final Map> batteryHistoryMap = new HashMap<>(); + final int currentUserId = mContext.getUserId(); + // Adds the index = 0 data. + Map entryMap = new HashMap<>(); + BatteryHistEntry entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 5.0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 10.0, /*uid=*/ 2L, currentUserId + 1, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 5.0, /*uid=*/ 3L, currentUserId + 2, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 20L, + /*backgroundUsageTimeInMs=*/ 30L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[0], entryMap); + // Adds the index = 1 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 15.0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 20L, + /*backgroundUsageTimeInMs=*/ 30L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 30.0, /*uid=*/ 2L, currentUserId + 1, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 15.0, /*uid=*/ 3L, currentUserId + 2, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 30L, + /*backgroundUsageTimeInMs=*/ 30L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[1], entryMap); + // Adds the index = 2 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 25.0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 20L, + /*backgroundUsageTimeInMs=*/ 30L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 50.0, /*uid=*/ 2L, currentUserId + 1, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 20L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 25.0, /*uid=*/ 3L, currentUserId + 2, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 30L, + /*backgroundUsageTimeInMs=*/ 30L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[2], entryMap); + final List hourlyBatteryLevelsPerDay = + new ArrayList<>(); + List timestamps = List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]); + final List levels = List.of(100, 100); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels)); + + final Map> resultMap = + DataProcessor.getBatteryUsageMap( + mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap); + + final BatteryDiffData resultDiffData = + resultMap + .get(DataProcessor.SELECTED_INDEX_ALL) + .get(DataProcessor.SELECTED_INDEX_ALL); + assertBatteryDiffEntry( + resultDiffData.getAppDiffEntryList().get(0), currentUserId, /*uid=*/ 1L, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 25.0, + /*foregroundUsageTimeInMs=*/ 10, /*backgroundUsageTimeInMs=*/ 10); + assertBatteryDiffEntry( + resultDiffData.getSystemDiffEntryList().get(0), BatteryUtils.UID_OTHER_USERS, + /*uid=*/ BatteryUtils.UID_OTHER_USERS, ConvertUtils.CONSUMER_TYPE_UID_BATTERY, + /*consumePercentage=*/ 75.0, /*foregroundUsageTimeInMs=*/ 0, + /*backgroundUsageTimeInMs=*/ 0); + assertThat(resultMap.get(0).get(0)).isNotNull(); + assertThat(resultMap.get(0).get(DataProcessor.SELECTED_INDEX_ALL)).isNotNull(); + } + + @Test + public void getBatteryUsageMap_usageTimeExceed_returnsExpectedResult() { + final long[] batteryHistoryKeys = new long[]{ + 1641052800000L, // 2022-01-02 00:00:00 + 1641056400000L, // 2022-01-02 01:00:00 + 1641060000000L // 2022-01-02 02:00:00 + }; + final Map> batteryHistoryMap = new HashMap<>(); + final int currentUserId = mContext.getUserId(); + // Adds the index = 0 data. + Map entryMap = new HashMap<>(); + BatteryHistEntry entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[0], entryMap); + // Adds the index = 1 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[1], entryMap); + // Adds the index = 2 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 500.0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 3600000L, + /*backgroundUsageTimeInMs=*/ 7200000L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[2], entryMap); + final List hourlyBatteryLevelsPerDay = + new ArrayList<>(); + List timestamps = List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]); + final List levels = List.of(100, 100); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels)); + + final Map> resultMap = + DataProcessor.getBatteryUsageMap( + mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap); + + final BatteryDiffData resultDiffData = + resultMap + .get(DataProcessor.SELECTED_INDEX_ALL) + .get(DataProcessor.SELECTED_INDEX_ALL); + // Verifies the clipped usage time. + final float ratio = (float) (7200) / (float) (3600 + 7200); + final BatteryDiffEntry resultEntry = resultDiffData.getAppDiffEntryList().get(0); + assertThat(resultEntry.mForegroundUsageTimeInMs) + .isEqualTo(Math.round(entry.mForegroundUsageTimeInMs * ratio)); + assertThat(resultEntry.mBackgroundUsageTimeInMs) + .isEqualTo(Math.round(entry.mBackgroundUsageTimeInMs * ratio)); + assertThat(resultEntry.mConsumePower) + .isEqualTo(entry.mConsumePower * ratio); + assertThat(resultMap.get(0).get(0)).isNotNull(); + assertThat(resultMap.get(0).get(DataProcessor.SELECTED_INDEX_ALL)).isNotNull(); + } + + @Test + public void getBatteryUsageMap_hideApplicationEntries_returnsExpectedResult() { + final long[] batteryHistoryKeys = new long[]{ + 1641052800000L, // 2022-01-02 00:00:00 + 1641056400000L, // 2022-01-02 01:00:00 + 1641060000000L // 2022-01-02 02:00:00 + }; + final Map> batteryHistoryMap = new HashMap<>(); + final int currentUserId = mContext.getUserId(); + // Adds the index = 0 data. + Map entryMap = new HashMap<>(); + BatteryHistEntry entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[0], entryMap); + // Adds the index = 1 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[1], entryMap); + // Adds the index = 2 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 10.0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 10.0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[2], entryMap); + final List hourlyBatteryLevelsPerDay = + new ArrayList<>(); + List timestamps = List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]); + final List levels = List.of(100, 100); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels)); + when(mPowerUsageFeatureProvider.getHideApplicationEntries(mContext)) + .thenReturn(new CharSequence[]{"package1"}); + + final Map> resultMap = + DataProcessor.getBatteryUsageMap( + mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap); + + final BatteryDiffData resultDiffData = + resultMap + .get(DataProcessor.SELECTED_INDEX_ALL) + .get(DataProcessor.SELECTED_INDEX_ALL); + assertBatteryDiffEntry( + resultDiffData.getAppDiffEntryList().get(0), currentUserId, /*uid=*/ 2L, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*consumePercentage=*/ 50.0, + /*foregroundUsageTimeInMs=*/ 10, /*backgroundUsageTimeInMs=*/ 20); + } + + @Test + public void getBatteryUsageMap_hideBackgroundUsageTime_returnsExpectedResult() { + final long[] batteryHistoryKeys = new long[]{ + 1641052800000L, // 2022-01-02 00:00:00 + 1641056400000L, // 2022-01-02 01:00:00 + 1641060000000L // 2022-01-02 02:00:00 + }; + final Map> batteryHistoryMap = new HashMap<>(); + final int currentUserId = mContext.getUserId(); + // Adds the index = 0 data. + Map entryMap = new HashMap<>(); + BatteryHistEntry entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[0], entryMap); + // Adds the index = 1 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 0L, + /*backgroundUsageTimeInMs=*/ 0L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[1], entryMap); + // Adds the index = 2 data. + entryMap = new HashMap<>(); + entry = createBatteryHistEntry( + "package1", "label1", /*consumePower=*/ 10.0, /*uid=*/ 1L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + entry = createBatteryHistEntry( + "package2", "label2", /*consumePower=*/ 10.0, /*uid=*/ 2L, currentUserId, + ConvertUtils.CONSUMER_TYPE_UID_BATTERY, /*foregroundUsageTimeInMs=*/ 10L, + /*backgroundUsageTimeInMs=*/ 20L); + entryMap.put(entry.getKey(), entry); + batteryHistoryMap.put(batteryHistoryKeys[2], entryMap); + final List hourlyBatteryLevelsPerDay = + new ArrayList<>(); + List timestamps = List.of(batteryHistoryKeys[0], batteryHistoryKeys[2]); + final List levels = List.of(100, 100); + hourlyBatteryLevelsPerDay.add( + new BatteryLevelData.PeriodBatteryLevelData(timestamps, levels)); + when(mPowerUsageFeatureProvider.getHideBackgroundUsageTimeSet(mContext)) + .thenReturn(new HashSet(Arrays.asList((CharSequence) "package2"))); + + final Map> resultMap = + DataProcessor.getBatteryUsageMap( + mContext, hourlyBatteryLevelsPerDay, batteryHistoryMap); + + final BatteryDiffData resultDiffData = + resultMap + .get(DataProcessor.SELECTED_INDEX_ALL) + .get(DataProcessor.SELECTED_INDEX_ALL); + BatteryDiffEntry resultEntry = resultDiffData.getAppDiffEntryList().get(0); + assertThat(resultEntry.mBackgroundUsageTimeInMs).isEqualTo(20); + resultEntry = resultDiffData.getAppDiffEntryList().get(1); + assertThat(resultEntry.mBackgroundUsageTimeInMs).isEqualTo(0); + } + + private static Map> createHistoryMap( + final long[] timestamps, final int[] levels) { + final Map> batteryHistoryMap = new HashMap<>(); + for (int index = 0; index < timestamps.length; index++) { + final Map entryMap = new HashMap<>(); + final ContentValues values = new ContentValues(); + values.put(BatteryHistEntry.KEY_BATTERY_LEVEL, levels[index]); + final BatteryHistEntry entry = new BatteryHistEntry(values); + entryMap.put(FAKE_ENTRY_KEY, entry); + batteryHistoryMap.put(timestamps[index], entryMap); + } + return batteryHistoryMap; + } + + private static BatteryHistEntry createBatteryHistEntry( + final String packageName, final String appLabel, final double consumePower, + final long uid, final long userId, final int consumerType, + final long foregroundUsageTimeInMs, final long backgroundUsageTimeInMs) { + // Only insert required fields. + final ContentValues values = new ContentValues(); + values.put(BatteryHistEntry.KEY_PACKAGE_NAME, packageName); + values.put(BatteryHistEntry.KEY_APP_LABEL, appLabel); + values.put(BatteryHistEntry.KEY_UID, uid); + values.put(BatteryHistEntry.KEY_USER_ID, userId); + values.put(BatteryHistEntry.KEY_CONSUMER_TYPE, consumerType); + values.put(BatteryHistEntry.KEY_CONSUME_POWER, consumePower); + values.put(BatteryHistEntry.KEY_FOREGROUND_USAGE_TIME, foregroundUsageTimeInMs); + values.put(BatteryHistEntry.KEY_BACKGROUND_USAGE_TIME, backgroundUsageTimeInMs); + return new BatteryHistEntry(values); + } + + private static void verifyExpectedBatteryLevelData( + final BatteryLevelData resultData, + final List expectedDailyTimestamps, + final List expectedDailyLevels, + final List> expectedHourlyTimestamps, + final List> expectedHourlyLevels) { + final BatteryLevelData.PeriodBatteryLevelData dailyResultData = + resultData.getDailyBatteryLevels(); + final List hourlyResultData = + resultData.getHourlyBatteryLevelsPerDay(); + verifyExpectedDailyBatteryLevelData( + dailyResultData, expectedDailyTimestamps, expectedDailyLevels); + verifyExpectedHourlyBatteryLevelData( + hourlyResultData, expectedHourlyTimestamps, expectedHourlyLevels); + } + + private static void verifyExpectedDailyBatteryLevelData( + final BatteryLevelData.PeriodBatteryLevelData dailyResultData, + final List expectedDailyTimestamps, + final List expectedDailyLevels) { + assertThat(dailyResultData.getTimestamps()).isEqualTo(expectedDailyTimestamps); + assertThat(dailyResultData.getLevels()).isEqualTo(expectedDailyLevels); + } + + private static void verifyExpectedHourlyBatteryLevelData( + final List hourlyResultData, + final List> expectedHourlyTimestamps, + final List> expectedHourlyLevels) { + final int expectedHourlySize = expectedHourlyTimestamps.size(); + assertThat(hourlyResultData).hasSize(expectedHourlySize); + for (int dailyIndex = 0; dailyIndex < expectedHourlySize; dailyIndex++) { + assertThat(hourlyResultData.get(dailyIndex).getTimestamps()) + .isEqualTo(expectedHourlyTimestamps.get(dailyIndex)); + assertThat(hourlyResultData.get(dailyIndex).getLevels()) + .isEqualTo(expectedHourlyLevels.get(dailyIndex)); + } + } + + private static void verifyExpectedTimestampSlots( + final Calendar start, + final Calendar end, + final Calendar expectedStart, + final Calendar expectedEnd) { + expectedStart.set(Calendar.MILLISECOND, 0); + expectedEnd.set(Calendar.MILLISECOND, 0); + final ArrayList timestampSlots = new ArrayList<>(); + timestampSlots.add(start.getTimeInMillis()); + timestampSlots.add(end.getTimeInMillis()); + final List resultList = + DataProcessor.getTimestampSlots(timestampSlots); + + for (int index = 0; index < resultList.size(); index++) { + final long expectedTimestamp = + expectedStart.getTimeInMillis() + index * DateUtils.HOUR_IN_MILLIS; + assertThat(resultList.get(index)).isEqualTo(expectedTimestamp); + } + assertThat(resultList.get(resultList.size() - 1)) + .isEqualTo(expectedEnd.getTimeInMillis()); + } + + private static void assertBatteryDiffEntry( + final BatteryDiffEntry entry, final long userId, final long uid, + final int consumerType, final double consumePercentage, + final long foregroundUsageTimeInMs, final long backgroundUsageTimeInMs) { + assertThat(entry.mBatteryHistEntry.mUserId).isEqualTo(userId); + assertThat(entry.mBatteryHistEntry.mUid).isEqualTo(uid); + assertThat(entry.mBatteryHistEntry.mConsumerType).isEqualTo(consumerType); + assertThat(entry.getPercentOfTotal()).isEqualTo(consumePercentage); + assertThat(entry.mForegroundUsageTimeInMs).isEqualTo(foregroundUsageTimeInMs); + assertThat(entry.mBackgroundUsageTimeInMs).isEqualTo(backgroundUsageTimeInMs); + } +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummaryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummaryTest.java index 81b574aa7d6..c0494972f35 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummaryTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batteryusage/PowerUsageSummaryTest.java @@ -139,17 +139,7 @@ public class PowerUsageSummaryTest { } @Test - public void initPreference_chartGraphEnabled_hasCorrectSummary() { - mFragment.initPreference(); - - verify(mBatteryUsagePreference).setSummary("View usage for past 24 hours"); - } - - @Test - public void initPreference_chartGraphDisabled_hasCorrectSummary() { - when(mFeatureFactory.powerUsageFeatureProvider.isChartGraphEnabled(mRealContext)) - .thenReturn(false); - + public void initPreference_hasCorrectSummary() { mFragment.initPreference(); verify(mBatteryUsagePreference).setSummary("View usage from last full charge");