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