Home | History | Annotate | Download | only in fuelgauge
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.settings.fuelgauge;
     18 
     19 import static com.android.settings.fuelgauge.BatteryBroadcastReceiver.BatteryUpdateType;
     20 
     21 import android.app.Activity;
     22 import android.app.LoaderManager;
     23 import android.app.LoaderManager.LoaderCallbacks;
     24 import android.content.Context;
     25 import android.content.Loader;
     26 import android.os.BatteryStats;
     27 import android.os.Bundle;
     28 import android.provider.SearchIndexableResource;
     29 import android.support.annotation.VisibleForTesting;
     30 import android.text.BidiFormatter;
     31 import android.text.format.Formatter;
     32 import android.util.SparseArray;
     33 import android.view.Menu;
     34 import android.view.MenuInflater;
     35 import android.view.MenuItem;
     36 import android.view.View;
     37 import android.view.View.OnLongClickListener;
     38 import android.widget.TextView;
     39 
     40 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
     41 import com.android.settings.R;
     42 import com.android.settings.SettingsActivity;
     43 import com.android.settings.Utils;
     44 import com.android.settings.applications.LayoutPreference;
     45 import com.android.settings.core.SubSettingLauncher;
     46 import com.android.settings.dashboard.SummaryLoader;
     47 import com.android.settings.display.BatteryPercentagePreferenceController;
     48 import com.android.settings.fuelgauge.anomaly.Anomaly;
     49 import com.android.settings.fuelgauge.anomaly.AnomalyDetectionPolicy;
     50 import com.android.settings.fuelgauge.batterytip.BatteryTipLoader;
     51 import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController;
     52 import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
     53 import com.android.settings.overlay.FeatureFactory;
     54 import com.android.settings.search.BaseSearchIndexProvider;
     55 import com.android.settingslib.core.AbstractPreferenceController;
     56 import com.android.settingslib.core.lifecycle.Lifecycle;
     57 import com.android.settingslib.utils.PowerUtil;
     58 import com.android.settingslib.utils.StringUtil;
     59 
     60 import java.util.ArrayList;
     61 import java.util.Collections;
     62 import java.util.List;
     63 
     64 /**
     65  * Displays a list of apps and subsystems that consume power, ordered by how much power was
     66  * consumed since the last time it was unplugged.
     67  */
     68 public class PowerUsageSummary extends PowerUsageBase implements OnLongClickListener,
     69         BatteryTipPreferenceController.BatteryTipListener {
     70 
     71     static final String TAG = "PowerUsageSummary";
     72 
     73     private static final boolean DEBUG = false;
     74     private static final String KEY_BATTERY_HEADER = "battery_header";
     75     private static final String KEY_BATTERY_TIP = "battery_tip";
     76 
     77     private static final String KEY_SCREEN_USAGE = "screen_usage";
     78     private static final String KEY_TIME_SINCE_LAST_FULL_CHARGE = "last_full_charge";
     79     private static final String KEY_BATTERY_SAVER_SUMMARY = "battery_saver_summary";
     80 
     81     @VisibleForTesting
     82     static final int BATTERY_INFO_LOADER = 1;
     83     @VisibleForTesting
     84     static final int BATTERY_TIP_LOADER = 2;
     85     @VisibleForTesting
     86     static final int MENU_STATS_TYPE = Menu.FIRST;
     87     @VisibleForTesting
     88     static final int MENU_ADVANCED_BATTERY = Menu.FIRST + 1;
     89     public static final int DEBUG_INFO_LOADER = 3;
     90 
     91     @VisibleForTesting
     92     PowerGaugePreference mScreenUsagePref;
     93     @VisibleForTesting
     94     PowerGaugePreference mLastFullChargePref;
     95     @VisibleForTesting
     96     PowerUsageFeatureProvider mPowerFeatureProvider;
     97     @VisibleForTesting
     98     BatteryUtils mBatteryUtils;
     99     @VisibleForTesting
    100     LayoutPreference mBatteryLayoutPref;
    101     @VisibleForTesting
    102     BatteryInfo mBatteryInfo;
    103 
    104     /**
    105      * SparseArray that maps uid to {@link Anomaly}, so we could find {@link Anomaly} by uid
    106      */
    107     @VisibleForTesting
    108     SparseArray<List<Anomaly>> mAnomalySparseArray;
    109     @VisibleForTesting
    110     BatteryHeaderPreferenceController mBatteryHeaderPreferenceController;
    111     @VisibleForTesting
    112     boolean mNeedUpdateBatteryTip;
    113     @VisibleForTesting
    114     BatteryTipPreferenceController mBatteryTipPreferenceController;
    115     private int mStatsType = BatteryStats.STATS_SINCE_CHARGED;
    116 
    117     @VisibleForTesting
    118     LoaderManager.LoaderCallbacks<BatteryInfo> mBatteryInfoLoaderCallbacks =
    119             new LoaderManager.LoaderCallbacks<BatteryInfo>() {
    120 
    121                 @Override
    122                 public Loader<BatteryInfo> onCreateLoader(int i, Bundle bundle) {
    123                     return new BatteryInfoLoader(getContext(), mStatsHelper);
    124                 }
    125 
    126                 @Override
    127                 public void onLoadFinished(Loader<BatteryInfo> loader, BatteryInfo batteryInfo) {
    128                     mBatteryHeaderPreferenceController.updateHeaderPreference(batteryInfo);
    129                     mBatteryInfo = batteryInfo;
    130                     updateLastFullChargePreference();
    131                 }
    132 
    133                 @Override
    134                 public void onLoaderReset(Loader<BatteryInfo> loader) {
    135                     // do nothing
    136                 }
    137             };
    138 
    139     LoaderManager.LoaderCallbacks<List<BatteryInfo>> mBatteryInfoDebugLoaderCallbacks =
    140             new LoaderCallbacks<List<BatteryInfo>>() {
    141                 @Override
    142                 public Loader<List<BatteryInfo>> onCreateLoader(int i, Bundle bundle) {
    143                     return new DebugEstimatesLoader(getContext(), mStatsHelper);
    144                 }
    145 
    146                 @Override
    147                 public void onLoadFinished(Loader<List<BatteryInfo>> loader,
    148                         List<BatteryInfo> batteryInfos) {
    149                     updateViews(batteryInfos);
    150                 }
    151 
    152                 @Override
    153                 public void onLoaderReset(Loader<List<BatteryInfo>> loader) {
    154                 }
    155             };
    156 
    157     protected void updateViews(List<BatteryInfo> batteryInfos) {
    158         final BatteryMeterView batteryView = mBatteryLayoutPref
    159             .findViewById(R.id.battery_header_icon);
    160         final TextView percentRemaining =
    161             mBatteryLayoutPref.findViewById(R.id.battery_percent);
    162         final TextView summary1 = mBatteryLayoutPref.findViewById(R.id.summary1);
    163         final TextView summary2 = mBatteryLayoutPref.findViewById(R.id.summary2);
    164         BatteryInfo oldInfo = batteryInfos.get(0);
    165         BatteryInfo newInfo = batteryInfos.get(1);
    166         percentRemaining.setText(Utils.formatPercentage(oldInfo.batteryLevel));
    167 
    168         // set the text to the old estimate (copied from battery info). Note that this
    169         // can sometimes say 0 time remaining because battery stats requires the phone
    170         // be unplugged for a period of time before being willing ot make an estimate.
    171         summary1.setText(mPowerFeatureProvider.getOldEstimateDebugString(
    172             Formatter.formatShortElapsedTime(getContext(),
    173                 PowerUtil.convertUsToMs(oldInfo.remainingTimeUs))));
    174 
    175         // for this one we can just set the string directly
    176         summary2.setText(mPowerFeatureProvider.getEnhancedEstimateDebugString(
    177             Formatter.formatShortElapsedTime(getContext(),
    178                 PowerUtil.convertUsToMs(newInfo.remainingTimeUs))));
    179 
    180         batteryView.setBatteryLevel(oldInfo.batteryLevel);
    181         batteryView.setCharging(!oldInfo.discharging);
    182     }
    183 
    184     private LoaderManager.LoaderCallbacks<List<BatteryTip>> mBatteryTipsCallbacks =
    185             new LoaderManager.LoaderCallbacks<List<BatteryTip>>() {
    186 
    187                 @Override
    188                 public Loader<List<BatteryTip>> onCreateLoader(int id, Bundle args) {
    189                     return new BatteryTipLoader(getContext(), mStatsHelper);
    190                 }
    191 
    192                 @Override
    193                 public void onLoadFinished(Loader<List<BatteryTip>> loader,
    194                         List<BatteryTip> data) {
    195                     mBatteryTipPreferenceController.updateBatteryTips(data);
    196                 }
    197 
    198                 @Override
    199                 public void onLoaderReset(Loader<List<BatteryTip>> loader) {
    200 
    201                 }
    202             };
    203 
    204     @Override
    205     public void onCreate(Bundle icicle) {
    206         super.onCreate(icicle);
    207         setAnimationAllowed(true);
    208 
    209         initFeatureProvider();
    210         mBatteryLayoutPref = (LayoutPreference) findPreference(KEY_BATTERY_HEADER);
    211 
    212         mScreenUsagePref = (PowerGaugePreference) findPreference(KEY_SCREEN_USAGE);
    213         mLastFullChargePref = (PowerGaugePreference) findPreference(
    214                 KEY_TIME_SINCE_LAST_FULL_CHARGE);
    215         mFooterPreferenceMixin.createFooterPreference().setTitle(R.string.battery_footer_summary);
    216         mBatteryUtils = BatteryUtils.getInstance(getContext());
    217         mAnomalySparseArray = new SparseArray<>();
    218 
    219         restartBatteryInfoLoader();
    220         mBatteryTipPreferenceController.restoreInstanceState(icicle);
    221         updateBatteryTipFlag(icicle);
    222     }
    223 
    224     @Override
    225     public int getMetricsCategory() {
    226         return MetricsEvent.FUELGAUGE_POWER_USAGE_SUMMARY_V2;
    227     }
    228 
    229     @Override
    230     protected String getLogTag() {
    231         return TAG;
    232     }
    233 
    234     @Override
    235     protected int getPreferenceScreenResId() {
    236         return R.xml.power_usage_summary;
    237     }
    238 
    239     @Override
    240     protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
    241         final Lifecycle lifecycle = getLifecycle();
    242         final SettingsActivity activity = (SettingsActivity) getActivity();
    243         final List<AbstractPreferenceController> controllers = new ArrayList<>();
    244         mBatteryHeaderPreferenceController = new BatteryHeaderPreferenceController(
    245                 context, activity, this /* host */, lifecycle);
    246         controllers.add(mBatteryHeaderPreferenceController);
    247         mBatteryTipPreferenceController = new BatteryTipPreferenceController(context,
    248                 KEY_BATTERY_TIP, (SettingsActivity) getActivity(), this /* fragment */, this /*
    249                 BatteryTipListener */);
    250         controllers.add(mBatteryTipPreferenceController);
    251         controllers.add(new BatteryPercentagePreferenceController(context));
    252         return controllers;
    253     }
    254 
    255     @Override
    256     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    257         if (DEBUG) {
    258             menu.add(Menu.NONE, MENU_STATS_TYPE, Menu.NONE, R.string.menu_stats_total)
    259                     .setIcon(com.android.internal.R.drawable.ic_menu_info_details)
    260                     .setAlphabeticShortcut('t');
    261         }
    262 
    263         menu.add(Menu.NONE, MENU_ADVANCED_BATTERY, Menu.NONE, R.string.advanced_battery_title);
    264 
    265         super.onCreateOptionsMenu(menu, inflater);
    266     }
    267 
    268     @Override
    269     public int getHelpResource() {
    270         return R.string.help_url_battery;
    271     }
    272 
    273     @Override
    274     public boolean onOptionsItemSelected(MenuItem item) {
    275         switch (item.getItemId()) {
    276             case MENU_STATS_TYPE:
    277                 if (mStatsType == BatteryStats.STATS_SINCE_CHARGED) {
    278                     mStatsType = BatteryStats.STATS_SINCE_UNPLUGGED;
    279                 } else {
    280                     mStatsType = BatteryStats.STATS_SINCE_CHARGED;
    281                 }
    282                 refreshUi(BatteryUpdateType.MANUAL);
    283                 return true;
    284             case MENU_ADVANCED_BATTERY:
    285                 new SubSettingLauncher(getContext())
    286                         .setDestination(PowerUsageAdvanced.class.getName())
    287                         .setSourceMetricsCategory(getMetricsCategory())
    288                         .setTitle(R.string.advanced_battery_title)
    289                         .launch();
    290                 return true;
    291             default:
    292                 return super.onOptionsItemSelected(item);
    293         }
    294     }
    295 
    296     protected void refreshUi(@BatteryUpdateType int refreshType) {
    297         final Context context = getContext();
    298         if (context == null) {
    299             return;
    300         }
    301 
    302         // Skip BatteryTipLoader if device is rotated or only battery level change
    303         if (mNeedUpdateBatteryTip
    304                 && refreshType != BatteryUpdateType.BATTERY_LEVEL) {
    305             restartBatteryTipLoader();
    306         } else {
    307             mNeedUpdateBatteryTip = true;
    308         }
    309 
    310         // reload BatteryInfo and updateUI
    311         restartBatteryInfoLoader();
    312         updateLastFullChargePreference();
    313         mScreenUsagePref.setSubtitle(StringUtil.formatElapsedTime(getContext(),
    314                 mBatteryUtils.calculateScreenUsageTime(mStatsHelper), false));
    315     }
    316 
    317     @VisibleForTesting
    318     void restartBatteryTipLoader() {
    319         getLoaderManager().restartLoader(BATTERY_TIP_LOADER, Bundle.EMPTY, mBatteryTipsCallbacks);
    320     }
    321 
    322     @VisibleForTesting
    323     void setBatteryLayoutPreference(LayoutPreference layoutPreference) {
    324         mBatteryLayoutPref = layoutPreference;
    325     }
    326 
    327     @VisibleForTesting
    328     AnomalyDetectionPolicy getAnomalyDetectionPolicy() {
    329         return new AnomalyDetectionPolicy(getContext());
    330     }
    331 
    332     @VisibleForTesting
    333     void updateLastFullChargePreference() {
    334         if (mBatteryInfo != null && mBatteryInfo.averageTimeToDischarge
    335                 != Estimate.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN) {
    336             mLastFullChargePref.setTitle(R.string.battery_full_charge_last);
    337             mLastFullChargePref.setSubtitle(
    338                     StringUtil.formatElapsedTime(getContext(), mBatteryInfo.averageTimeToDischarge,
    339                             false /* withSeconds */));
    340         } else {
    341             final long lastFullChargeTime = mBatteryUtils.calculateLastFullChargeTime(mStatsHelper,
    342                     System.currentTimeMillis());
    343             mLastFullChargePref.setTitle(R.string.battery_last_full_charge);
    344             mLastFullChargePref.setSubtitle(
    345                     StringUtil.formatRelativeTime(getContext(), lastFullChargeTime,
    346                             false /* withSeconds */));
    347         }
    348     }
    349 
    350     @VisibleForTesting
    351     void showBothEstimates() {
    352         final Context context = getContext();
    353         if (context == null
    354                 || !mPowerFeatureProvider.isEnhancedBatteryPredictionEnabled(context)) {
    355             return;
    356         }
    357         getLoaderManager().restartLoader(DEBUG_INFO_LOADER, Bundle.EMPTY,
    358                 mBatteryInfoDebugLoaderCallbacks);
    359     }
    360 
    361     @VisibleForTesting
    362     void initFeatureProvider() {
    363         final Context context = getContext();
    364         mPowerFeatureProvider = FeatureFactory.getFactory(context)
    365                 .getPowerUsageFeatureProvider(context);
    366     }
    367 
    368     @VisibleForTesting
    369     void updateAnomalySparseArray(List<Anomaly> anomalies) {
    370         mAnomalySparseArray.clear();
    371         for (final Anomaly anomaly : anomalies) {
    372             if (mAnomalySparseArray.get(anomaly.uid) == null) {
    373                 mAnomalySparseArray.append(anomaly.uid, new ArrayList<>());
    374             }
    375             mAnomalySparseArray.get(anomaly.uid).add(anomaly);
    376         }
    377     }
    378 
    379     @VisibleForTesting
    380     void restartBatteryInfoLoader() {
    381         getLoaderManager().restartLoader(BATTERY_INFO_LOADER, Bundle.EMPTY,
    382                 mBatteryInfoLoaderCallbacks);
    383         if (mPowerFeatureProvider.isEstimateDebugEnabled()) {
    384             // Set long click action for summary to show debug info
    385             View header = mBatteryLayoutPref.findViewById(R.id.summary1);
    386             header.setOnLongClickListener(this);
    387         }
    388     }
    389 
    390     @VisibleForTesting
    391     void updateBatteryTipFlag(Bundle icicle) {
    392         mNeedUpdateBatteryTip = icicle == null || mBatteryTipPreferenceController.needUpdate();
    393     }
    394 
    395     @Override
    396     public boolean onLongClick(View view) {
    397         showBothEstimates();
    398         view.setOnLongClickListener(null);
    399         return true;
    400     }
    401 
    402     @Override
    403     protected void restartBatteryStatsLoader(@BatteryUpdateType int refreshType) {
    404         super.restartBatteryStatsLoader(refreshType);
    405         mBatteryHeaderPreferenceController.quickUpdateHeaderPreference();
    406     }
    407 
    408     @Override
    409     public void onSaveInstanceState(Bundle outState) {
    410         super.onSaveInstanceState(outState);
    411         mBatteryTipPreferenceController.saveInstanceState(outState);
    412     }
    413 
    414     @Override
    415     public void onBatteryTipHandled(BatteryTip batteryTip) {
    416         restartBatteryTipLoader();
    417     }
    418 
    419     private static class SummaryProvider implements SummaryLoader.SummaryProvider {
    420         private final Context mContext;
    421         private final SummaryLoader mLoader;
    422         private final BatteryBroadcastReceiver mBatteryBroadcastReceiver;
    423 
    424         private SummaryProvider(Context context, SummaryLoader loader) {
    425             mContext = context;
    426             mLoader = loader;
    427             mBatteryBroadcastReceiver = new BatteryBroadcastReceiver(mContext);
    428             mBatteryBroadcastReceiver.setBatteryChangedListener(type -> {
    429                 BatteryInfo.getBatteryInfo(mContext, new BatteryInfo.Callback() {
    430                     @Override
    431                     public void onBatteryInfoLoaded(BatteryInfo info) {
    432                         mLoader.setSummary(SummaryProvider.this, getDashboardLabel(mContext, info));
    433                     }
    434                 }, true /* shortString */);
    435             });
    436         }
    437 
    438         @Override
    439         public void setListening(boolean listening) {
    440             if (listening) {
    441                 mBatteryBroadcastReceiver.register();
    442             } else {
    443                 mBatteryBroadcastReceiver.unRegister();
    444             }
    445         }
    446     }
    447 
    448     @VisibleForTesting
    449     static CharSequence getDashboardLabel(Context context, BatteryInfo info) {
    450         CharSequence label;
    451         final BidiFormatter formatter = BidiFormatter.getInstance();
    452         if (info.remainingLabel == null) {
    453             label = info.batteryPercentString;
    454         } else {
    455             label = context.getString(R.string.power_remaining_settings_home_page,
    456                     formatter.unicodeWrap(info.batteryPercentString),
    457                     formatter.unicodeWrap(info.remainingLabel));
    458         }
    459         return label;
    460     }
    461 
    462     public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
    463             new BaseSearchIndexProvider() {
    464                 @Override
    465                 public List<SearchIndexableResource> getXmlResourcesToIndex(
    466                         Context context, boolean enabled) {
    467                     final SearchIndexableResource sir = new SearchIndexableResource(context);
    468                     sir.xmlResId = R.xml.power_usage_summary;
    469                     return Collections.singletonList(sir);
    470                 }
    471 
    472                 @Override
    473                 public List<String> getNonIndexableKeys(Context context) {
    474                     List<String> niks = super.getNonIndexableKeys(context);
    475                     niks.add(KEY_BATTERY_SAVER_SUMMARY);
    476                     return niks;
    477                 }
    478             };
    479 
    480     public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY
    481             = new SummaryLoader.SummaryProviderFactory() {
    482         @Override
    483         public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity,
    484                 SummaryLoader summaryLoader) {
    485             return new SummaryProvider(activity, summaryLoader);
    486         }
    487     };
    488 }
    489