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