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