From 1240549591b9319022ec36f6b51b7c885482d88a Mon Sep 17 00:00:00 2001 From: jackqdyulei Date: Tue, 5 Dec 2017 13:03:29 -0800 Subject: [PATCH] Move AppListGroup to PreferenceController Move the app list in battery settings to PreferenceController. So that we can: 1. Clean the code in PowerUsageSummary 2. Make it easy to add/move the app list to other place in furture. This cl: 1. Move and make it invisible since in P we don't show app list in battery main page. 2. Move related test to BatteryAppListPreferenceControllerTest Bug: 70234293 Test: RunSettingsRoboTests Change-Id: Ice7a42394916ff5e71305bfe22f5c35868d87fc7 --- res/color/battery_icon_color_error.xml | 2 +- .../BatteryAppListPreferenceController.java | 482 +++++++++++++++++++++ .../settings/fuelgauge/PowerUsageSummary.java | 369 +--------------- .../BatteryAppListPreferenceControllerTest.java | 204 +++++++++ .../settings/fuelgauge/PowerUsageSummaryTest.java | 422 ++++++++++++++++++ 5 files changed, 1125 insertions(+), 354 deletions(-) create mode 100644 src/com/android/settings/fuelgauge/BatteryAppListPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/fuelgauge/BatteryAppListPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java diff --git a/res/color/battery_icon_color_error.xml b/res/color/battery_icon_color_error.xml index 3a71aaef89..99c7d7d9da 100644 --- a/res/color/battery_icon_color_error.xml +++ b/res/color/battery_icon_color_error.xml @@ -14,6 +14,6 @@ limitations under the License. --> - \ No newline at end of file diff --git a/src/com/android/settings/fuelgauge/BatteryAppListPreferenceController.java b/src/com/android/settings/fuelgauge/BatteryAppListPreferenceController.java new file mode 100644 index 0000000000..ee0ed212f9 --- /dev/null +++ b/src/com/android/settings/fuelgauge/BatteryAppListPreferenceController.java @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2017 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; + +import android.app.Activity; +import android.app.Fragment; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.BatteryStats; +import android.os.Handler; +import android.os.Message; +import android.os.Process; +import android.os.UserHandle; +import android.os.UserManager; +import android.support.annotation.VisibleForTesting; +import android.support.v14.preference.PreferenceFragment; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceGroup; +import android.support.v7.preference.PreferenceManager; +import android.support.v7.preference.PreferenceScreen; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.util.ArrayMap; +import android.util.Log; +import android.util.SparseArray; + +import com.android.internal.os.BatterySipper; +import com.android.internal.os.BatterySipper.DrainType; +import com.android.internal.os.BatteryStatsHelper; +import com.android.internal.os.PowerProfile; +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.Utils; +import com.android.settings.core.instrumentation.MetricsFeatureProvider; +import com.android.settings.fuelgauge.anomaly.Anomaly; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnDestroy; +import com.android.settingslib.core.lifecycle.events.OnPause; + +import java.util.ArrayList; +import java.util.List; + +/** + * Controller that update the battery header view + */ +public class BatteryAppListPreferenceController extends AbstractPreferenceController + implements PreferenceControllerMixin, LifecycleObserver, OnPause, OnDestroy { + private static final boolean USE_FAKE_DATA = true; + private static final int MAX_ITEMS_TO_LIST = USE_FAKE_DATA ? 30 : 10; + private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10; + private static final int STATS_TYPE = BatteryStats.STATS_SINCE_CHARGED; + + private final String mPreferenceKey; + @VisibleForTesting + PreferenceGroup mAppListGroup; + private BatteryStatsHelper mBatteryStatsHelper; + private ArrayMap mPreferenceCache; + @VisibleForTesting + BatteryUtils mBatteryUtils; + private UserManager mUserManager; + private SettingsActivity mActivity; + private PreferenceFragment mFragment; + private Context mPrefContext; + SparseArray> mAnomalySparseArray; + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case BatteryEntry.MSG_UPDATE_NAME_ICON: + BatteryEntry entry = (BatteryEntry) msg.obj; + PowerGaugePreference pgp = + (PowerGaugePreference) mAppListGroup.findPreference( + Integer.toString(entry.sipper.uidObj.getUid())); + if (pgp != null) { + final int userId = UserHandle.getUserId(entry.sipper.getUid()); + final UserHandle userHandle = new UserHandle(userId); + pgp.setIcon(mUserManager.getBadgedIconForUser(entry.getIcon(), userHandle)); + pgp.setTitle(entry.name); + if (entry.sipper.drainType == DrainType.APP) { + pgp.setContentDescription(entry.name); + } + } + break; + case BatteryEntry.MSG_REPORT_FULLY_DRAWN: + Activity activity = mActivity; + if (activity != null) { + activity.reportFullyDrawn(); + } + break; + } + super.handleMessage(msg); + } + }; + + public BatteryAppListPreferenceController(Context context, String preferenceKey, + Lifecycle lifecycle, SettingsActivity activity, PreferenceFragment fragment) { + super(context); + + if (lifecycle != null) { + lifecycle.addObserver(this); + } + + mPreferenceKey = preferenceKey; + mBatteryUtils = BatteryUtils.getInstance(context); + mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE); + mActivity = activity; + mFragment = fragment; + } + + @Override + public void onPause() { + BatteryEntry.stopRequestQueue(); + mHandler.removeMessages(BatteryEntry.MSG_UPDATE_NAME_ICON); + } + + @Override + public void onDestroy() { + if (mActivity.isChangingConfigurations()) { + BatteryEntry.clearUidCache(); + } + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPrefContext = screen.getContext(); + mAppListGroup = (PreferenceGroup) screen.findPreference(mPreferenceKey); + } + + @Override + public boolean isAvailable() { + return false; + } + + @Override + public String getPreferenceKey() { + return mPreferenceKey; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (preference instanceof PowerGaugePreference) { + PowerGaugePreference pgp = (PowerGaugePreference) preference; + BatteryEntry entry = pgp.getInfo(); + AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity, + mFragment, mBatteryStatsHelper, STATS_TYPE, entry, pgp.getPercent(), + mAnomalySparseArray != null ? mAnomalySparseArray.get(entry.sipper.getUid()) + : null); + return true; + } + return false; + } + + public void refreshAnomalyIcon(final SparseArray> anomalySparseArray) { + if (!isAvailable()) { + return; + } + mAnomalySparseArray = anomalySparseArray; + for (int i = 0, size = anomalySparseArray.size(); i < size; i++) { + final String key = extractKeyFromUid(anomalySparseArray.keyAt(i)); + final PowerGaugePreference pref = (PowerGaugePreference) mAppListGroup.findPreference( + key); + if (pref != null) { + pref.shouldShowAnomalyIcon(true); + } + } + } + + public void refreshAppListGroup(BatteryStatsHelper statsHelper, boolean showAllApps, + CharSequence timeSequence) { + if (!isAvailable()) { + return; + } + mBatteryStatsHelper = statsHelper; + final int resId = showAllApps ? R.string.power_usage_list_summary_device + : R.string.power_usage_list_summary; + mAppListGroup.setTitle(TextUtils.expandTemplate(mContext.getText(resId), timeSequence)); + + final PowerProfile powerProfile = statsHelper.getPowerProfile(); + final BatteryStats stats = statsHelper.getStats(); + final double averagePower = powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL); + boolean addedSome = false; + final int dischargeAmount = USE_FAKE_DATA ? 5000 + : stats != null ? stats.getDischargeAmount(STATS_TYPE) : 0; + + cacheRemoveAllPrefs(mAppListGroup); + mAppListGroup.setOrderingAsAdded(false); + + if (averagePower >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP || USE_FAKE_DATA) { + final List usageList = getCoalescedUsageList( + USE_FAKE_DATA ? getFakeStats() : statsHelper.getUsageList()); + double hiddenPowerMah = showAllApps ? 0 : + mBatteryUtils.removeHiddenBatterySippers(usageList); + mBatteryUtils.sortUsageList(usageList); + + final int numSippers = usageList.size(); + for (int i = 0; i < numSippers; i++) { + final BatterySipper sipper = usageList.get(i); + double totalPower = USE_FAKE_DATA ? 4000 : statsHelper.getTotalPower(); + + final double percentOfTotal = mBatteryUtils.calculateBatteryPercent( + sipper.totalPowerMah, totalPower, hiddenPowerMah, dischargeAmount); + + if (((int) (percentOfTotal + .5)) < 1) { + continue; + } + if (shouldHideSipper(sipper)) { + continue; + } + final UserHandle userHandle = new UserHandle(UserHandle.getUserId(sipper.getUid())); + final BatteryEntry entry = new BatteryEntry(mActivity, mHandler, mUserManager, + sipper); + final Drawable badgedIcon = mUserManager.getBadgedIconForUser(entry.getIcon(), + userHandle); + final CharSequence contentDescription = mUserManager.getBadgedLabelForUser( + entry.getLabel(), + userHandle); + + final String key = extractKeyFromSipper(sipper); + PowerGaugePreference pref = (PowerGaugePreference) getCachedPreference(key); + if (pref == null) { + pref = new PowerGaugePreference(mPrefContext, badgedIcon, + contentDescription, entry); + pref.setKey(key); + } + sipper.percent = percentOfTotal; + pref.setTitle(entry.getLabel()); + pref.setOrder(i + 1); + pref.setPercent(percentOfTotal); + pref.shouldShowAnomalyIcon(false); + if (sipper.usageTimeMs == 0 && sipper.drainType == DrainType.APP) { + sipper.usageTimeMs = mBatteryUtils.getProcessTimeMs( + BatteryUtils.StatusType.FOREGROUND, sipper.uidObj, STATS_TYPE); + } + setUsageSummary(pref, sipper); + addedSome = true; + mAppListGroup.addPreference(pref); + if (mAppListGroup.getPreferenceCount() - getCachedCount() + > (MAX_ITEMS_TO_LIST + 1)) { + break; + } + } + } + if (!addedSome) { + addNotAvailableMessage(); + } + removeCachedPrefs(mAppListGroup); + + BatteryEntry.startRequestQueue(); + } + + /** + * We want to coalesce some UIDs. For example, dex2oat runs under a shared gid that + * exists for all users of the same app. We detect this case and merge the power use + * for dex2oat to the device OWNER's use of the app. + * + * @return A sorted list of apps using power. + */ + private List getCoalescedUsageList(final List sippers) { + final SparseArray uidList = new SparseArray<>(); + + final ArrayList results = new ArrayList<>(); + final int numSippers = sippers.size(); + for (int i = 0; i < numSippers; i++) { + BatterySipper sipper = sippers.get(i); + if (sipper.getUid() > 0) { + int realUid = sipper.getUid(); + + // Check if this UID is a shared GID. If so, we combine it with the OWNER's + // actual app UID. + if (isSharedGid(sipper.getUid())) { + realUid = UserHandle.getUid(UserHandle.USER_SYSTEM, + UserHandle.getAppIdFromSharedAppGid(sipper.getUid())); + } + + // Check if this UID is a system UID (mediaserver, logd, nfc, drm, etc). + if (isSystemUid(realUid) + && !"mediaserver".equals(sipper.packageWithHighestDrain)) { + // Use the system UID for all UIDs running in their own sandbox that + // are not apps. We exclude mediaserver because we already are expected to + // report that as a separate item. + realUid = Process.SYSTEM_UID; + } + + if (realUid != sipper.getUid()) { + // Replace the BatterySipper with a new one with the real UID set. + BatterySipper newSipper = new BatterySipper(sipper.drainType, + new FakeUid(realUid), 0.0); + newSipper.add(sipper); + newSipper.packageWithHighestDrain = sipper.packageWithHighestDrain; + newSipper.mPackages = sipper.mPackages; + sipper = newSipper; + } + + int index = uidList.indexOfKey(realUid); + if (index < 0) { + // New entry. + uidList.put(realUid, sipper); + } else { + // Combine BatterySippers if we already have one with this UID. + final BatterySipper existingSipper = uidList.valueAt(index); + existingSipper.add(sipper); + if (existingSipper.packageWithHighestDrain == null + && sipper.packageWithHighestDrain != null) { + existingSipper.packageWithHighestDrain = sipper.packageWithHighestDrain; + } + + final int existingPackageLen = existingSipper.mPackages != null ? + existingSipper.mPackages.length : 0; + final int newPackageLen = sipper.mPackages != null ? + sipper.mPackages.length : 0; + if (newPackageLen > 0) { + String[] newPackages = new String[existingPackageLen + newPackageLen]; + if (existingPackageLen > 0) { + System.arraycopy(existingSipper.mPackages, 0, newPackages, 0, + existingPackageLen); + } + System.arraycopy(sipper.mPackages, 0, newPackages, existingPackageLen, + newPackageLen); + existingSipper.mPackages = newPackages; + } + } + } else { + results.add(sipper); + } + } + + final int numUidSippers = uidList.size(); + for (int i = 0; i < numUidSippers; i++) { + results.add(uidList.valueAt(i)); + } + + // The sort order must have changed, so re-sort based on total power use. + mBatteryUtils.sortUsageList(results); + return results; + } + + @VisibleForTesting + void setUsageSummary(Preference preference, BatterySipper sipper) { + // Only show summary when usage time is longer than one minute + final long usageTimeMs = sipper.usageTimeMs; + if (usageTimeMs >= DateUtils.MINUTE_IN_MILLIS) { + final CharSequence timeSequence = Utils.formatElapsedTime(mContext, usageTimeMs, + false); + preference.setSummary( + (sipper.drainType != DrainType.APP || mBatteryUtils.shouldHideSipper(sipper)) + ? timeSequence + : TextUtils.expandTemplate(mContext.getText(R.string.battery_used_for), + timeSequence)); + } + } + + @VisibleForTesting + boolean shouldHideSipper(BatterySipper sipper) { + // Don't show over-counted and unaccounted in any condition + return sipper.drainType == BatterySipper.DrainType.OVERCOUNTED + || sipper.drainType == BatterySipper.DrainType.UNACCOUNTED; + } + + @VisibleForTesting + String extractKeyFromSipper(BatterySipper sipper) { + if (sipper.uidObj != null) { + return extractKeyFromUid(sipper.getUid()); + } else if (sipper.drainType == DrainType.USER) { + return sipper.drainType.toString() + sipper.userId; + } else if (sipper.drainType != DrainType.APP) { + return sipper.drainType.toString(); + } else if (sipper.getPackages() != null) { + return TextUtils.concat(sipper.getPackages()).toString(); + } else { + Log.w(TAG, "Inappropriate BatterySipper without uid and package names: " + sipper); + return "-1"; + } + } + + @VisibleForTesting + String extractKeyFromUid(int uid) { + return Integer.toString(uid); + } + + private void cacheRemoveAllPrefs(PreferenceGroup group) { + mPreferenceCache = new ArrayMap<>(); + final int N = group.getPreferenceCount(); + for (int i = 0; i < N; i++) { + Preference p = group.getPreference(i); + if (TextUtils.isEmpty(p.getKey())) { + continue; + } + mPreferenceCache.put(p.getKey(), p); + } + } + + private static boolean isSharedGid(int uid) { + return UserHandle.getAppIdFromSharedAppGid(uid) > 0; + } + + private static boolean isSystemUid(int uid) { + final int appUid = UserHandle.getAppId(uid); + return appUid >= Process.SYSTEM_UID && appUid < Process.FIRST_APPLICATION_UID; + } + + private static List getFakeStats() { + ArrayList stats = new ArrayList<>(); + float use = 5; + for (DrainType type : DrainType.values()) { + if (type == DrainType.APP) { + continue; + } + stats.add(new BatterySipper(type, null, use)); + use += 5; + } + for (int i = 0; i < 100; i++) { + stats.add(new BatterySipper(DrainType.APP, + new FakeUid(Process.FIRST_APPLICATION_UID + i), use)); + } + stats.add(new BatterySipper(DrainType.APP, + new FakeUid(0), use)); + + // Simulate dex2oat process. + BatterySipper sipper = new BatterySipper(DrainType.APP, + new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID)), 10.0f); + sipper.packageWithHighestDrain = "dex2oat"; + stats.add(sipper); + + sipper = new BatterySipper(DrainType.APP, + new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID + 1)), 10.0f); + sipper.packageWithHighestDrain = "dex2oat"; + stats.add(sipper); + + sipper = new BatterySipper(DrainType.APP, + new FakeUid(UserHandle.getSharedAppGid(Process.LOG_UID)), 9.0f); + stats.add(sipper); + + return stats; + } + + private Preference getCachedPreference(String key) { + return mPreferenceCache != null ? mPreferenceCache.remove(key) : null; + } + + private void removeCachedPrefs(PreferenceGroup group) { + for (Preference p : mPreferenceCache.values()) { + group.removePreference(p); + } + mPreferenceCache = null; + } + + private int getCachedCount() { + return mPreferenceCache != null ? mPreferenceCache.size() : 0; + } + + private void addNotAvailableMessage() { + final String NOT_AVAILABLE = "not_available"; + Preference notAvailable = getCachedPreference(NOT_AVAILABLE); + if (notAvailable == null) { + notAvailable = new Preference(mPrefContext); + notAvailable.setKey(NOT_AVAILABLE); + notAvailable.setTitle(R.string.power_usage_not_available); + mAppListGroup.addPreference(notAvailable); + } + } +} diff --git a/src/com/android/settings/fuelgauge/PowerUsageSummary.java b/src/com/android/settings/fuelgauge/PowerUsageSummary.java index ed5b6f40cf..e4b70a1437 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageSummary.java +++ b/src/com/android/settings/fuelgauge/PowerUsageSummary.java @@ -21,21 +21,13 @@ import android.app.LoaderManager; import android.app.LoaderManager.LoaderCallbacks; import android.content.Context; import android.content.Loader; -import android.graphics.drawable.Drawable; import android.os.BatteryStats; import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.os.Process; -import android.os.UserHandle; import android.provider.SearchIndexableResource; import android.support.annotation.VisibleForTesting; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceGroup; -import android.text.TextUtils; -import android.text.format.DateUtils; import android.text.format.Formatter; -import android.util.Log; import android.util.SparseArray; import android.view.Menu; import android.view.MenuInflater; @@ -49,7 +41,6 @@ import com.android.internal.hardware.AmbientDisplayConfiguration; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.os.BatterySipper; import com.android.internal.os.BatterySipper.DrainType; -import com.android.internal.os.PowerProfile; import com.android.settings.R; import com.android.settings.Settings.HighPowerApplicationsActivity; import com.android.settings.SettingsActivity; @@ -71,6 +62,7 @@ import com.android.settings.fuelgauge.anomaly.AnomalyUtils; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; import java.util.ArrayList; import java.util.Arrays; @@ -86,12 +78,9 @@ public class PowerUsageSummary extends PowerUsageBase implements static final String TAG = "PowerUsageSummary"; private static final boolean DEBUG = false; - private static final boolean USE_FAKE_DATA = false; private static final String KEY_APP_LIST = "app_list"; private static final String KEY_BATTERY_HEADER = "battery_header"; private static final String KEY_SHOW_ALL_APPS = "show_all_apps"; - private static final int MAX_ITEMS_TO_LIST = USE_FAKE_DATA ? 30 : 10; - private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10; private static final String KEY_SCREEN_USAGE = "screen_usage"; private static final String KEY_TIME_SINCE_LAST_FULL_CHARGE = "last_full_charge"; @@ -136,6 +125,7 @@ public class PowerUsageSummary extends PowerUsageBase implements PreferenceGroup mAppListGroup; @VisibleForTesting BatteryHeaderPreferenceController mBatteryHeaderPreferenceController; + private BatteryAppListPreferenceController mBatteryAppListPreferenceController; private AnomalySummaryPreferenceController mAnomalySummaryPreferenceController; private int mStatsType = BatteryStats.STATS_SINCE_CHARGED; @@ -157,7 +147,7 @@ public class PowerUsageSummary extends PowerUsageBase implements mAnomalySummaryPreferenceController.updateAnomalySummaryPreference(data); updateAnomalySparseArray(data); - refreshAnomalyIcon(); + mBatteryAppListPreferenceController.refreshAnomalyIcon(mAnomalySparseArray); } @Override @@ -235,7 +225,6 @@ public class PowerUsageSummary extends PowerUsageBase implements initFeatureProvider(); mBatteryLayoutPref = (LayoutPreference) findPreference(KEY_BATTERY_HEADER); - mAppListGroup = (PreferenceGroup) findPreference(KEY_APP_LIST); mScreenUsagePref = (PowerGaugePreference) findPreference(KEY_SCREEN_USAGE); mLastFullChargePref = (PowerGaugePreference) findPreference( KEY_TIME_SINCE_LAST_FULL_CHARGE); @@ -255,21 +244,6 @@ public class PowerUsageSummary extends PowerUsageBase implements } @Override - public void onPause() { - BatteryEntry.stopRequestQueue(); - mHandler.removeMessages(BatteryEntry.MSG_UPDATE_NAME_ICON); - super.onPause(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (getActivity().isChangingConfigurations()) { - BatteryEntry.clearUidCache(); - } - } - - @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(KEY_SHOW_ALL_APPS, mShowAllApps); @@ -283,14 +257,7 @@ public class PowerUsageSummary extends PowerUsageBase implements if (KEY_BATTERY_HEADER.equals(preference.getKey())) { performBatteryHeaderClick(); return true; - } else if (!(preference instanceof PowerGaugePreference)) { - return super.onPreferenceTreeClick(preference); } - PowerGaugePreference pgp = (PowerGaugePreference) preference; - BatteryEntry entry = pgp.getInfo(); - AdvancedPowerUsageDetail.startBatteryDetailPage((SettingsActivity) getActivity(), - this, mStatsHelper, mStatsType, entry, pgp.getPercent(), - mAnomalySparseArray.get(entry.sipper.getUid())); return super.onPreferenceTreeClick(preference); } @@ -306,10 +273,15 @@ public class PowerUsageSummary extends PowerUsageBase implements @Override protected List getPreferenceControllers(Context context) { + final Lifecycle lifecycle = getLifecycle(); + final SettingsActivity activity = (SettingsActivity) getActivity(); final List controllers = new ArrayList<>(); mBatteryHeaderPreferenceController = new BatteryHeaderPreferenceController( - context, getActivity(), this /* host */, getLifecycle()); + context, activity, this /* host */, getLifecycle()); controllers.add(mBatteryHeaderPreferenceController); + mBatteryAppListPreferenceController = new BatteryAppListPreferenceController(context, + KEY_APP_LIST, lifecycle, activity, this); + controllers.add(mBatteryAppListPreferenceController); controllers.add(new AutoBrightnessPreferenceController(context, KEY_AUTO_BRIGHTNESS)); controllers.add(new TimeoutPreferenceController(context, KEY_SCREEN_TIMEOUT)); controllers.add(new BatterySaverController(context, getLifecycle())); @@ -388,17 +360,6 @@ public class PowerUsageSummary extends PowerUsageBase implements } } - private void addNotAvailableMessage() { - final String NOT_AVAILABLE = "not_available"; - Preference notAvailable = getCachedPreference(NOT_AVAILABLE); - if (notAvailable == null) { - notAvailable = new Preference(getPrefContext()); - notAvailable.setKey(NOT_AVAILABLE); - notAvailable.setTitle(R.string.power_usage_not_available); - mAppListGroup.addPreference(notAvailable); - } - } - private void performBatteryHeaderClick() { if (mPowerFeatureProvider.isAdvancedUiEnabled()) { Utils.startWithFragment(getContext(), PowerUsageAdvanced.class.getName(), null, @@ -415,101 +376,6 @@ public class PowerUsageSummary extends PowerUsageBase implements } } - private static boolean isSharedGid(int uid) { - return UserHandle.getAppIdFromSharedAppGid(uid) > 0; - } - - private static boolean isSystemUid(int uid) { - final int appUid = UserHandle.getAppId(uid); - return appUid >= Process.SYSTEM_UID && appUid < Process.FIRST_APPLICATION_UID; - } - - /** - * We want to coalesce some UIDs. For example, dex2oat runs under a shared gid that - * exists for all users of the same app. We detect this case and merge the power use - * for dex2oat to the device OWNER's use of the app. - * - * @return A sorted list of apps using power. - */ - private List getCoalescedUsageList(final List sippers) { - final SparseArray uidList = new SparseArray<>(); - - final ArrayList results = new ArrayList<>(); - final int numSippers = sippers.size(); - for (int i = 0; i < numSippers; i++) { - BatterySipper sipper = sippers.get(i); - if (sipper.getUid() > 0) { - int realUid = sipper.getUid(); - - // Check if this UID is a shared GID. If so, we combine it with the OWNER's - // actual app UID. - if (isSharedGid(sipper.getUid())) { - realUid = UserHandle.getUid(UserHandle.USER_SYSTEM, - UserHandle.getAppIdFromSharedAppGid(sipper.getUid())); - } - - // Check if this UID is a system UID (mediaserver, logd, nfc, drm, etc). - if (isSystemUid(realUid) - && !"mediaserver".equals(sipper.packageWithHighestDrain)) { - // Use the system UID for all UIDs running in their own sandbox that - // are not apps. We exclude mediaserver because we already are expected to - // report that as a separate item. - realUid = Process.SYSTEM_UID; - } - - if (realUid != sipper.getUid()) { - // Replace the BatterySipper with a new one with the real UID set. - BatterySipper newSipper = new BatterySipper(sipper.drainType, - new FakeUid(realUid), 0.0); - newSipper.add(sipper); - newSipper.packageWithHighestDrain = sipper.packageWithHighestDrain; - newSipper.mPackages = sipper.mPackages; - sipper = newSipper; - } - - int index = uidList.indexOfKey(realUid); - if (index < 0) { - // New entry. - uidList.put(realUid, sipper); - } else { - // Combine BatterySippers if we already have one with this UID. - final BatterySipper existingSipper = uidList.valueAt(index); - existingSipper.add(sipper); - if (existingSipper.packageWithHighestDrain == null - && sipper.packageWithHighestDrain != null) { - existingSipper.packageWithHighestDrain = sipper.packageWithHighestDrain; - } - - final int existingPackageLen = existingSipper.mPackages != null ? - existingSipper.mPackages.length : 0; - final int newPackageLen = sipper.mPackages != null ? - sipper.mPackages.length : 0; - if (newPackageLen > 0) { - String[] newPackages = new String[existingPackageLen + newPackageLen]; - if (existingPackageLen > 0) { - System.arraycopy(existingSipper.mPackages, 0, newPackages, 0, - existingPackageLen); - } - System.arraycopy(sipper.mPackages, 0, newPackages, existingPackageLen, - newPackageLen); - existingSipper.mPackages = newPackages; - } - } - } else { - results.add(sipper); - } - } - - final int numUidSippers = uidList.size(); - for (int i = 0; i < numUidSippers; i++) { - results.add(uidList.valueAt(i)); - } - - // The sort order must have changed, so re-sort based on total power use. - mBatteryUtils.sortUsageList(results); - return results; - } - protected void refreshUi() { final Context context = getContext(); if (context == null) { @@ -527,102 +393,8 @@ public class PowerUsageSummary extends PowerUsageBase implements final CharSequence timeSequence = Utils.formatRelativeTime(context, lastFullChargeTime, false); - final int resId = mShowAllApps ? R.string.power_usage_list_summary_device - : R.string.power_usage_list_summary; - mAppListGroup.setTitle(TextUtils.expandTemplate(getText(resId), timeSequence)); - - refreshAppListGroup(); - } - - private void refreshAppListGroup() { - final PowerProfile powerProfile = mStatsHelper.getPowerProfile(); - final BatteryStats stats = mStatsHelper.getStats(); - final double averagePower = powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL); - boolean addedSome = false; - final int dischargeAmount = USE_FAKE_DATA ? 5000 - : stats != null ? stats.getDischargeAmount(mStatsType) : 0; - - cacheRemoveAllPrefs(mAppListGroup); - mAppListGroup.setOrderingAsAdded(false); - - if (averagePower >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP || USE_FAKE_DATA) { - final List usageList = getCoalescedUsageList( - USE_FAKE_DATA ? getFakeStats() : mStatsHelper.getUsageList()); - double hiddenPowerMah = mShowAllApps ? 0 : - mBatteryUtils.removeHiddenBatterySippers(usageList); - mBatteryUtils.sortUsageList(usageList); - - final int numSippers = usageList.size(); - for (int i = 0; i < numSippers; i++) { - final BatterySipper sipper = usageList.get(i); - double totalPower = USE_FAKE_DATA ? 4000 : mStatsHelper.getTotalPower(); - - final double percentOfTotal = mBatteryUtils.calculateBatteryPercent( - sipper.totalPowerMah, totalPower, hiddenPowerMah, dischargeAmount); - - if (((int) (percentOfTotal + .5)) < 1) { - continue; - } - if (shouldHideSipper(sipper)) { - continue; - } - final UserHandle userHandle = new UserHandle(UserHandle.getUserId(sipper.getUid())); - final BatteryEntry entry = new BatteryEntry(getActivity(), mHandler, mUm, sipper); - final Drawable badgedIcon = mUm.getBadgedIconForUser(entry.getIcon(), - userHandle); - final CharSequence contentDescription = mUm.getBadgedLabelForUser(entry.getLabel(), - userHandle); - - final String key = extractKeyFromSipper(sipper); - PowerGaugePreference pref = (PowerGaugePreference) getCachedPreference(key); - if (pref == null) { - pref = new PowerGaugePreference(getPrefContext(), badgedIcon, - contentDescription, entry); - pref.setKey(key); - } - sipper.percent = percentOfTotal; - pref.setTitle(entry.getLabel()); - pref.setOrder(i + 1); - pref.setPercent(percentOfTotal); - pref.shouldShowAnomalyIcon(false); - if (sipper.usageTimeMs == 0 && sipper.drainType == DrainType.APP) { - sipper.usageTimeMs = mBatteryUtils.getProcessTimeMs( - BatteryUtils.StatusType.FOREGROUND, sipper.uidObj, mStatsType); - } - setUsageSummary(pref, sipper); - addedSome = true; - mAppListGroup.addPreference(pref); - if (mAppListGroup.getPreferenceCount() - getCachedCount() - > (MAX_ITEMS_TO_LIST + 1)) { - break; - } - } - } - if (!addedSome) { - addNotAvailableMessage(); - } - removeCachedPrefs(mAppListGroup); - - BatteryEntry.startRequestQueue(); - } - - @VisibleForTesting - boolean shouldHideSipper(BatterySipper sipper) { - // Don't show over-counted and unaccounted in any condition - return sipper.drainType == BatterySipper.DrainType.OVERCOUNTED - || sipper.drainType == BatterySipper.DrainType.UNACCOUNTED; - } - - @VisibleForTesting - void refreshAnomalyIcon() { - for (int i = 0, size = mAnomalySparseArray.size(); i < size; i++) { - final String key = extractKeyFromUid(mAnomalySparseArray.keyAt(i)); - final PowerGaugePreference pref = (PowerGaugePreference) mAppListGroup.findPreference( - key); - if (pref != null) { - pref.shouldShowAnomalyIcon(true); - } - } + mBatteryAppListPreferenceController.refreshAppListGroup(mStatsHelper, mShowAllApps, + timeSequence); } @VisibleForTesting @@ -633,6 +405,11 @@ public class PowerUsageSummary extends PowerUsageBase implements } @VisibleForTesting + void setBatteryLayoutPreference(LayoutPreference layoutPreference) { + mBatteryLayoutPref = layoutPreference; + } + + @VisibleForTesting AnomalyDetectionPolicy getAnomalyDetectionPolicy() { return new AnomalyDetectionPolicy(getContext()); } @@ -675,54 +452,6 @@ public class PowerUsageSummary extends PowerUsageBase implements } @VisibleForTesting - double calculatePercentage(double powerUsage, double dischargeAmount) { - final double totalPower = mStatsHelper.getTotalPower(); - return totalPower == 0 ? 0 : - ((powerUsage / totalPower) * dischargeAmount); - } - - @VisibleForTesting - void setUsageSummary(Preference preference, BatterySipper sipper) { - // Only show summary when usage time is longer than one minute - final long usageTimeMs = sipper.usageTimeMs; - if (usageTimeMs >= DateUtils.MINUTE_IN_MILLIS) { - final CharSequence timeSequence = Utils.formatElapsedTime(getContext(), usageTimeMs, - false); - preference.setSummary( - (sipper.drainType != DrainType.APP || mBatteryUtils.shouldHideSipper(sipper)) - ? timeSequence - : TextUtils.expandTemplate(getText(R.string.battery_used_for), - timeSequence)); - } - } - - @VisibleForTesting - String extractKeyFromSipper(BatterySipper sipper) { - if (sipper.uidObj != null) { - return extractKeyFromUid(sipper.getUid()); - } else if (sipper.drainType == DrainType.USER) { - return sipper.drainType.toString() + sipper.userId; - } else if (sipper.drainType != DrainType.APP) { - return sipper.drainType.toString(); - } else if (sipper.getPackages() != null) { - return TextUtils.concat(sipper.getPackages()).toString(); - } else { - Log.w(TAG, "Inappropriate BatterySipper without uid and package names: " + sipper); - return "-1"; - } - } - - @VisibleForTesting - String extractKeyFromUid(int uid) { - return Integer.toString(uid); - } - - @VisibleForTesting - void setBatteryLayoutPreference(LayoutPreference layoutPreference) { - mBatteryLayoutPref = layoutPreference; - } - - @VisibleForTesting void initFeatureProvider() { final Context context = getContext(); mPowerFeatureProvider = FeatureFactory.getFactory(context) @@ -755,72 +484,6 @@ public class PowerUsageSummary extends PowerUsageBase implements } } - private static List getFakeStats() { - ArrayList stats = new ArrayList<>(); - float use = 5; - for (DrainType type : DrainType.values()) { - if (type == DrainType.APP) { - continue; - } - stats.add(new BatterySipper(type, null, use)); - use += 5; - } - for (int i = 0; i < 100; i++) { - stats.add(new BatterySipper(DrainType.APP, - new FakeUid(Process.FIRST_APPLICATION_UID + i), use)); - } - stats.add(new BatterySipper(DrainType.APP, - new FakeUid(0), use)); - - // Simulate dex2oat process. - BatterySipper sipper = new BatterySipper(DrainType.APP, - new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID)), 10.0f); - sipper.packageWithHighestDrain = "dex2oat"; - stats.add(sipper); - - sipper = new BatterySipper(DrainType.APP, - new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID + 1)), 10.0f); - sipper.packageWithHighestDrain = "dex2oat"; - stats.add(sipper); - - sipper = new BatterySipper(DrainType.APP, - new FakeUid(UserHandle.getSharedAppGid(Process.LOG_UID)), 9.0f); - stats.add(sipper); - - return stats; - } - - Handler mHandler = new Handler() { - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case BatteryEntry.MSG_UPDATE_NAME_ICON: - BatteryEntry entry = (BatteryEntry) msg.obj; - PowerGaugePreference pgp = - (PowerGaugePreference) findPreference( - Integer.toString(entry.sipper.uidObj.getUid())); - if (pgp != null) { - final int userId = UserHandle.getUserId(entry.sipper.getUid()); - final UserHandle userHandle = new UserHandle(userId); - pgp.setIcon(mUm.getBadgedIconForUser(entry.getIcon(), userHandle)); - pgp.setTitle(entry.name); - if (entry.sipper.drainType == DrainType.APP) { - pgp.setContentDescription(entry.name); - } - } - break; - case BatteryEntry.MSG_REPORT_FULLY_DRAWN: - Activity activity = getActivity(); - if (activity != null) { - activity.reportFullyDrawn(); - } - break; - } - super.handleMessage(msg); - } - }; - @Override public void onAnomalyHandled(Anomaly anomaly) { mAnomalySummaryPreferenceController.hideHighUsagePreference(); diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryAppListPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryAppListPreferenceControllerTest.java new file mode 100644 index 0000000000..528209115e --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryAppListPreferenceControllerTest.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2017 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; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.support.v14.preference.PreferenceFragment; +import android.support.v7.preference.PreferenceGroup; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.util.SparseArray; + +import com.android.internal.os.BatterySipper; +import com.android.internal.os.BatteryStatsImpl; +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.TestConfig; +import com.android.settings.fuelgauge.anomaly.Anomaly; +import com.android.settings.testutils.FakeFeatureFactory; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class BatteryAppListPreferenceControllerTest { + private static final String[] PACKAGE_NAMES = {"com.app1", "com.app2"}; + private static final String KEY_APP_LIST = "app_list"; + private static final int UID = 123; + + @Mock + private BatterySipper mNormalBatterySipper; + @Mock + private SettingsActivity mSettingsActivity; + @Mock + private PreferenceGroup mAppListGroup; + @Mock + private PreferenceFragment mFragment; + @Mock + private BatteryUtils mBatteryUtils; + + private Context mContext; + private PowerGaugePreference mPreference; + private BatteryAppListPreferenceController mPreferenceController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = spy(RuntimeEnvironment.application); + FakeFeatureFactory.setupForTest(); + + mPreference = new PowerGaugePreference(mContext); + when(mNormalBatterySipper.getPackages()).thenReturn(PACKAGE_NAMES); + when(mNormalBatterySipper.getUid()).thenReturn(UID); + mNormalBatterySipper.drainType = BatterySipper.DrainType.APP; + + mPreferenceController = new BatteryAppListPreferenceController(mContext, KEY_APP_LIST, null, + mSettingsActivity, mFragment); + mPreferenceController.mBatteryUtils = mBatteryUtils; + mPreferenceController.mAppListGroup = mAppListGroup; + } + + @Test + public void testExtractKeyFromSipper_typeAPPUidObjectNull_returnPackageNames() { + mNormalBatterySipper.uidObj = null; + mNormalBatterySipper.drainType = BatterySipper.DrainType.APP; + + final String key = mPreferenceController.extractKeyFromSipper(mNormalBatterySipper); + assertThat(key).isEqualTo(TextUtils.concat(mNormalBatterySipper.getPackages()).toString()); + } + + @Test + public void testExtractKeyFromSipper_typeOther_returnDrainType() { + mNormalBatterySipper.uidObj = null; + mNormalBatterySipper.drainType = BatterySipper.DrainType.BLUETOOTH; + + final String key = mPreferenceController.extractKeyFromSipper(mNormalBatterySipper); + assertThat(key).isEqualTo(mNormalBatterySipper.drainType.toString()); + } + + @Test + public void testExtractKeyFromSipper_typeUser_returnDrainTypeWithUserId() { + mNormalBatterySipper.uidObj = null; + mNormalBatterySipper.drainType = BatterySipper.DrainType.USER; + mNormalBatterySipper.userId = 2; + + final String key = mPreferenceController.extractKeyFromSipper(mNormalBatterySipper); + assertThat(key).isEqualTo("USER2"); + } + + @Test + public void testExtractKeyFromSipper_typeAPPUidObjectNotNull_returnUid() { + mNormalBatterySipper.uidObj = new BatteryStatsImpl.Uid(new BatteryStatsImpl(), UID); + mNormalBatterySipper.drainType = BatterySipper.DrainType.APP; + + final String key = mPreferenceController.extractKeyFromSipper(mNormalBatterySipper); + assertThat(key).isEqualTo(Integer.toString(mNormalBatterySipper.getUid())); + } + + @Test + public void testSetUsageSummary_timeLessThanOneMinute_DoNotSetSummary() { + mNormalBatterySipper.usageTimeMs = 59 * DateUtils.SECOND_IN_MILLIS; + + mPreferenceController.setUsageSummary(mPreference, mNormalBatterySipper); + assertThat(mPreference.getSummary()).isNull(); + } + + @Test + public void testSetUsageSummary_timeMoreThanOneMinute_normalApp_setScreenSummary() { + mNormalBatterySipper.usageTimeMs = 2 * DateUtils.MINUTE_IN_MILLIS; + doReturn(mContext.getText(R.string.battery_used_for)).when(mFragment).getText( + R.string.battery_used_for); + doReturn(mContext).when(mFragment).getContext(); + + mPreferenceController.setUsageSummary(mPreference, mNormalBatterySipper); + + assertThat(mPreference.getSummary().toString()).isEqualTo("Used for 2m"); + } + + @Test + public void testSetUsageSummary_timeMoreThanOneMinute_hiddenApp_setUsedSummary() { + mNormalBatterySipper.usageTimeMs = 2 * DateUtils.MINUTE_IN_MILLIS; + doReturn(true).when(mBatteryUtils).shouldHideSipper(mNormalBatterySipper); + doReturn(mContext).when(mFragment).getContext(); + + mPreferenceController.setUsageSummary(mPreference, mNormalBatterySipper); + + assertThat(mPreference.getSummary().toString()).isEqualTo("2m"); + } + + @Test + public void testSetUsageSummary_timeMoreThanOneMinute_notApp_setUsedSummary() { + mNormalBatterySipper.usageTimeMs = 2 * DateUtils.MINUTE_IN_MILLIS; + mNormalBatterySipper.drainType = BatterySipper.DrainType.PHONE; + doReturn(mContext).when(mFragment).getContext(); + + mPreferenceController.setUsageSummary(mPreference, mNormalBatterySipper); + + assertThat(mPreference.getSummary().toString()).isEqualTo("2m"); + } + + @Test + public void testRefreshAnomalyIcon_containsAnomaly_showAnomalyIcon() { + PowerGaugePreference preference = new PowerGaugePreference(mContext); + final String key = mPreferenceController.extractKeyFromUid(UID); + final SparseArray> anomalySparseArray = new SparseArray<>(); + anomalySparseArray.append(UID, null); + preference.setKey(key); + doReturn(preference).when(mAppListGroup).findPreference(key); + + mPreferenceController.refreshAnomalyIcon(anomalySparseArray); + + assertThat(preference.showAnomalyIcon()).isTrue(); + } + + @Test + public void testShouldHideSipper_typeOvercounted_returnTrue() { + mNormalBatterySipper.drainType = BatterySipper.DrainType.OVERCOUNTED; + + assertThat(mPreferenceController.shouldHideSipper(mNormalBatterySipper)).isTrue(); + } + + @Test + public void testShouldHideSipper_typeUnaccounted_returnTrue() { + mNormalBatterySipper.drainType = BatterySipper.DrainType.UNACCOUNTED; + + assertThat(mPreferenceController.shouldHideSipper(mNormalBatterySipper)).isTrue(); + } + + @Test + public void testShouldHideSipper_typeNormal_returnFalse() { + mNormalBatterySipper.drainType = BatterySipper.DrainType.APP; + + assertThat(mPreferenceController.shouldHideSipper(mNormalBatterySipper)).isFalse(); + } +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java new file mode 100644 index 0000000000..16439f43a8 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2016 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; + +import static com.android.settings.fuelgauge.PowerUsageSummary.MENU_HIGH_POWER_APPS; +import static com.android.settings.fuelgauge.PowerUsageSummary.MENU_TOGGLE_APPS; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.LoaderManager; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.PowerManager; +import android.support.v7.preference.PreferenceScreen; +import android.util.SparseArray; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.TextView; + +import com.android.internal.logging.nano.MetricsProto; +import com.android.internal.os.BatterySipper; +import com.android.internal.os.BatteryStatsHelper; +import com.android.settings.R; +import com.android.settings.SettingsActivity; +import com.android.settings.TestConfig; +import com.android.settings.Utils; +import com.android.settings.applications.LayoutPreference; +import com.android.settings.fuelgauge.anomaly.Anomaly; +import com.android.settings.fuelgauge.anomaly.AnomalyDetectionPolicy; +import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.testutils.XmlTestUtils; +import com.android.settings.testutils.shadow.SettingsShadowResources; +import com.android.settingslib.core.AbstractPreferenceController; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; + +/** + * Unit tests for {@link PowerUsageSummary}. + */ +// TODO: Improve this test class so that it starts up the real activity and fragment. +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, + sdk = TestConfig.SDK_VERSION_O, + shadows = { + SettingsShadowResources.class, + SettingsShadowResources.SettingsShadowTheme.class, + }) +public class PowerUsageSummaryTest { + private static final String STUB_STRING = "stub_string"; + private static final int UID = 123; + private static final int UID_2 = 234; + private static final int POWER_MAH = 100; + private static final long TIME_SINCE_LAST_FULL_CHARGE_MS = 120 * 60 * 1000; + private static final long TIME_SINCE_LAST_FULL_CHARGE_US = + TIME_SINCE_LAST_FULL_CHARGE_MS * 1000; + private static final long USAGE_TIME_MS = 65 * 60 * 1000; + private static final double TOTAL_POWER = 200; + public static final String NEW_ML_EST_SUFFIX = "(New ML est)"; + public static final String OLD_EST_SUFFIX = "(Old est)"; + private static Intent sAdditionalBatteryInfoIntent; + + @BeforeClass + public static void beforeClass() { + sAdditionalBatteryInfoIntent = new Intent("com.example.app.ADDITIONAL_BATTERY_INFO"); + } + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Context mContext; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Menu mMenu; + @Mock + private MenuItem mToggleAppsMenu; + @Mock + private MenuItem mHighPowerMenu; + @Mock + private MenuInflater mMenuInflater; + @Mock + private BatterySipper mNormalBatterySipper; + @Mock + private BatterySipper mScreenBatterySipper; + @Mock + private BatterySipper mCellBatterySipper; + @Mock + private LayoutPreference mBatteryLayoutPref; + @Mock + private TextView mBatteryPercentText; + @Mock + private TextView mSummary1; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private BatteryStatsHelper mBatteryHelper; + @Mock + private PowerManager mPowerManager; + @Mock + private SettingsActivity mSettingsActivity; + @Mock + private LoaderManager mLoaderManager; + @Mock + private PreferenceScreen mPreferenceScreen; + @Mock + private AnomalyDetectionPolicy mAnomalyDetectionPolicy; + @Mock + private BatteryHeaderPreferenceController mBatteryHeaderPreferenceController; + + private List mUsageList; + private Context mRealContext; + private TestFragment mFragment; + private FakeFeatureFactory mFeatureFactory; + private BatteryMeterView mBatteryMeterView; + private PowerGaugePreference mScreenUsagePref; + private PowerGaugePreference mLastFullChargePref; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mRealContext = RuntimeEnvironment.application; + mFeatureFactory = FakeFeatureFactory.setupForTest(); + when(mContext.getSystemService(Context.POWER_SERVICE)).thenReturn(mPowerManager); + + mScreenUsagePref = new PowerGaugePreference(mRealContext); + mLastFullChargePref = new PowerGaugePreference(mRealContext); + mFragment = spy(new TestFragment(mContext)); + mFragment.initFeatureProvider(); + mBatteryMeterView = new BatteryMeterView(mRealContext); + mBatteryMeterView.mDrawable = new BatteryMeterView.BatteryMeterDrawable(mRealContext, 0); + doNothing().when(mFragment).restartBatteryStatsLoader(); + doReturn(mock(LoaderManager.class)).when(mFragment).getLoaderManager(); + + when(mFragment.getActivity()).thenReturn(mSettingsActivity); + when(mToggleAppsMenu.getItemId()).thenReturn(MENU_TOGGLE_APPS); + when(mHighPowerMenu.getItemId()).thenReturn(MENU_HIGH_POWER_APPS); + when(mFeatureFactory.powerUsageFeatureProvider.getAdditionalBatteryInfoIntent()) + .thenReturn(sAdditionalBatteryInfoIntent); + when(mBatteryHelper.getTotalPower()).thenReturn(TOTAL_POWER); + when(mBatteryHelper.getStats().computeBatteryRealtime(anyLong(), anyInt())).thenReturn( + TIME_SINCE_LAST_FULL_CHARGE_US); + + when(mNormalBatterySipper.getUid()).thenReturn(UID); + mNormalBatterySipper.totalPowerMah = POWER_MAH; + mNormalBatterySipper.drainType = BatterySipper.DrainType.APP; + + mCellBatterySipper.drainType = BatterySipper.DrainType.CELL; + mCellBatterySipper.totalPowerMah = POWER_MAH; + + when(mBatteryLayoutPref.findViewById(R.id.summary1)).thenReturn(mSummary1); + when(mBatteryLayoutPref.findViewById(R.id.battery_percent)).thenReturn(mBatteryPercentText); + when(mBatteryLayoutPref.findViewById(R.id.battery_header_icon)) + .thenReturn(mBatteryMeterView); + mFragment.setBatteryLayoutPreference(mBatteryLayoutPref); + + mScreenBatterySipper.drainType = BatterySipper.DrainType.SCREEN; + mScreenBatterySipper.usageTimeMs = USAGE_TIME_MS; + + mUsageList = new ArrayList<>(); + mUsageList.add(mNormalBatterySipper); + mUsageList.add(mScreenBatterySipper); + mUsageList.add(mCellBatterySipper); + + mFragment.mStatsHelper = mBatteryHelper; + when(mBatteryHelper.getUsageList()).thenReturn(mUsageList); + mFragment.mScreenUsagePref = mScreenUsagePref; + mFragment.mLastFullChargePref = mLastFullChargePref; + mFragment.mBatteryUtils = spy(new BatteryUtils(mRealContext)); + } + + @Test + public void testOptionsMenu_menuHighPower_metricEventInvoked() { + mFragment.onOptionsItemSelected(mHighPowerMenu); + + verify(mFeatureFactory.metricsFeatureProvider).action(mContext, + MetricsProto.MetricsEvent.ACTION_SETTINGS_MENU_BATTERY_OPTIMIZATION); + } + + @Test + public void testOptionsMenu_menuAppToggle_metricEventInvoked() { + mFragment.onOptionsItemSelected(mToggleAppsMenu); + mFragment.mShowAllApps = false; + + verify(mFeatureFactory.metricsFeatureProvider).action(mContext, + MetricsProto.MetricsEvent.ACTION_SETTINGS_MENU_BATTERY_APPS_TOGGLE, true); + } + + @Test + public void testOptionsMenu_toggleAppsEnabled() { + when(mFeatureFactory.powerUsageFeatureProvider.isPowerAccountingToggleEnabled()) + .thenReturn(true); + mFragment.mShowAllApps = false; + + mFragment.onCreateOptionsMenu(mMenu, mMenuInflater); + + verify(mMenu).add(Menu.NONE, MENU_TOGGLE_APPS, Menu.NONE, R.string.show_all_apps); + } + + @Test + public void testOptionsMenu_clickToggleAppsMenu_dataChanged() { + testToggleAllApps(true); + testToggleAllApps(false); + } + + private void testToggleAllApps(final boolean isShowApps) { + mFragment.mShowAllApps = isShowApps; + + mFragment.onOptionsItemSelected(mToggleAppsMenu); + assertThat(mFragment.mShowAllApps).isEqualTo(!isShowApps); + } + + @Test + public void testFindBatterySipperByType_findTypeScreen() { + BatterySipper sipper = mFragment.findBatterySipperByType(mUsageList, + BatterySipper.DrainType.SCREEN); + + assertThat(sipper).isSameAs(mScreenBatterySipper); + } + + @Test + public void testFindBatterySipperByType_findTypeApp() { + BatterySipper sipper = mFragment.findBatterySipperByType(mUsageList, + BatterySipper.DrainType.APP); + + assertThat(sipper).isSameAs(mNormalBatterySipper); + } + + @Test + public void testUpdateScreenPreference_showCorrectSummary() { + doReturn(mScreenBatterySipper).when(mFragment).findBatterySipperByType(any(), any()); + doReturn(mRealContext).when(mFragment).getContext(); + final CharSequence expectedSummary = Utils.formatElapsedTime(mRealContext, USAGE_TIME_MS, + false); + + mFragment.updateScreenPreference(); + + assertThat(mScreenUsagePref.getSubtitle()).isEqualTo(expectedSummary); + } + + @Test + public void testUpdateLastFullChargePreference_showCorrectSummary() { + doReturn(mRealContext).when(mFragment).getContext(); + + mFragment.updateLastFullChargePreference(TIME_SINCE_LAST_FULL_CHARGE_MS); + + assertThat(mLastFullChargePref.getSubtitle()).isEqualTo("2 hr. ago"); + } + + @Test + public void testUpdatePreference_usageListEmpty_shouldNotCrash() { + when(mBatteryHelper.getUsageList()).thenReturn(new ArrayList()); + doReturn(STUB_STRING).when(mFragment).getString(anyInt(), any()); + doReturn(mRealContext).when(mFragment).getContext(); + + // Should not crash when update + mFragment.updateScreenPreference(); + } + + @Test + public void testNonIndexableKeys_MatchPreferenceKeys() { + final Context context = RuntimeEnvironment.application; + final List niks = PowerUsageSummary.SEARCH_INDEX_DATA_PROVIDER + .getNonIndexableKeys(context); + + final List keys = XmlTestUtils.getKeysFromPreferenceXml(context, + R.xml.power_usage_summary); + + assertThat(keys).containsAllIn(niks); + } + + @Test + public void testPreferenceControllers_getPreferenceKeys_existInPreferenceScreen() { + final Context context = RuntimeEnvironment.application; + final PowerUsageSummary fragment = new PowerUsageSummary(); + final List preferenceScreenKeys = XmlTestUtils.getKeysFromPreferenceXml(context, + fragment.getPreferenceScreenResId()); + final List preferenceKeys = new ArrayList<>(); + + for (AbstractPreferenceController controller : fragment.getPreferenceControllers(context)) { + preferenceKeys.add(controller.getPreferenceKey()); + } + + assertThat(preferenceScreenKeys).containsAllIn(preferenceKeys); + } + + @Test + public void testUpdateAnomalySparseArray() { + mFragment.mAnomalySparseArray = new SparseArray<>(); + final List anomalies = new ArrayList<>(); + final Anomaly anomaly1 = new Anomaly.Builder().setUid(UID).build(); + final Anomaly anomaly2 = new Anomaly.Builder().setUid(UID).build(); + final Anomaly anomaly3 = new Anomaly.Builder().setUid(UID_2).build(); + anomalies.add(anomaly1); + anomalies.add(anomaly2); + anomalies.add(anomaly3); + + mFragment.updateAnomalySparseArray(anomalies); + + assertThat(mFragment.mAnomalySparseArray.get(UID)).containsExactly(anomaly1, anomaly2); + assertThat(mFragment.mAnomalySparseArray.get(UID_2)).containsExactly(anomaly3); + } + + @Test + public void testInitAnomalyDetectionIfPossible_detectionEnabled_init() { + doReturn(mLoaderManager).when(mFragment).getLoaderManager(); + doReturn(mAnomalyDetectionPolicy).when(mFragment).getAnomalyDetectionPolicy(); + when(mAnomalyDetectionPolicy.isAnomalyDetectionEnabled()).thenReturn(true); + + mFragment.restartAnomalyDetectionIfPossible(); + + verify(mLoaderManager).restartLoader(eq(PowerUsageSummary.ANOMALY_LOADER), eq(Bundle.EMPTY), + any()); + } + + @Test + public void testShowBothEstimates_summariesAreBothModified() { + doReturn(new TextView(mRealContext)).when(mBatteryLayoutPref).findViewById(R.id.summary2); + doReturn(new TextView(mRealContext)).when(mBatteryLayoutPref).findViewById(R.id.summary1); + mFragment.onLongClick(new View(mRealContext)); + TextView summary1 = mFragment.mBatteryLayoutPref.findViewById(R.id.summary1); + TextView summary2 = mFragment.mBatteryLayoutPref.findViewById(R.id.summary2); + Robolectric.flushBackgroundThreadScheduler(); + assertThat(summary2.getText().toString().contains(NEW_ML_EST_SUFFIX)); + assertThat(summary1.getText().toString().contains(OLD_EST_SUFFIX)); + } + + @Test + public void testSaveInstanceState_showAllAppsRestored() { + Bundle bundle = new Bundle(); + mFragment.mShowAllApps = true; + doReturn(mPreferenceScreen).when(mFragment).getPreferenceScreen(); + + mFragment.onSaveInstanceState(bundle); + mFragment.restoreSavedInstance(bundle); + + assertThat(mFragment.mShowAllApps).isTrue(); + } + + @Test + public void testDebugMode() { + doReturn(true).when(mFeatureFactory.powerUsageFeatureProvider).isEstimateDebugEnabled(); + + mFragment.restartBatteryInfoLoader(); + ArgumentCaptor listener = ArgumentCaptor.forClass( + View.OnLongClickListener.class); + verify(mSummary1).setOnLongClickListener(listener.capture()); + + // Calling the listener should disable it. + listener.getValue().onLongClick(mSummary1); + verify(mSummary1).setOnLongClickListener(null); + + // Restarting the loader should reset the listener. + mFragment.restartBatteryInfoLoader(); + verify(mSummary1, times(2)).setOnLongClickListener(any(View.OnLongClickListener.class)); + } + + @Test + public void testRestartBatteryStatsLoader_notClearHeader_quickUpdateNotInvoked() { + mFragment.mBatteryHeaderPreferenceController = mBatteryHeaderPreferenceController; + + mFragment.restartBatteryStatsLoader(false /* clearHeader */); + + verify(mBatteryHeaderPreferenceController, never()).quickUpdateHeaderPreference(); + } + + public static class TestFragment extends PowerUsageSummary { + private Context mContext; + + public TestFragment(Context context) { + mContext = context; + } + + @Override + public Context getContext() { + return mContext; + } + + + @Override + protected void refreshUi() { + // Leave it empty for toggle apps menu test + } + } +} -- 2.11.0