diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 07908e5d32c..8eaf761d478 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2213,16 +2213,13 @@ - + android:enabled="false"> + + - - - - - + @@ -2241,6 +2238,27 @@ android:value="com.android.settings.fuelgauge.PowerUsageSummary" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index da35c4b8b23..ff0f59f47b1 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -16,6 +16,7 @@ package com.android.settings; +import static com.android.settings.core.FeatureFlags.BATTERY_SETTINGS_V2; import static com.android.settings.core.FeatureFlags.CONNECTED_DEVICE_V2; import android.os.Bundle; @@ -72,7 +73,6 @@ public class Settings extends SettingsActivity { public static class PrivacySettingsActivity extends SettingsActivity { /* empty */ } public static class FactoryResetActivity extends SettingsActivity { /* empty */ } public static class RunningServicesActivity extends SettingsActivity { /* empty */ } - public static class PowerUsageSummaryActivity extends SettingsActivity { /* empty */ } public static class BatterySaverSettingsActivity extends SettingsActivity { /* empty */ } public static class AccountSyncSettingsActivity extends SettingsActivity { /* empty */ } public static class AccountSyncSettingsInAddAccountActivity extends SettingsActivity { /* empty */ } @@ -174,6 +174,8 @@ public class Settings extends SettingsActivity { } } public static class ConnectedDeviceDashboardActivityOld extends SettingsActivity {} + public static class PowerUsageSummaryActivity extends SettingsActivity { /* empty */ } + public static class PowerUsageSummaryLegacyActivity extends SettingsActivity { /* empty */ } public static class AppAndNotificationDashboardActivity extends SettingsActivity {} public static class StorageDashboardActivity extends SettingsActivity {} public static class UserAndAccountDashboardActivity extends SettingsActivity {} diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java index dc961be45c2..e684e5ea161 100644 --- a/src/com/android/settings/SettingsActivity.java +++ b/src/com/android/settings/SettingsActivity.java @@ -810,9 +810,17 @@ public class SettingsActivity extends SettingsDrawerActivity Utils.showSimCardTile(this), isAdmin) || somethingChanged; + final boolean isBatterySettingsV2Enabled = FeatureFactory.getFactory(this) + .getPowerUsageFeatureProvider(this) + .isBatteryV2Enabled(); + // Enable new battery page if v2 enabled somethingChanged = setTileEnabled(new ComponentName(packageName, Settings.PowerUsageSummaryActivity.class.getName()), - mBatteryPresent, isAdmin) || somethingChanged; + mBatteryPresent && isBatterySettingsV2Enabled, isAdmin) || somethingChanged; + // Enable legacy battery page if v2 disabled + somethingChanged = setTileEnabled(new ComponentName(packageName, + Settings.PowerUsageSummaryLegacyActivity.class.getName()), + mBatteryPresent && !isBatterySettingsV2Enabled, isAdmin) || somethingChanged; somethingChanged = setTileEnabled(new ComponentName(packageName, Settings.UserSettingsActivity.class.getName()), diff --git a/src/com/android/settings/core/FeatureFlags.java b/src/com/android/settings/core/FeatureFlags.java index 547318faf02..197876fcb17 100644 --- a/src/com/android/settings/core/FeatureFlags.java +++ b/src/com/android/settings/core/FeatureFlags.java @@ -25,4 +25,5 @@ public class FeatureFlags { public static final String SUGGESTIONS_V2 = "new_settings_suggestion"; public static final String APP_INFO_V2 = "settings_app_info_v2"; public static final String CONNECTED_DEVICE_V2 = "settings_connected_device_v2"; + public static final String BATTERY_SETTINGS_V2 = "settings_battery_v2"; } diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java index 37cd4310be7..7720b48b84b 100644 --- a/src/com/android/settings/core/gateway/SettingsGateway.java +++ b/src/com/android/settings/core/gateway/SettingsGateway.java @@ -79,6 +79,7 @@ import com.android.settings.enterprise.EnterprisePrivacySettings; import com.android.settings.fuelgauge.AdvancedPowerUsageDetail; import com.android.settings.fuelgauge.BatterySaverSettings; import com.android.settings.fuelgauge.PowerUsageSummary; +import com.android.settings.fuelgauge.PowerUsageSummaryLegacy; import com.android.settings.gestures.AssistGestureSettings; import com.android.settings.gestures.DoubleTapPowerSettings; import com.android.settings.gestures.DoubleTapScreenSettings; @@ -181,6 +182,7 @@ public class SettingsGateway { AndroidBeam.class.getName(), WifiDisplaySettings.class.getName(), PowerUsageSummary.class.getName(), + PowerUsageSummaryLegacy.class.getName(), AccountSyncSettings.class.getName(), AssistGestureSettings.class.getName(), SwipeToNotificationSettings.class.getName(), diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java index 6d7e1e8dac3..1fb02c4aafa 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java +++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java @@ -115,4 +115,9 @@ public interface PowerUsageFeatureProvider { * enabled. This string notifies users that the estimate is using enhanced prediction. */ String getAdvancedUsageScreenInfoString(); + + /** + * Checks whether to display the battery v2. + */ + boolean isBatteryV2Enabled(); } diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java index 08143645ef7..cda4d3d7be9 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java +++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java @@ -16,12 +16,15 @@ package com.android.settings.fuelgauge; +import static com.android.settings.core.FeatureFlags.BATTERY_SETTINGS_V2; + import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Process; +import android.util.FeatureFlagUtils; import android.util.SparseIntArray; import com.android.internal.os.BatterySipper; @@ -36,9 +39,11 @@ public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider PACKAGE_CALENDAR_PROVIDER, PACKAGE_SYSTEMUI}; protected PackageManager mPackageManager; + protected Context mContext; public PowerUsageFeatureProviderImpl(Context context) { mPackageManager = context.getPackageManager(); + mContext = context.getApplicationContext(); } @Override @@ -133,4 +138,9 @@ public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider public String getAdvancedUsageScreenInfoString() { return null; } + + @Override + public boolean isBatteryV2Enabled() { + return FeatureFlagUtils.isEnabled(mContext, BATTERY_SETTINGS_V2); + } } diff --git a/src/com/android/settings/fuelgauge/PowerUsageSummary.java b/src/com/android/settings/fuelgauge/PowerUsageSummary.java index bf3cc645975..ed5b6f40cfc 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageSummary.java +++ b/src/com/android/settings/fuelgauge/PowerUsageSummary.java @@ -251,7 +251,7 @@ public class PowerUsageSummary extends PowerUsageBase implements @Override public int getMetricsCategory() { - return MetricsEvent.FUELGAUGE_POWER_USAGE_SUMMARY; + return MetricsEvent.FUELGAUGE_POWER_USAGE_SUMMARY_V2; } @Override diff --git a/src/com/android/settings/fuelgauge/PowerUsageSummaryLegacy.java b/src/com/android/settings/fuelgauge/PowerUsageSummaryLegacy.java new file mode 100644 index 00000000000..c50d5808629 --- /dev/null +++ b/src/com/android/settings/fuelgauge/PowerUsageSummaryLegacy.java @@ -0,0 +1,889 @@ +/* + * Copyright (C) 2009 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.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.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; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.widget.TextView; + +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; +import com.android.settings.Utils; +import com.android.settings.applications.LayoutPreference; +import com.android.settings.applications.manageapplications.ManageApplications; +import com.android.settings.core.instrumentation.MetricsFeatureProvider; +import com.android.settings.dashboard.SummaryLoader; +import com.android.settings.display.AmbientDisplayPreferenceController; +import com.android.settings.display.AutoBrightnessPreferenceController; +import com.android.settings.display.BatteryPercentagePreferenceController; +import com.android.settings.display.TimeoutPreferenceController; +import com.android.settings.fuelgauge.anomaly.Anomaly; +import com.android.settings.fuelgauge.anomaly.AnomalyDetectionPolicy; +import com.android.settings.fuelgauge.anomaly.AnomalyDialogFragment.AnomalyDialogListener; +import com.android.settings.fuelgauge.anomaly.AnomalyLoader; +import com.android.settings.fuelgauge.anomaly.AnomalySummaryPreferenceController; +import com.android.settings.fuelgauge.anomaly.AnomalyUtils; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.List; + +/** + * Displays a list of apps and subsystems that consume power, ordered by how much power was + * consumed since the last time it was unplugged. + * + * This is the battery page used in Android O with the app usage list. It is also used for battery + * debug. + */ +public class PowerUsageSummaryLegacy extends PowerUsageBase implements + AnomalyDialogListener, OnLongClickListener, OnClickListener { + + static final String TAG = "PowerUsageSummaryLegacy"; + + 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"; + + private static final String KEY_AUTO_BRIGHTNESS = "auto_brightness_battery"; + private static final String KEY_SCREEN_TIMEOUT = "screen_timeout_battery"; + private static final String KEY_AMBIENT_DISPLAY = "ambient_display_battery"; + private static final String KEY_BATTERY_SAVER_SUMMARY = "battery_saver_summary"; + private static final String KEY_HIGH_USAGE = "high_usage"; + + @VisibleForTesting + static final int ANOMALY_LOADER = 1; + @VisibleForTesting + static final int BATTERY_INFO_LOADER = 2; + private static final int MENU_STATS_TYPE = Menu.FIRST; + @VisibleForTesting + static final int MENU_HIGH_POWER_APPS = Menu.FIRST + 3; + @VisibleForTesting + static final int MENU_TOGGLE_APPS = Menu.FIRST + 4; + private static final int MENU_HELP = Menu.FIRST + 5; + public static final int DEBUG_INFO_LOADER = 3; + + @VisibleForTesting + boolean mShowAllApps = false; + @VisibleForTesting + PowerGaugePreference mScreenUsagePref; + @VisibleForTesting + PowerGaugePreference mLastFullChargePref; + @VisibleForTesting + PowerUsageFeatureProvider mPowerFeatureProvider; + @VisibleForTesting + BatteryUtils mBatteryUtils; + @VisibleForTesting + LayoutPreference mBatteryLayoutPref; + + /** + * SparseArray that maps uid to {@link Anomaly}, so we could find {@link Anomaly} by uid + */ + @VisibleForTesting + SparseArray> mAnomalySparseArray; + @VisibleForTesting + PreferenceGroup mAppListGroup; + @VisibleForTesting + BatteryHeaderPreferenceController mBatteryHeaderPreferenceController; + private AnomalySummaryPreferenceController mAnomalySummaryPreferenceController; + private int mStatsType = BatteryStats.STATS_SINCE_CHARGED; + + private LoaderCallbacks> mAnomalyLoaderCallbacks = + new LoaderCallbacks>() { + + @Override + public Loader> onCreateLoader(int id, Bundle args) { + return new AnomalyLoader(getContext(), mStatsHelper); + } + + @Override + public void onLoadFinished(Loader> loader, List data) { + final AnomalyUtils anomalyUtils = AnomalyUtils.getInstance(getContext()); + anomalyUtils.logAnomalies(mMetricsFeatureProvider, data, + MetricsEvent.FUELGAUGE_POWER_USAGE_SUMMARY); + + // show high usage preference if possible + mAnomalySummaryPreferenceController.updateAnomalySummaryPreference(data); + + updateAnomalySparseArray(data); + refreshAnomalyIcon(); + } + + @Override + public void onLoaderReset(Loader> loader) { + + } + }; + + @VisibleForTesting + LoaderCallbacks mBatteryInfoLoaderCallbacks = + new LoaderCallbacks() { + + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + return new BatteryInfoLoader(getContext(), mStatsHelper); + } + + @Override + public void onLoadFinished(Loader loader, BatteryInfo batteryInfo) { + mBatteryHeaderPreferenceController.updateHeaderPreference(batteryInfo); + } + + @Override + public void onLoaderReset(Loader loader) { + // do nothing + } + }; + + LoaderCallbacks> mBatteryInfoDebugLoaderCallbacks = + new LoaderCallbacks>() { + @Override + public Loader> onCreateLoader(int i, Bundle bundle) { + return new DebugEstimatesLoader(getContext(), mStatsHelper); + } + + @Override + public void onLoadFinished(Loader> loader, + List batteryInfos) { + final BatteryMeterView batteryView = (BatteryMeterView) mBatteryLayoutPref + .findViewById(R.id.battery_header_icon); + final TextView percentRemaining = + mBatteryLayoutPref.findViewById(R.id.battery_percent); + final TextView summary1 = mBatteryLayoutPref.findViewById(R.id.summary1); + final TextView summary2 = mBatteryLayoutPref.findViewById(R.id.summary2); + BatteryInfo oldInfo = batteryInfos.get(0); + BatteryInfo newInfo = batteryInfos.get(1); + percentRemaining.setText(Utils.formatPercentage(oldInfo.batteryLevel)); + + // set the text to the old estimate (copied from battery info). Note that this + // can sometimes say 0 time remaining because battery stats requires the phone + // be unplugged for a period of time before being willing ot make an estimate. + summary1.setText(mPowerFeatureProvider.getOldEstimateDebugString( + Formatter.formatShortElapsedTime(getContext(), + BatteryUtils.convertUsToMs(oldInfo.remainingTimeUs)))); + + // for this one we can just set the string directly + summary2.setText(mPowerFeatureProvider.getEnhancedEstimateDebugString( + Formatter.formatShortElapsedTime(getContext(), + BatteryUtils.convertUsToMs(newInfo.remainingTimeUs)))); + + batteryView.setBatteryLevel(oldInfo.batteryLevel); + batteryView.setCharging(!oldInfo.discharging); + } + + @Override + public void onLoaderReset(Loader> loader) { + } + }; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setAnimationAllowed(true); + + 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); + mFooterPreferenceMixin.createFooterPreference().setTitle(R.string.battery_footer_summary); + mAnomalySummaryPreferenceController = new AnomalySummaryPreferenceController( + (SettingsActivity) getActivity(), this, MetricsEvent.FUELGAUGE_POWER_USAGE_SUMMARY); + mBatteryUtils = BatteryUtils.getInstance(getContext()); + mAnomalySparseArray = new SparseArray<>(); + + restartBatteryInfoLoader(); + restoreSavedInstance(icicle); + } + + @Override + public int getMetricsCategory() { + return MetricsEvent.FUELGAUGE_POWER_USAGE_SUMMARY; + } + + @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); + } + + @Override + public boolean onPreferenceTreeClick(Preference preference) { + if (mAnomalySummaryPreferenceController.onPreferenceTreeClick(preference)) { + return true; + } + 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); + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.power_usage_summary_legacy; + } + + @Override + protected List getPreferenceControllers(Context context) { + final List controllers = new ArrayList<>(); + mBatteryHeaderPreferenceController = new BatteryHeaderPreferenceController( + context, getActivity(), this /* host */, getLifecycle()); + controllers.add(mBatteryHeaderPreferenceController); + controllers.add(new AutoBrightnessPreferenceController(context, KEY_AUTO_BRIGHTNESS)); + controllers.add(new TimeoutPreferenceController(context, KEY_SCREEN_TIMEOUT)); + controllers.add(new BatterySaverController(context, getLifecycle())); + controllers.add(new BatteryPercentagePreferenceController(context)); + controllers.add(new AmbientDisplayPreferenceController( + context, + new AmbientDisplayConfiguration(context), + KEY_AMBIENT_DISPLAY)); + return controllers; + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + if (DEBUG) { + menu.add(Menu.NONE, MENU_STATS_TYPE, Menu.NONE, R.string.menu_stats_total) + .setIcon(com.android.internal.R.drawable.ic_menu_info_details) + .setAlphabeticShortcut('t'); + } + + menu.add(Menu.NONE, MENU_HIGH_POWER_APPS, Menu.NONE, R.string.high_power_apps); + + if (mPowerFeatureProvider.isPowerAccountingToggleEnabled()) { + menu.add(Menu.NONE, MENU_TOGGLE_APPS, Menu.NONE, + mShowAllApps ? R.string.hide_extra_apps : R.string.show_all_apps); + } + + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public int getHelpResource() { + return R.string.help_url_battery; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + final SettingsActivity sa = (SettingsActivity) getActivity(); + final Context context = getContext(); + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(context).getMetricsFeatureProvider(); + + switch (item.getItemId()) { + case MENU_STATS_TYPE: + if (mStatsType == BatteryStats.STATS_SINCE_CHARGED) { + mStatsType = BatteryStats.STATS_SINCE_UNPLUGGED; + } else { + mStatsType = BatteryStats.STATS_SINCE_CHARGED; + } + refreshUi(); + return true; + case MENU_HIGH_POWER_APPS: + Bundle args = new Bundle(); + args.putString(ManageApplications.EXTRA_CLASSNAME, + HighPowerApplicationsActivity.class.getName()); + sa.startPreferencePanel(this, ManageApplications.class.getName(), args, + R.string.high_power_apps, null, null, 0); + metricsFeatureProvider.action(context, + MetricsEvent.ACTION_SETTINGS_MENU_BATTERY_OPTIMIZATION); + return true; + case MENU_TOGGLE_APPS: + mShowAllApps = !mShowAllApps; + item.setTitle(mShowAllApps ? R.string.hide_extra_apps : R.string.show_all_apps); + metricsFeatureProvider.action(context, + MetricsEvent.ACTION_SETTINGS_MENU_BATTERY_APPS_TOGGLE, mShowAllApps); + restartBatteryStatsLoader(false /* clearHeader */); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @VisibleForTesting + void restoreSavedInstance(Bundle savedInstance) { + if (savedInstance != null) { + mShowAllApps = savedInstance.getBoolean(KEY_SHOW_ALL_APPS, false); + } + } + + 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, + null, 0, R.string.advanced_battery_title, null, getMetricsCategory()); + } else { + mStatsHelper.storeStatsHistoryInFile(BatteryHistoryDetail.BATTERY_HISTORY_FILE); + Bundle args = new Bundle(2); + args.putString(BatteryHistoryDetail.EXTRA_STATS, + BatteryHistoryDetail.BATTERY_HISTORY_FILE); + args.putParcelable(BatteryHistoryDetail.EXTRA_BROADCAST, + mStatsHelper.getBatteryBroadcast()); + Utils.startWithFragment(getContext(), BatteryHistoryDetail.class.getName(), args, + null, 0, R.string.history_details_title, null, getMetricsCategory()); + } + } + + 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) { + return; + } + + restartAnomalyDetectionIfPossible(); + + // reload BatteryInfo and updateUI + restartBatteryInfoLoader(); + final long lastFullChargeTime = mBatteryUtils.calculateLastFullChargeTime(mStatsHelper, + System.currentTimeMillis()); + updateScreenPreference(); + updateLastFullChargePreference(lastFullChargeTime); + + 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 == DrainType.OVERCOUNTED + || sipper.drainType == 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); + } + } + } + + @VisibleForTesting + void restartAnomalyDetectionIfPossible() { + if (getAnomalyDetectionPolicy().isAnomalyDetectionEnabled()) { + getLoaderManager().restartLoader(ANOMALY_LOADER, Bundle.EMPTY, mAnomalyLoaderCallbacks); + } + } + + @VisibleForTesting + AnomalyDetectionPolicy getAnomalyDetectionPolicy() { + return new AnomalyDetectionPolicy(getContext()); + } + + @VisibleForTesting + BatterySipper findBatterySipperByType(List usageList, DrainType type) { + for (int i = 0, size = usageList.size(); i < size; i++) { + final BatterySipper sipper = usageList.get(i); + if (sipper.drainType == type) { + return sipper; + } + } + return null; + } + + @VisibleForTesting + void updateScreenPreference() { + final BatterySipper sipper = findBatterySipperByType( + mStatsHelper.getUsageList(), DrainType.SCREEN); + final long usageTimeMs = sipper != null ? sipper.usageTimeMs : 0; + + mScreenUsagePref.setSubtitle(Utils.formatElapsedTime(getContext(), usageTimeMs, false)); + } + + @VisibleForTesting + void updateLastFullChargePreference(long timeMs) { + final CharSequence timeSequence = Utils.formatRelativeTime(getContext(), timeMs, false); + mLastFullChargePref.setSubtitle(timeSequence); + } + + @VisibleForTesting + void showBothEstimates() { + final Context context = getContext(); + if (context == null + || !mPowerFeatureProvider.isEnhancedBatteryPredictionEnabled(context)) { + return; + } + getLoaderManager().restartLoader(DEBUG_INFO_LOADER, Bundle.EMPTY, + mBatteryInfoDebugLoaderCallbacks); + } + + @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) + .getPowerUsageFeatureProvider(context); + } + + @VisibleForTesting + void updateAnomalySparseArray(List anomalies) { + mAnomalySparseArray.clear(); + for (int i = 0, size = anomalies.size(); i < size; i++) { + final Anomaly anomaly = anomalies.get(i); + if (mAnomalySparseArray.get(anomaly.uid) == null) { + mAnomalySparseArray.append(anomaly.uid, new ArrayList<>()); + } + mAnomalySparseArray.get(anomaly.uid).add(anomaly); + } + } + + @VisibleForTesting + void restartBatteryInfoLoader() { + getLoaderManager().restartLoader(BATTERY_INFO_LOADER, Bundle.EMPTY, + mBatteryInfoLoaderCallbacks); + if (mPowerFeatureProvider.isEstimateDebugEnabled()) { + // Unfortunately setting a long click listener on a view means it will no + // longer pass the regular click event to the parent, so we have to register + // a regular click listener as well. + View header = mBatteryLayoutPref.findViewById(R.id.summary1); + header.setOnLongClickListener(this); + header.setOnClickListener(this); + } + } + + 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(); + } + + @Override + public boolean onLongClick(View view) { + showBothEstimates(); + view.setOnLongClickListener(null); + return true; + } + + @Override + public void onClick(View view) { + performBatteryHeaderClick(); + } + + @Override + protected void restartBatteryStatsLoader() { + restartBatteryStatsLoader(true /* clearHeader */); + } + + void restartBatteryStatsLoader(boolean clearHeader) { + super.restartBatteryStatsLoader(); + if (clearHeader) { + mBatteryHeaderPreferenceController.quickUpdateHeaderPreference(); + } + } + + private static class SummaryProvider implements SummaryLoader.SummaryProvider { + private final Context mContext; + private final SummaryLoader mLoader; + private final BatteryBroadcastReceiver mBatteryBroadcastReceiver; + + private SummaryProvider(Context context, SummaryLoader loader) { + mContext = context; + mLoader = loader; + mBatteryBroadcastReceiver = new BatteryBroadcastReceiver(mContext); + mBatteryBroadcastReceiver.setBatteryChangedListener(() -> { + BatteryInfo.getBatteryInfo(mContext, new BatteryInfo.Callback() { + @Override + public void onBatteryInfoLoaded(BatteryInfo info) { + mLoader.setSummary(PowerUsageSummaryLegacy.SummaryProvider.this, info.chargeLabel); + } + }, true /* shortString */); + }); + } + + @Override + public void setListening(boolean listening) { + if (listening) { + mBatteryBroadcastReceiver.register(); + } else { + mBatteryBroadcastReceiver.unRegister(); + } + } + } + + public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY + = new SummaryLoader.SummaryProviderFactory() { + @Override + public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity, + SummaryLoader summaryLoader) { + return new SummaryProvider(activity, summaryLoader); + } + }; +} diff --git a/tests/robotests/assets/grandfather_not_implementing_index_provider b/tests/robotests/assets/grandfather_not_implementing_index_provider index 4d5b2564b90..6d3ec9a3c96 100644 --- a/tests/robotests/assets/grandfather_not_implementing_index_provider +++ b/tests/robotests/assets/grandfather_not_implementing_index_provider @@ -4,6 +4,7 @@ com.android.settings.bluetooth.BluetoothDeviceDetailsFragment com.android.settings.bluetooth.BluetoothPairingDetail com.android.settings.accounts.AccountDetailDashboardFragment com.android.settings.fuelgauge.PowerUsageAnomalyDetails +com.android.settings.fuelgauge.PowerUsageSummaryLegacy com.android.settings.fuelgauge.AdvancedPowerUsageDetail com.android.settings.development.featureflags.FeatureFlagsDashboard com.android.settings.development.qstile.DevelopmentTileConfigFragment diff --git a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryLegacyTest.java similarity index 99% rename from tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java rename to tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryLegacyTest.java index 5ab7496d7e0..55ab6cf7fa3 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryLegacyTest.java @@ -91,7 +91,7 @@ import java.util.List; SettingsShadowResources.class, SettingsShadowResources.SettingsShadowTheme.class, }) -public class PowerUsageSummaryTest { +public class PowerUsageSummaryLegacyTest { private static final String[] PACKAGE_NAMES = {"com.app1", "com.app2"}; private static final String STUB_STRING = "stub_string"; private static final int UID = 123; @@ -542,7 +542,7 @@ public class PowerUsageSummaryTest { verify(mBatteryHeaderPreferenceController, never()).quickUpdateHeaderPreference(); } - public static class TestFragment extends PowerUsageSummary { + public static class TestFragment extends PowerUsageSummaryLegacy { private Context mContext; private boolean mStartActivityCalled;