Many features are using the enhanced estimates but the copy for them has gotten out of sync. This CL moves shared strings between Settings and SysUI to SettingsLib and also updates features that use the strings to have consistent behavior/text. Test: Robotests Bug: 65656091 Bug: 66909350 Bug: 67469159 Change-Id: Ie5ef1ed65429ca9805cff374f1439e5d61eb6591
481 lines
20 KiB
Java
481 lines
20 KiB
Java
/*
|
|
* Copyright (C) 2017 The Android Open Source Project
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*
|
|
*/
|
|
|
|
package com.android.settings.fuelgauge;
|
|
|
|
import android.app.Activity;
|
|
import android.content.Context;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.os.BatteryStats;
|
|
import android.os.Handler;
|
|
import android.os.Message;
|
|
import android.os.Process;
|
|
import android.os.UserHandle;
|
|
import android.os.UserManager;
|
|
import android.support.annotation.VisibleForTesting;
|
|
import android.support.v14.preference.PreferenceFragment;
|
|
import android.support.v7.preference.Preference;
|
|
import android.support.v7.preference.PreferenceGroup;
|
|
import android.support.v7.preference.PreferenceScreen;
|
|
import android.text.TextUtils;
|
|
import android.text.format.DateUtils;
|
|
import android.util.ArrayMap;
|
|
import android.util.FeatureFlagUtils;
|
|
import android.util.Log;
|
|
import android.util.SparseArray;
|
|
|
|
import com.android.internal.os.BatterySipper;
|
|
import com.android.internal.os.BatterySipper.DrainType;
|
|
import com.android.internal.os.BatteryStatsHelper;
|
|
import com.android.internal.os.PowerProfile;
|
|
import com.android.settings.R;
|
|
import com.android.settings.SettingsActivity;
|
|
import com.android.settings.core.FeatureFlags;
|
|
import com.android.settings.core.PreferenceControllerMixin;
|
|
import com.android.settings.fuelgauge.anomaly.Anomaly;
|
|
import com.android.settingslib.core.AbstractPreferenceController;
|
|
import com.android.settingslib.core.lifecycle.Lifecycle;
|
|
import com.android.settingslib.core.lifecycle.LifecycleObserver;
|
|
import com.android.settingslib.core.lifecycle.events.OnDestroy;
|
|
import com.android.settingslib.core.lifecycle.events.OnPause;
|
|
|
|
import com.android.settingslib.utils.StringUtil;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* Controller that update the battery header view
|
|
*/
|
|
public class BatteryAppListPreferenceController extends AbstractPreferenceController
|
|
implements PreferenceControllerMixin, LifecycleObserver, OnPause, OnDestroy {
|
|
private static final boolean USE_FAKE_DATA = true;
|
|
private static final int MAX_ITEMS_TO_LIST = USE_FAKE_DATA ? 30 : 10;
|
|
private static final int MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP = 10;
|
|
private static final int STATS_TYPE = BatteryStats.STATS_SINCE_CHARGED;
|
|
|
|
private final String mPreferenceKey;
|
|
@VisibleForTesting
|
|
PreferenceGroup mAppListGroup;
|
|
private BatteryStatsHelper mBatteryStatsHelper;
|
|
private ArrayMap<String, Preference> mPreferenceCache;
|
|
@VisibleForTesting
|
|
BatteryUtils mBatteryUtils;
|
|
private UserManager mUserManager;
|
|
private SettingsActivity mActivity;
|
|
private PreferenceFragment mFragment;
|
|
private Context mPrefContext;
|
|
SparseArray<List<Anomaly>> mAnomalySparseArray;
|
|
|
|
private Handler mHandler = new Handler() {
|
|
@Override
|
|
public void handleMessage(Message msg) {
|
|
switch (msg.what) {
|
|
case BatteryEntry.MSG_UPDATE_NAME_ICON:
|
|
BatteryEntry entry = (BatteryEntry) msg.obj;
|
|
PowerGaugePreference pgp =
|
|
(PowerGaugePreference) mAppListGroup.findPreference(
|
|
Integer.toString(entry.sipper.uidObj.getUid()));
|
|
if (pgp != null) {
|
|
final int userId = UserHandle.getUserId(entry.sipper.getUid());
|
|
final UserHandle userHandle = new UserHandle(userId);
|
|
pgp.setIcon(mUserManager.getBadgedIconForUser(entry.getIcon(), userHandle));
|
|
pgp.setTitle(entry.name);
|
|
if (entry.sipper.drainType == DrainType.APP) {
|
|
pgp.setContentDescription(entry.name);
|
|
}
|
|
}
|
|
break;
|
|
case BatteryEntry.MSG_REPORT_FULLY_DRAWN:
|
|
Activity activity = mActivity;
|
|
if (activity != null) {
|
|
activity.reportFullyDrawn();
|
|
}
|
|
break;
|
|
}
|
|
super.handleMessage(msg);
|
|
}
|
|
};
|
|
|
|
public BatteryAppListPreferenceController(Context context, String preferenceKey,
|
|
Lifecycle lifecycle, SettingsActivity activity, PreferenceFragment fragment) {
|
|
super(context);
|
|
|
|
if (lifecycle != null) {
|
|
lifecycle.addObserver(this);
|
|
}
|
|
|
|
mPreferenceKey = preferenceKey;
|
|
mBatteryUtils = BatteryUtils.getInstance(context);
|
|
mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
|
|
mActivity = activity;
|
|
mFragment = fragment;
|
|
}
|
|
|
|
@Override
|
|
public void onPause() {
|
|
BatteryEntry.stopRequestQueue();
|
|
mHandler.removeMessages(BatteryEntry.MSG_UPDATE_NAME_ICON);
|
|
}
|
|
|
|
@Override
|
|
public void onDestroy() {
|
|
if (mActivity.isChangingConfigurations()) {
|
|
BatteryEntry.clearUidCache();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void displayPreference(PreferenceScreen screen) {
|
|
super.displayPreference(screen);
|
|
mPrefContext = screen.getContext();
|
|
mAppListGroup = (PreferenceGroup) screen.findPreference(mPreferenceKey);
|
|
}
|
|
|
|
@Override
|
|
public boolean isAvailable() {
|
|
return FeatureFlagUtils.isEnabled(mContext, FeatureFlags.BATTERY_DISPLAY_APP_LIST);
|
|
}
|
|
|
|
@Override
|
|
public String getPreferenceKey() {
|
|
return mPreferenceKey;
|
|
}
|
|
|
|
@Override
|
|
public boolean handlePreferenceTreeClick(Preference preference) {
|
|
if (preference instanceof PowerGaugePreference) {
|
|
PowerGaugePreference pgp = (PowerGaugePreference) preference;
|
|
BatteryEntry entry = pgp.getInfo();
|
|
AdvancedPowerUsageDetail.startBatteryDetailPage(mActivity,
|
|
mFragment, mBatteryStatsHelper, STATS_TYPE, entry, pgp.getPercent(),
|
|
mAnomalySparseArray != null ? mAnomalySparseArray.get(entry.sipper.getUid())
|
|
: null);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void refreshAnomalyIcon(final SparseArray<List<Anomaly>> anomalySparseArray) {
|
|
if (!isAvailable()) {
|
|
return;
|
|
}
|
|
mAnomalySparseArray = anomalySparseArray;
|
|
for (int i = 0, size = anomalySparseArray.size(); i < size; i++) {
|
|
final String key = extractKeyFromUid(anomalySparseArray.keyAt(i));
|
|
final PowerGaugePreference pref = (PowerGaugePreference) mAppListGroup.findPreference(
|
|
key);
|
|
if (pref != null) {
|
|
pref.shouldShowAnomalyIcon(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void refreshAppListGroup(BatteryStatsHelper statsHelper, boolean showAllApps,
|
|
CharSequence timeSequence) {
|
|
if (!isAvailable()) {
|
|
return;
|
|
}
|
|
mBatteryStatsHelper = statsHelper;
|
|
final int resId = showAllApps ? R.string.power_usage_list_summary_device
|
|
: R.string.power_usage_list_summary;
|
|
mAppListGroup.setTitle(TextUtils.expandTemplate(mContext.getText(resId), timeSequence));
|
|
|
|
final PowerProfile powerProfile = statsHelper.getPowerProfile();
|
|
final BatteryStats stats = statsHelper.getStats();
|
|
final double averagePower = powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL);
|
|
boolean addedSome = false;
|
|
final int dischargeAmount = USE_FAKE_DATA ? 5000
|
|
: stats != null ? stats.getDischargeAmount(STATS_TYPE) : 0;
|
|
|
|
cacheRemoveAllPrefs(mAppListGroup);
|
|
mAppListGroup.setOrderingAsAdded(false);
|
|
|
|
if (averagePower >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP || USE_FAKE_DATA) {
|
|
final List<BatterySipper> usageList = getCoalescedUsageList(
|
|
USE_FAKE_DATA ? getFakeStats() : statsHelper.getUsageList());
|
|
double hiddenPowerMah = showAllApps ? 0 :
|
|
mBatteryUtils.removeHiddenBatterySippers(usageList);
|
|
mBatteryUtils.sortUsageList(usageList);
|
|
|
|
final int numSippers = usageList.size();
|
|
for (int i = 0; i < numSippers; i++) {
|
|
final BatterySipper sipper = usageList.get(i);
|
|
double totalPower = USE_FAKE_DATA ? 4000 : statsHelper.getTotalPower();
|
|
|
|
final double percentOfTotal = mBatteryUtils.calculateBatteryPercent(
|
|
sipper.totalPowerMah, totalPower, hiddenPowerMah, dischargeAmount);
|
|
|
|
if (((int) (percentOfTotal + .5)) < 1) {
|
|
continue;
|
|
}
|
|
if (shouldHideSipper(sipper)) {
|
|
continue;
|
|
}
|
|
final UserHandle userHandle = new UserHandle(UserHandle.getUserId(sipper.getUid()));
|
|
final BatteryEntry entry = new BatteryEntry(mActivity, mHandler, mUserManager,
|
|
sipper);
|
|
final Drawable badgedIcon = mUserManager.getBadgedIconForUser(entry.getIcon(),
|
|
userHandle);
|
|
final CharSequence contentDescription = mUserManager.getBadgedLabelForUser(
|
|
entry.getLabel(),
|
|
userHandle);
|
|
|
|
final String key = extractKeyFromSipper(sipper);
|
|
PowerGaugePreference pref = (PowerGaugePreference) getCachedPreference(key);
|
|
if (pref == null) {
|
|
pref = new PowerGaugePreference(mPrefContext, badgedIcon,
|
|
contentDescription, entry);
|
|
pref.setKey(key);
|
|
}
|
|
sipper.percent = percentOfTotal;
|
|
pref.setTitle(entry.getLabel());
|
|
pref.setOrder(i + 1);
|
|
pref.setPercent(percentOfTotal);
|
|
pref.shouldShowAnomalyIcon(false);
|
|
if (sipper.usageTimeMs == 0 && sipper.drainType == DrainType.APP) {
|
|
sipper.usageTimeMs = mBatteryUtils.getProcessTimeMs(
|
|
BatteryUtils.StatusType.FOREGROUND, sipper.uidObj, STATS_TYPE);
|
|
}
|
|
setUsageSummary(pref, sipper);
|
|
addedSome = true;
|
|
mAppListGroup.addPreference(pref);
|
|
if (mAppListGroup.getPreferenceCount() - getCachedCount()
|
|
> (MAX_ITEMS_TO_LIST + 1)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!addedSome) {
|
|
addNotAvailableMessage();
|
|
}
|
|
removeCachedPrefs(mAppListGroup);
|
|
|
|
BatteryEntry.startRequestQueue();
|
|
}
|
|
|
|
/**
|
|
* We want to coalesce some UIDs. For example, dex2oat runs under a shared gid that
|
|
* exists for all users of the same app. We detect this case and merge the power use
|
|
* for dex2oat to the device OWNER's use of the app.
|
|
*
|
|
* @return A sorted list of apps using power.
|
|
*/
|
|
private List<BatterySipper> getCoalescedUsageList(final List<BatterySipper> sippers) {
|
|
final SparseArray<BatterySipper> uidList = new SparseArray<>();
|
|
|
|
final ArrayList<BatterySipper> results = new ArrayList<>();
|
|
final int numSippers = sippers.size();
|
|
for (int i = 0; i < numSippers; i++) {
|
|
BatterySipper sipper = sippers.get(i);
|
|
if (sipper.getUid() > 0) {
|
|
int realUid = sipper.getUid();
|
|
|
|
// Check if this UID is a shared GID. If so, we combine it with the OWNER's
|
|
// actual app UID.
|
|
if (isSharedGid(sipper.getUid())) {
|
|
realUid = UserHandle.getUid(UserHandle.USER_SYSTEM,
|
|
UserHandle.getAppIdFromSharedAppGid(sipper.getUid()));
|
|
}
|
|
|
|
// Check if this UID is a system UID (mediaserver, logd, nfc, drm, etc).
|
|
if (isSystemUid(realUid)
|
|
&& !"mediaserver".equals(sipper.packageWithHighestDrain)) {
|
|
// Use the system UID for all UIDs running in their own sandbox that
|
|
// are not apps. We exclude mediaserver because we already are expected to
|
|
// report that as a separate item.
|
|
realUid = Process.SYSTEM_UID;
|
|
}
|
|
|
|
if (realUid != sipper.getUid()) {
|
|
// Replace the BatterySipper with a new one with the real UID set.
|
|
BatterySipper newSipper = new BatterySipper(sipper.drainType,
|
|
new FakeUid(realUid), 0.0);
|
|
newSipper.add(sipper);
|
|
newSipper.packageWithHighestDrain = sipper.packageWithHighestDrain;
|
|
newSipper.mPackages = sipper.mPackages;
|
|
sipper = newSipper;
|
|
}
|
|
|
|
int index = uidList.indexOfKey(realUid);
|
|
if (index < 0) {
|
|
// New entry.
|
|
uidList.put(realUid, sipper);
|
|
} else {
|
|
// Combine BatterySippers if we already have one with this UID.
|
|
final BatterySipper existingSipper = uidList.valueAt(index);
|
|
existingSipper.add(sipper);
|
|
if (existingSipper.packageWithHighestDrain == null
|
|
&& sipper.packageWithHighestDrain != null) {
|
|
existingSipper.packageWithHighestDrain = sipper.packageWithHighestDrain;
|
|
}
|
|
|
|
final int existingPackageLen = existingSipper.mPackages != null ?
|
|
existingSipper.mPackages.length : 0;
|
|
final int newPackageLen = sipper.mPackages != null ?
|
|
sipper.mPackages.length : 0;
|
|
if (newPackageLen > 0) {
|
|
String[] newPackages = new String[existingPackageLen + newPackageLen];
|
|
if (existingPackageLen > 0) {
|
|
System.arraycopy(existingSipper.mPackages, 0, newPackages, 0,
|
|
existingPackageLen);
|
|
}
|
|
System.arraycopy(sipper.mPackages, 0, newPackages, existingPackageLen,
|
|
newPackageLen);
|
|
existingSipper.mPackages = newPackages;
|
|
}
|
|
}
|
|
} else {
|
|
results.add(sipper);
|
|
}
|
|
}
|
|
|
|
final int numUidSippers = uidList.size();
|
|
for (int i = 0; i < numUidSippers; i++) {
|
|
results.add(uidList.valueAt(i));
|
|
}
|
|
|
|
// The sort order must have changed, so re-sort based on total power use.
|
|
mBatteryUtils.sortUsageList(results);
|
|
return results;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
void setUsageSummary(Preference preference, BatterySipper sipper) {
|
|
// Only show summary when usage time is longer than one minute
|
|
final long usageTimeMs = sipper.usageTimeMs;
|
|
if (usageTimeMs >= DateUtils.MINUTE_IN_MILLIS) {
|
|
final CharSequence timeSequence =
|
|
StringUtil.formatElapsedTime(mContext, usageTimeMs, false);
|
|
preference.setSummary(
|
|
(sipper.drainType != DrainType.APP || mBatteryUtils.shouldHideSipper(sipper))
|
|
? timeSequence
|
|
: TextUtils.expandTemplate(mContext.getText(R.string.battery_used_for),
|
|
timeSequence));
|
|
}
|
|
}
|
|
|
|
@VisibleForTesting
|
|
boolean shouldHideSipper(BatterySipper sipper) {
|
|
// Don't show over-counted and unaccounted in any condition
|
|
return sipper.drainType == BatterySipper.DrainType.OVERCOUNTED
|
|
|| sipper.drainType == BatterySipper.DrainType.UNACCOUNTED;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
String extractKeyFromSipper(BatterySipper sipper) {
|
|
if (sipper.uidObj != null) {
|
|
return extractKeyFromUid(sipper.getUid());
|
|
} else if (sipper.drainType == DrainType.USER) {
|
|
return sipper.drainType.toString() + sipper.userId;
|
|
} else if (sipper.drainType != DrainType.APP) {
|
|
return sipper.drainType.toString();
|
|
} else if (sipper.getPackages() != null) {
|
|
return TextUtils.concat(sipper.getPackages()).toString();
|
|
} else {
|
|
Log.w(TAG, "Inappropriate BatterySipper without uid and package names: " + sipper);
|
|
return "-1";
|
|
}
|
|
}
|
|
|
|
@VisibleForTesting
|
|
String extractKeyFromUid(int uid) {
|
|
return Integer.toString(uid);
|
|
}
|
|
|
|
private void cacheRemoveAllPrefs(PreferenceGroup group) {
|
|
mPreferenceCache = new ArrayMap<>();
|
|
final int N = group.getPreferenceCount();
|
|
for (int i = 0; i < N; i++) {
|
|
Preference p = group.getPreference(i);
|
|
if (TextUtils.isEmpty(p.getKey())) {
|
|
continue;
|
|
}
|
|
mPreferenceCache.put(p.getKey(), p);
|
|
}
|
|
}
|
|
|
|
private static boolean isSharedGid(int uid) {
|
|
return UserHandle.getAppIdFromSharedAppGid(uid) > 0;
|
|
}
|
|
|
|
private static boolean isSystemUid(int uid) {
|
|
final int appUid = UserHandle.getAppId(uid);
|
|
return appUid >= Process.SYSTEM_UID && appUid < Process.FIRST_APPLICATION_UID;
|
|
}
|
|
|
|
private static List<BatterySipper> getFakeStats() {
|
|
ArrayList<BatterySipper> stats = new ArrayList<>();
|
|
float use = 5;
|
|
for (DrainType type : DrainType.values()) {
|
|
if (type == DrainType.APP) {
|
|
continue;
|
|
}
|
|
stats.add(new BatterySipper(type, null, use));
|
|
use += 5;
|
|
}
|
|
for (int i = 0; i < 100; i++) {
|
|
stats.add(new BatterySipper(DrainType.APP,
|
|
new FakeUid(Process.FIRST_APPLICATION_UID + i), use));
|
|
}
|
|
stats.add(new BatterySipper(DrainType.APP,
|
|
new FakeUid(0), use));
|
|
|
|
// Simulate dex2oat process.
|
|
BatterySipper sipper = new BatterySipper(DrainType.APP,
|
|
new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID)), 10.0f);
|
|
sipper.packageWithHighestDrain = "dex2oat";
|
|
stats.add(sipper);
|
|
|
|
sipper = new BatterySipper(DrainType.APP,
|
|
new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID + 1)), 10.0f);
|
|
sipper.packageWithHighestDrain = "dex2oat";
|
|
stats.add(sipper);
|
|
|
|
sipper = new BatterySipper(DrainType.APP,
|
|
new FakeUid(UserHandle.getSharedAppGid(Process.LOG_UID)), 9.0f);
|
|
stats.add(sipper);
|
|
|
|
return stats;
|
|
}
|
|
|
|
private Preference getCachedPreference(String key) {
|
|
return mPreferenceCache != null ? mPreferenceCache.remove(key) : null;
|
|
}
|
|
|
|
private void removeCachedPrefs(PreferenceGroup group) {
|
|
for (Preference p : mPreferenceCache.values()) {
|
|
group.removePreference(p);
|
|
}
|
|
mPreferenceCache = null;
|
|
}
|
|
|
|
private int getCachedCount() {
|
|
return mPreferenceCache != null ? mPreferenceCache.size() : 0;
|
|
}
|
|
|
|
private void addNotAvailableMessage() {
|
|
final String NOT_AVAILABLE = "not_available";
|
|
Preference notAvailable = getCachedPreference(NOT_AVAILABLE);
|
|
if (notAvailable == null) {
|
|
notAvailable = new Preference(mPrefContext);
|
|
notAvailable.setKey(NOT_AVAILABLE);
|
|
notAvailable.setTitle(R.string.power_usage_not_available);
|
|
mAppListGroup.addPreference(notAvailable);
|
|
}
|
|
}
|
|
}
|