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.settings.SettingsEnums;
22 import android.content.Context;
23 import android.database.ContentObserver;
24 import android.net.Uri;
25 import android.os.BatteryStats;
26 import android.os.Bundle;
27 import android.os.Handler;
28 import android.provider.SearchIndexableResource;
29 import android.provider.Settings.Global;
30 import android.text.format.Formatter;
31 import android.view.Menu;
32 import android.view.MenuInflater;
33 import android.view.MenuItem;
34 import android.view.View;
35 import android.view.View.OnLongClickListener;
36 import android.widget.TextView;
37 
38 import androidx.annotation.VisibleForTesting;
39 import androidx.loader.app.LoaderManager;
40 import androidx.loader.app.LoaderManager.LoaderCallbacks;
41 import androidx.loader.content.Loader;
42 
43 import com.android.settings.R;
44 import com.android.settings.SettingsActivity;
45 import com.android.settings.Utils;
46 import com.android.settings.core.SubSettingLauncher;
47 import com.android.settings.fuelgauge.batterytip.BatteryTipLoader;
48 import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController;
49 import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
50 import com.android.settings.overlay.FeatureFactory;
51 import com.android.settings.search.BaseSearchIndexProvider;
52 import com.android.settingslib.fuelgauge.EstimateKt;
53 import com.android.settingslib.search.SearchIndexable;
54 import com.android.settingslib.utils.PowerUtil;
55 import com.android.settingslib.utils.StringUtil;
56 import com.android.settingslib.widget.LayoutPreference;
57 
58 import java.util.Collections;
59 import java.util.List;
60 
61 /**
62  * Displays a list of apps and subsystems that consume power, ordered by how much power was
63  * consumed since the last time it was unplugged.
64  */
65 @SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
66 public class PowerUsageSummary extends PowerUsageBase implements OnLongClickListener,
67         BatteryTipPreferenceController.BatteryTipListener {
68 
69     static final String TAG = "PowerUsageSummary";
70 
71     private static final boolean DEBUG = false;
72     private static final String KEY_BATTERY_HEADER = "battery_header";
73 
74     private static final String KEY_SCREEN_USAGE = "screen_usage";
75     private static final String KEY_TIME_SINCE_LAST_FULL_CHARGE = "last_full_charge";
76     private static final String KEY_BATTERY_SAVER_SUMMARY = "battery_saver_summary";
77 
78     @VisibleForTesting
79     static final int BATTERY_INFO_LOADER = 1;
80     @VisibleForTesting
81     static final int BATTERY_TIP_LOADER = 2;
82     @VisibleForTesting
83     static final int MENU_STATS_TYPE = Menu.FIRST;
84     @VisibleForTesting
85     static final int MENU_ADVANCED_BATTERY = Menu.FIRST + 1;
86     public static final int DEBUG_INFO_LOADER = 3;
87 
88     @VisibleForTesting
89     PowerGaugePreference mScreenUsagePref;
90     @VisibleForTesting
91     PowerGaugePreference mLastFullChargePref;
92     @VisibleForTesting
93     PowerUsageFeatureProvider mPowerFeatureProvider;
94     @VisibleForTesting
95     BatteryUtils mBatteryUtils;
96     @VisibleForTesting
97     LayoutPreference mBatteryLayoutPref;
98     @VisibleForTesting
99     BatteryInfo mBatteryInfo;
100 
101     @VisibleForTesting
102     BatteryHeaderPreferenceController mBatteryHeaderPreferenceController;
103     @VisibleForTesting
104     boolean mNeedUpdateBatteryTip;
105     @VisibleForTesting
106     BatteryTipPreferenceController mBatteryTipPreferenceController;
107     private int mStatsType = BatteryStats.STATS_SINCE_CHARGED;
108 
109     @VisibleForTesting
110     final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) {
111         @Override
112         public void onChange(boolean selfChange, Uri uri) {
113             restartBatteryInfoLoader();
114         }
115     };
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 
updateViews(List<BatteryInfo> batteryInfos)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         BatteryInfo oldInfo = batteryInfos.get(0);
164         BatteryInfo newInfo = batteryInfos.get(1);
165         percentRemaining.setText(Utils.formatPercentage(oldInfo.batteryLevel));
166 
167         // set the text to the old estimate (copied from battery info). Note that this
168         // can sometimes say 0 time remaining because battery stats requires the phone
169         // be unplugged for a period of time before being willing ot make an estimate.
170         final String OldEstimateString = mPowerFeatureProvider.getOldEstimateDebugString(
171                 Formatter.formatShortElapsedTime(getContext(),
172                         PowerUtil.convertUsToMs(oldInfo.remainingTimeUs)));
173         final String NewEstimateString = mPowerFeatureProvider.getEnhancedEstimateDebugString(
174                 Formatter.formatShortElapsedTime(getContext(),
175                         PowerUtil.convertUsToMs(newInfo.remainingTimeUs)));
176         summary1.setText(OldEstimateString + "\n" + NewEstimateString);
177 
178         batteryView.setBatteryLevel(oldInfo.batteryLevel);
179         batteryView.setCharging(!oldInfo.discharging);
180     }
181 
182     private LoaderManager.LoaderCallbacks<List<BatteryTip>> mBatteryTipsCallbacks =
183             new LoaderManager.LoaderCallbacks<List<BatteryTip>>() {
184 
185                 @Override
186                 public Loader<List<BatteryTip>> onCreateLoader(int id, Bundle args) {
187                     return new BatteryTipLoader(getContext(), mStatsHelper);
188                 }
189 
190                 @Override
191                 public void onLoadFinished(Loader<List<BatteryTip>> loader,
192                         List<BatteryTip> data) {
193                     mBatteryTipPreferenceController.updateBatteryTips(data);
194                 }
195 
196                 @Override
197                 public void onLoaderReset(Loader<List<BatteryTip>> loader) {
198 
199                 }
200             };
201 
202     @Override
onAttach(Context context)203     public void onAttach(Context context) {
204         super.onAttach(context);
205         final SettingsActivity activity = (SettingsActivity) getActivity();
206 
207         mBatteryHeaderPreferenceController = use(BatteryHeaderPreferenceController.class);
208         mBatteryHeaderPreferenceController.setActivity(activity);
209         mBatteryHeaderPreferenceController.setFragment(this);
210         mBatteryHeaderPreferenceController.setLifecycle(getSettingsLifecycle());
211 
212         mBatteryTipPreferenceController = use(BatteryTipPreferenceController.class);
213         mBatteryTipPreferenceController.setActivity(activity);
214         mBatteryTipPreferenceController.setFragment(this);
215         mBatteryTipPreferenceController.setBatteryTipListener(this::onBatteryTipHandled);
216     }
217 
218     @Override
onCreate(Bundle icicle)219     public void onCreate(Bundle icicle) {
220         super.onCreate(icicle);
221         setAnimationAllowed(true);
222 
223         initFeatureProvider();
224         mBatteryLayoutPref = (LayoutPreference) findPreference(KEY_BATTERY_HEADER);
225 
226         mScreenUsagePref = (PowerGaugePreference) findPreference(KEY_SCREEN_USAGE);
227         mLastFullChargePref = (PowerGaugePreference) findPreference(
228                 KEY_TIME_SINCE_LAST_FULL_CHARGE);
229         mFooterPreferenceMixin.createFooterPreference().setTitle(R.string.battery_footer_summary);
230         mBatteryUtils = BatteryUtils.getInstance(getContext());
231 
232         restartBatteryInfoLoader();
233         mBatteryTipPreferenceController.restoreInstanceState(icicle);
234         updateBatteryTipFlag(icicle);
235     }
236 
237     @Override
onResume()238     public void onResume() {
239         super.onResume();
240         getContentResolver().registerContentObserver(
241                 Global.getUriFor(Global.BATTERY_ESTIMATES_LAST_UPDATE_TIME),
242                 false,
243                 mSettingsObserver);
244     }
245 
246     @Override
onPause()247     public void onPause() {
248         getContentResolver().unregisterContentObserver(mSettingsObserver);
249         super.onPause();
250     }
251 
252     @Override
getMetricsCategory()253     public int getMetricsCategory() {
254         return SettingsEnums.FUELGAUGE_POWER_USAGE_SUMMARY_V2;
255     }
256 
257     @Override
getLogTag()258     protected String getLogTag() {
259         return TAG;
260     }
261 
262     @Override
getPreferenceScreenResId()263     protected int getPreferenceScreenResId() {
264         return R.xml.power_usage_summary;
265     }
266 
267     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)268     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
269         if (DEBUG) {
270             menu.add(Menu.NONE, MENU_STATS_TYPE, Menu.NONE, R.string.menu_stats_total)
271                     .setIcon(com.android.internal.R.drawable.ic_menu_info_details)
272                     .setAlphabeticShortcut('t');
273         }
274 
275         menu.add(Menu.NONE, MENU_ADVANCED_BATTERY, Menu.NONE, R.string.advanced_battery_title);
276 
277         super.onCreateOptionsMenu(menu, inflater);
278     }
279 
280     @Override
getHelpResource()281     public int getHelpResource() {
282         return R.string.help_url_battery;
283     }
284 
285     @Override
onOptionsItemSelected(MenuItem item)286     public boolean onOptionsItemSelected(MenuItem item) {
287         switch (item.getItemId()) {
288             case MENU_STATS_TYPE:
289                 if (mStatsType == BatteryStats.STATS_SINCE_CHARGED) {
290                     mStatsType = BatteryStats.STATS_SINCE_UNPLUGGED;
291                 } else {
292                     mStatsType = BatteryStats.STATS_SINCE_CHARGED;
293                 }
294                 refreshUi(BatteryUpdateType.MANUAL);
295                 return true;
296             case MENU_ADVANCED_BATTERY:
297                 new SubSettingLauncher(getContext())
298                         .setDestination(PowerUsageAdvanced.class.getName())
299                         .setSourceMetricsCategory(getMetricsCategory())
300                         .setTitleRes(R.string.advanced_battery_title)
301                         .launch();
302                 return true;
303             default:
304                 return super.onOptionsItemSelected(item);
305         }
306     }
307 
refreshUi(@atteryUpdateType int refreshType)308     protected void refreshUi(@BatteryUpdateType int refreshType) {
309         final Context context = getContext();
310         if (context == null) {
311             return;
312         }
313 
314         // Skip BatteryTipLoader if device is rotated or only battery level change
315         if (mNeedUpdateBatteryTip
316                 && refreshType != BatteryUpdateType.BATTERY_LEVEL) {
317             restartBatteryTipLoader();
318         } else {
319             mNeedUpdateBatteryTip = true;
320         }
321 
322         // reload BatteryInfo and updateUI
323         restartBatteryInfoLoader();
324         updateLastFullChargePreference();
325         mScreenUsagePref.setSubtitle(StringUtil.formatElapsedTime(getContext(),
326                 mBatteryUtils.calculateScreenUsageTime(mStatsHelper), false));
327     }
328 
329     @VisibleForTesting
restartBatteryTipLoader()330     void restartBatteryTipLoader() {
331         getLoaderManager().restartLoader(BATTERY_TIP_LOADER, Bundle.EMPTY, mBatteryTipsCallbacks);
332     }
333 
334     @VisibleForTesting
setBatteryLayoutPreference(LayoutPreference layoutPreference)335     void setBatteryLayoutPreference(LayoutPreference layoutPreference) {
336         mBatteryLayoutPref = layoutPreference;
337     }
338 
339     @VisibleForTesting
updateLastFullChargePreference()340     void updateLastFullChargePreference() {
341         if (mBatteryInfo != null && mBatteryInfo.averageTimeToDischarge
342                 != EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN) {
343             mLastFullChargePref.setTitle(R.string.battery_full_charge_last);
344             mLastFullChargePref.setSubtitle(
345                     StringUtil.formatElapsedTime(getContext(), mBatteryInfo.averageTimeToDischarge,
346                             false /* withSeconds */));
347         } else {
348             final long lastFullChargeTime = mBatteryUtils.calculateLastFullChargeTime(mStatsHelper,
349                     System.currentTimeMillis());
350             mLastFullChargePref.setTitle(R.string.battery_last_full_charge);
351             mLastFullChargePref.setSubtitle(
352                     StringUtil.formatRelativeTime(getContext(), lastFullChargeTime,
353                             false /* withSeconds */));
354         }
355     }
356 
357     @VisibleForTesting
showBothEstimates()358     void showBothEstimates() {
359         final Context context = getContext();
360         if (context == null
361                 || !mPowerFeatureProvider.isEnhancedBatteryPredictionEnabled(context)) {
362             return;
363         }
364         getLoaderManager().restartLoader(DEBUG_INFO_LOADER, Bundle.EMPTY,
365                 mBatteryInfoDebugLoaderCallbacks);
366     }
367 
368     @VisibleForTesting
initFeatureProvider()369     void initFeatureProvider() {
370         final Context context = getContext();
371         mPowerFeatureProvider = FeatureFactory.getFactory(context)
372                 .getPowerUsageFeatureProvider(context);
373     }
374 
375     @VisibleForTesting
restartBatteryInfoLoader()376     void restartBatteryInfoLoader() {
377         if (getContext() == null) {
378             return;
379         }
380         getLoaderManager().restartLoader(BATTERY_INFO_LOADER, Bundle.EMPTY,
381                 mBatteryInfoLoaderCallbacks);
382         if (mPowerFeatureProvider.isEstimateDebugEnabled()) {
383             // Set long click action for summary to show debug info
384             View header = mBatteryLayoutPref.findViewById(R.id.summary1);
385             header.setOnLongClickListener(this);
386         }
387     }
388 
389     @VisibleForTesting
updateBatteryTipFlag(Bundle icicle)390     void updateBatteryTipFlag(Bundle icicle) {
391         mNeedUpdateBatteryTip = icicle == null || mBatteryTipPreferenceController.needUpdate();
392     }
393 
394     @Override
onLongClick(View view)395     public boolean onLongClick(View view) {
396         showBothEstimates();
397         view.setOnLongClickListener(null);
398         return true;
399     }
400 
401     @Override
restartBatteryStatsLoader(@atteryUpdateType int refreshType)402     protected void restartBatteryStatsLoader(@BatteryUpdateType int refreshType) {
403         super.restartBatteryStatsLoader(refreshType);
404         mBatteryHeaderPreferenceController.quickUpdateHeaderPreference();
405     }
406 
407     @Override
onSaveInstanceState(Bundle outState)408     public void onSaveInstanceState(Bundle outState) {
409         super.onSaveInstanceState(outState);
410         mBatteryTipPreferenceController.saveInstanceState(outState);
411     }
412 
413     @Override
onBatteryTipHandled(BatteryTip batteryTip)414     public void onBatteryTipHandled(BatteryTip batteryTip) {
415         restartBatteryTipLoader();
416     }
417 
418 
419     public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
420             new BaseSearchIndexProvider() {
421                 @Override
422                 public List<SearchIndexableResource> getXmlResourcesToIndex(
423                         Context context, boolean enabled) {
424                     final SearchIndexableResource sir = new SearchIndexableResource(context);
425                     sir.xmlResId = R.xml.power_usage_summary;
426                     return Collections.singletonList(sir);
427                 }
428             };
429 }
430