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         mFooterPreferenceMixin.createFooterPreference().setTitle(R.string.battery_footer_summary);
219         mBatteryUtils = BatteryUtils.getInstance(getContext());
220 
221         restartBatteryInfoLoader();
222         mBatteryTipPreferenceController.restoreInstanceState(icicle);
223         updateBatteryTipFlag(icicle);
224     }
225 
226     @Override
getMetricsCategory()227     public int getMetricsCategory() {
228         return SettingsEnums.FUELGAUGE_POWER_USAGE_SUMMARY_V2;
229     }
230 
231     @Override
getLogTag()232     protected String getLogTag() {
233         return TAG;
234     }
235 
236     @Override
getPreferenceScreenResId()237     protected int getPreferenceScreenResId() {
238         return R.xml.power_usage_summary;
239     }
240 
241     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)242     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
243         if (DEBUG) {
244             menu.add(Menu.NONE, MENU_STATS_TYPE, Menu.NONE, R.string.menu_stats_total)
245                     .setIcon(com.android.internal.R.drawable.ic_menu_info_details)
246                     .setAlphabeticShortcut('t');
247         }
248 
249         menu.add(Menu.NONE, MENU_ADVANCED_BATTERY, Menu.NONE, R.string.advanced_battery_title);
250 
251         super.onCreateOptionsMenu(menu, inflater);
252     }
253 
254     @Override
getHelpResource()255     public int getHelpResource() {
256         return R.string.help_url_battery;
257     }
258 
259     @Override
onOptionsItemSelected(MenuItem item)260     public boolean onOptionsItemSelected(MenuItem item) {
261         switch (item.getItemId()) {
262             case MENU_STATS_TYPE:
263                 if (mStatsType == BatteryStats.STATS_SINCE_CHARGED) {
264                     mStatsType = BatteryStats.STATS_SINCE_UNPLUGGED;
265                 } else {
266                     mStatsType = BatteryStats.STATS_SINCE_CHARGED;
267                 }
268                 refreshUi(BatteryUpdateType.MANUAL);
269                 return true;
270             case MENU_ADVANCED_BATTERY:
271                 new SubSettingLauncher(getContext())
272                         .setDestination(PowerUsageAdvanced.class.getName())
273                         .setSourceMetricsCategory(getMetricsCategory())
274                         .setTitleRes(R.string.advanced_battery_title)
275                         .launch();
276                 return true;
277             default:
278                 return super.onOptionsItemSelected(item);
279         }
280     }
281 
refreshUi(@atteryUpdateType int refreshType)282     protected void refreshUi(@BatteryUpdateType int refreshType) {
283         final Context context = getContext();
284         if (context == null) {
285             return;
286         }
287 
288         // Skip BatteryTipLoader if device is rotated or only battery level change
289         if (mNeedUpdateBatteryTip
290                 && refreshType != BatteryUpdateType.BATTERY_LEVEL) {
291             restartBatteryTipLoader();
292         } else {
293             mNeedUpdateBatteryTip = true;
294         }
295 
296         // reload BatteryInfo and updateUI
297         restartBatteryInfoLoader();
298         updateLastFullChargePreference();
299         mScreenUsagePref.setSubtitle(StringUtil.formatElapsedTime(getContext(),
300                 mBatteryUtils.calculateScreenUsageTime(mStatsHelper), false));
301     }
302 
303     @VisibleForTesting
restartBatteryTipLoader()304     void restartBatteryTipLoader() {
305         getLoaderManager().restartLoader(BATTERY_TIP_LOADER, Bundle.EMPTY, mBatteryTipsCallbacks);
306     }
307 
308     @VisibleForTesting
setBatteryLayoutPreference(LayoutPreference layoutPreference)309     void setBatteryLayoutPreference(LayoutPreference layoutPreference) {
310         mBatteryLayoutPref = layoutPreference;
311     }
312 
313     @VisibleForTesting
updateLastFullChargePreference()314     void updateLastFullChargePreference() {
315         if (mBatteryInfo != null && mBatteryInfo.averageTimeToDischarge
316                 != Estimate.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN) {
317             mLastFullChargePref.setTitle(R.string.battery_full_charge_last);
318             mLastFullChargePref.setSubtitle(
319                     StringUtil.formatElapsedTime(getContext(), mBatteryInfo.averageTimeToDischarge,
320                             false /* withSeconds */));
321         } else {
322             final long lastFullChargeTime = mBatteryUtils.calculateLastFullChargeTime(mStatsHelper,
323                     System.currentTimeMillis());
324             mLastFullChargePref.setTitle(R.string.battery_last_full_charge);
325             mLastFullChargePref.setSubtitle(
326                     StringUtil.formatRelativeTime(getContext(), lastFullChargeTime,
327                             false /* withSeconds */));
328         }
329     }
330 
331     @VisibleForTesting
showBothEstimates()332     void showBothEstimates() {
333         final Context context = getContext();
334         if (context == null
335                 || !mPowerFeatureProvider.isEnhancedBatteryPredictionEnabled(context)) {
336             return;
337         }
338         getLoaderManager().restartLoader(DEBUG_INFO_LOADER, Bundle.EMPTY,
339                 mBatteryInfoDebugLoaderCallbacks);
340     }
341 
342     @VisibleForTesting
initFeatureProvider()343     void initFeatureProvider() {
344         final Context context = getContext();
345         mPowerFeatureProvider = FeatureFactory.getFactory(context)
346                 .getPowerUsageFeatureProvider(context);
347     }
348 
349     @VisibleForTesting
restartBatteryInfoLoader()350     void restartBatteryInfoLoader() {
351         getLoaderManager().restartLoader(BATTERY_INFO_LOADER, Bundle.EMPTY,
352                 mBatteryInfoLoaderCallbacks);
353         if (mPowerFeatureProvider.isEstimateDebugEnabled()) {
354             // Set long click action for summary to show debug info
355             View header = mBatteryLayoutPref.findViewById(R.id.summary1);
356             header.setOnLongClickListener(this);
357         }
358     }
359 
360     @VisibleForTesting
updateBatteryTipFlag(Bundle icicle)361     void updateBatteryTipFlag(Bundle icicle) {
362         mNeedUpdateBatteryTip = icicle == null || mBatteryTipPreferenceController.needUpdate();
363     }
364 
365     @Override
onLongClick(View view)366     public boolean onLongClick(View view) {
367         showBothEstimates();
368         view.setOnLongClickListener(null);
369         return true;
370     }
371 
372     @Override
restartBatteryStatsLoader(@atteryUpdateType int refreshType)373     protected void restartBatteryStatsLoader(@BatteryUpdateType int refreshType) {
374         super.restartBatteryStatsLoader(refreshType);
375         mBatteryHeaderPreferenceController.quickUpdateHeaderPreference();
376     }
377 
378     @Override
onSaveInstanceState(Bundle outState)379     public void onSaveInstanceState(Bundle outState) {
380         super.onSaveInstanceState(outState);
381         mBatteryTipPreferenceController.saveInstanceState(outState);
382     }
383 
384     @Override
onBatteryTipHandled(BatteryTip batteryTip)385     public void onBatteryTipHandled(BatteryTip batteryTip) {
386         restartBatteryTipLoader();
387     }
388 
389 
390     public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
391             new BaseSearchIndexProvider() {
392                 @Override
393                 public List<SearchIndexableResource> getXmlResourcesToIndex(
394                         Context context, boolean enabled) {
395                     final SearchIndexableResource sir = new SearchIndexableResource(context);
396                     sir.xmlResId = R.xml.power_usage_summary;
397                     return Collections.singletonList(sir);
398                 }
399             };
400 }
401