fix: 引入Settings的Module

This commit is contained in:
2024-12-10 14:57:24 +08:00
parent ad8fc8731d
commit df105485bd
6934 changed files with 896168 additions and 2 deletions

View File

@@ -0,0 +1,496 @@
/*
* 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 static com.android.settings.fuelgauge.batteryusage.ConvertUtils.isUserConsumer;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.Utils;
import com.android.settings.applications.appinfo.AppButtonsPreferenceController;
import com.android.settings.applications.appinfo.ButtonActionDialogFragment;
import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
import com.android.settings.fuelgauge.batteryusage.BatteryDiffEntry;
import com.android.settings.fuelgauge.batteryusage.BatteryEntry;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.EntityHeaderController;
import com.android.settingslib.PrimarySwitchPreference;
import com.android.settingslib.applications.AppUtils;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.instrumentation.Instrumentable;
import com.android.settingslib.datastore.ChangeReason;
import com.android.settingslib.widget.LayoutPreference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Power usage detail fragment for each app, this fragment contains <br>
* <br>
* 1. Detail battery usage information for app(i.e. usage time, usage amount) <br>
* 2. Battery related controls for app(i.e uninstall, force stop)
*/
public class AdvancedPowerUsageDetail extends DashboardFragment
implements ButtonActionDialogFragment.AppButtonsDialogListener,
Preference.OnPreferenceClickListener,
Preference.OnPreferenceChangeListener {
public static final String TAG = "AdvancedPowerDetail";
public static final String EXTRA_UID = "extra_uid";
public static final String EXTRA_PACKAGE_NAME = "extra_package_name";
public static final String EXTRA_FOREGROUND_TIME = "extra_foreground_time";
public static final String EXTRA_BACKGROUND_TIME = "extra_background_time";
public static final String EXTRA_SCREEN_ON_TIME = "extra_screen_on_time";
public static final String EXTRA_ANOMALY_HINT_PREF_KEY = "extra_anomaly_hint_pref_key";
public static final String EXTRA_ANOMALY_HINT_TEXT = "extra_anomaly_hint_text";
public static final String EXTRA_SHOW_TIME_INFO = "extra_show_time_info";
public static final String EXTRA_SLOT_TIME = "extra_slot_time";
public static final String EXTRA_LABEL = "extra_label";
public static final String EXTRA_ICON_ID = "extra_icon_id";
public static final String EXTRA_POWER_USAGE_PERCENT = "extra_power_usage_percent";
public static final String EXTRA_POWER_USAGE_AMOUNT = "extra_power_usage_amount";
private static final String KEY_PREF_HEADER = "header_view";
private static final String KEY_ALLOW_BACKGROUND_USAGE = "allow_background_usage";
private static final int REQUEST_UNINSTALL = 0;
private static final int REQUEST_REMOVE_DEVICE_ADMIN = 1;
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private AppButtonsPreferenceController mAppButtonsPreferenceController;
private PowerUsageTimeController mPowerUsageTimeController;
@VisibleForTesting LayoutPreference mHeaderPreference;
@VisibleForTesting ApplicationsState mState;
@VisibleForTesting ApplicationsState.AppEntry mAppEntry;
@VisibleForTesting BatteryOptimizeUtils mBatteryOptimizeUtils;
@VisibleForTesting PrimarySwitchPreference mAllowBackgroundUsagePreference;
@VisibleForTesting @BatteryOptimizeUtils.OptimizationMode
int mOptimizationMode = BatteryOptimizeUtils.MODE_UNKNOWN;
@VisibleForTesting StringBuilder mLogStringBuilder;
// A wrapper class to carry LaunchBatteryDetailPage required arguments.
private static final class LaunchBatteryDetailPageArgs {
private String mUsagePercent;
private String mPackageName;
private String mAppLabel;
private String mSlotInformation;
private String mAnomalyHintText;
private String mAnomalyHintPrefKey;
private int mUid;
private int mIconId;
private int mConsumedPower;
private long mForegroundTimeMs;
private long mBackgroundTimeMs;
private long mScreenOnTimeMs;
private boolean mShowTimeInformation;
private boolean mIsUserEntry;
}
/** Launches battery details page for an individual battery consumer fragment. */
public static void startBatteryDetailPage(
Context context,
int sourceMetricsCategory,
BatteryDiffEntry diffEntry,
String usagePercent,
String slotInformation,
boolean showTimeInformation,
String anomalyHintPrefKey,
String anomalyHintText) {
final LaunchBatteryDetailPageArgs launchArgs = new LaunchBatteryDetailPageArgs();
// configure the launch argument.
launchArgs.mUsagePercent = usagePercent;
launchArgs.mPackageName = diffEntry.getPackageName();
launchArgs.mAppLabel = diffEntry.getAppLabel();
launchArgs.mSlotInformation = slotInformation;
launchArgs.mUid = (int) diffEntry.mUid;
launchArgs.mIconId = diffEntry.getAppIconId();
launchArgs.mConsumedPower = (int) diffEntry.mConsumePower;
launchArgs.mShowTimeInformation = showTimeInformation;
if (launchArgs.mShowTimeInformation) {
launchArgs.mForegroundTimeMs = diffEntry.mForegroundUsageTimeInMs;
launchArgs.mBackgroundTimeMs =
diffEntry.mBackgroundUsageTimeInMs + diffEntry.mForegroundServiceUsageTimeInMs;
launchArgs.mScreenOnTimeMs = diffEntry.mScreenOnTimeInMs;
launchArgs.mAnomalyHintPrefKey = anomalyHintPrefKey;
launchArgs.mAnomalyHintText = anomalyHintText;
}
launchArgs.mIsUserEntry = isUserConsumer(diffEntry.mConsumerType);
startBatteryDetailPage(context, sourceMetricsCategory, launchArgs);
}
/** Launches battery details page for an individual battery consumer. */
public static void startBatteryDetailPage(
Activity caller,
InstrumentedPreferenceFragment fragment,
BatteryEntry entry,
String usagePercent) {
final LaunchBatteryDetailPageArgs launchArgs = new LaunchBatteryDetailPageArgs();
// configure the launch argument.
launchArgs.mUsagePercent = usagePercent;
launchArgs.mPackageName = entry.getDefaultPackageName();
launchArgs.mAppLabel = entry.getLabel();
launchArgs.mUid = entry.getUid();
launchArgs.mIconId = entry.mIconId;
launchArgs.mConsumedPower = (int) entry.getConsumedPower();
launchArgs.mIsUserEntry = entry.isUserEntry();
launchArgs.mShowTimeInformation = false;
startBatteryDetailPage(caller, fragment.getMetricsCategory(), launchArgs);
}
private static void startBatteryDetailPage(
Context context, int sourceMetricsCategory, LaunchBatteryDetailPageArgs launchArgs) {
final Bundle args = new Bundle();
if (launchArgs.mPackageName == null) {
// populate data for system app
args.putString(EXTRA_LABEL, launchArgs.mAppLabel);
args.putInt(EXTRA_ICON_ID, launchArgs.mIconId);
args.putString(EXTRA_PACKAGE_NAME, null);
} else {
// populate data for normal app
args.putString(EXTRA_PACKAGE_NAME, launchArgs.mPackageName);
}
args.putInt(EXTRA_UID, launchArgs.mUid);
args.putLong(EXTRA_BACKGROUND_TIME, launchArgs.mBackgroundTimeMs);
args.putLong(EXTRA_FOREGROUND_TIME, launchArgs.mForegroundTimeMs);
args.putLong(EXTRA_SCREEN_ON_TIME, launchArgs.mScreenOnTimeMs);
args.putString(EXTRA_SLOT_TIME, launchArgs.mSlotInformation);
args.putString(EXTRA_POWER_USAGE_PERCENT, launchArgs.mUsagePercent);
args.putInt(EXTRA_POWER_USAGE_AMOUNT, launchArgs.mConsumedPower);
args.putBoolean(EXTRA_SHOW_TIME_INFO, launchArgs.mShowTimeInformation);
args.putString(EXTRA_ANOMALY_HINT_PREF_KEY, launchArgs.mAnomalyHintPrefKey);
args.putString(EXTRA_ANOMALY_HINT_TEXT, launchArgs.mAnomalyHintText);
final int userId =
launchArgs.mIsUserEntry
? ActivityManager.getCurrentUser()
: UserHandle.getUserId(launchArgs.mUid);
new SubSettingLauncher(context)
.setDestination(AdvancedPowerUsageDetail.class.getName())
.setTitleRes(R.string.battery_details_title)
.setArguments(args)
.setSourceMetricsCategory(sourceMetricsCategory)
.setUserHandle(new UserHandle(userId))
.launch();
}
/** Start packageName's battery detail page. */
public static void startBatteryDetailPage(
Activity caller,
Instrumentable instrumentable,
String packageName,
UserHandle userHandle) {
final Bundle args = new Bundle(3);
final PackageManager packageManager = caller.getPackageManager();
args.putString(EXTRA_PACKAGE_NAME, packageName);
args.putString(EXTRA_POWER_USAGE_PERCENT, Utils.formatPercentage(0));
try {
args.putInt(EXTRA_UID, packageManager.getPackageUid(packageName, 0 /* no flag */));
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Cannot find package: " + packageName, e);
}
new SubSettingLauncher(caller)
.setDestination(AdvancedPowerUsageDetail.class.getName())
.setTitleRes(R.string.battery_details_title)
.setArguments(args)
.setSourceMetricsCategory(instrumentable.getMetricsCategory())
.setUserHandle(userHandle)
.launch();
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mState = ApplicationsState.getInstance(getActivity().getApplication());
}
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
final String packageName = getArguments().getString(EXTRA_PACKAGE_NAME);
onCreateBackgroundUsageState(packageName);
mHeaderPreference = findPreference(KEY_PREF_HEADER);
if (packageName != null) {
mAppEntry = mState.getEntry(packageName, UserHandle.myUserId());
}
}
@Override
public void onResume() {
super.onResume();
initHeader();
mOptimizationMode = mBatteryOptimizeUtils.getAppOptimizationMode();
initFooter();
mLogStringBuilder = new StringBuilder("onResume mode = ").append(mOptimizationMode);
}
@Override
public void onPause() {
super.onPause();
notifyBackupManager();
final int currentOptimizeMode = mBatteryOptimizeUtils.getAppOptimizationMode();
mLogStringBuilder.append(", onPause mode = ").append(currentOptimizeMode);
logMetricCategory(currentOptimizeMode);
mExecutor.execute(
() -> {
BatteryOptimizeLogUtils.writeLog(
getContext().getApplicationContext(),
Action.LEAVE,
BatteryOptimizeLogUtils.getPackageNameWithUserId(
mBatteryOptimizeUtils.getPackageName(), UserHandle.myUserId()),
mLogStringBuilder.toString());
});
Log.d(TAG, "Leave with mode: " + currentOptimizeMode);
}
@VisibleForTesting
void notifyBackupManager() {
if (mOptimizationMode != mBatteryOptimizeUtils.getAppOptimizationMode()) {
BatterySettingsStorage.get(getContext()).notifyChange(ChangeReason.UPDATE);
}
}
@VisibleForTesting
void initHeader() {
final View appSnippet = mHeaderPreference.findViewById(R.id.entity_header);
final Activity context = getActivity();
final Bundle bundle = getArguments();
EntityHeaderController controller =
EntityHeaderController.newInstance(context, this, appSnippet)
.setButtonActions(
EntityHeaderController.ActionType.ACTION_NONE,
EntityHeaderController.ActionType.ACTION_NONE);
if (mAppEntry == null) {
controller.setLabel(bundle.getString(EXTRA_LABEL));
final int iconId = bundle.getInt(EXTRA_ICON_ID, 0);
if (iconId == 0) {
controller.setIcon(context.getPackageManager().getDefaultActivityIcon());
} else {
controller.setIcon(context.getDrawable(bundle.getInt(EXTRA_ICON_ID)));
}
} else {
mState.ensureIcon(mAppEntry);
controller.setLabel(mAppEntry);
controller.setIcon(mAppEntry);
controller.setIsInstantApp(AppUtils.isInstant(mAppEntry.info));
}
if (mPowerUsageTimeController != null) {
final String slotTime = bundle.getString(EXTRA_SLOT_TIME);
final long screenOnTimeInMs = bundle.getLong(EXTRA_SCREEN_ON_TIME);
final long backgroundTimeMs = bundle.getLong(EXTRA_BACKGROUND_TIME);
final String anomalyHintPrefKey = bundle.getString(EXTRA_ANOMALY_HINT_PREF_KEY);
final String anomalyHintText = bundle.getString(EXTRA_ANOMALY_HINT_TEXT);
mPowerUsageTimeController.handleScreenTimeUpdated(
slotTime,
screenOnTimeInMs,
backgroundTimeMs,
anomalyHintPrefKey,
anomalyHintText);
}
controller.done(true /* rebindActions */);
}
@VisibleForTesting
void initFooter() {
final String stateString;
final String detailInfoString;
final Context context = getContext();
if (mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()) {
// Present optimized only string when the package name is invalid.
stateString = context.getString(R.string.manager_battery_usage_optimized_only);
detailInfoString =
context.getString(R.string.manager_battery_usage_footer_limited, stateString);
} else if (mBatteryOptimizeUtils.isSystemOrDefaultApp()) {
// Present unrestricted only string when the package is system or default active app.
stateString = context.getString(R.string.manager_battery_usage_unrestricted_only);
detailInfoString =
context.getString(R.string.manager_battery_usage_footer_limited, stateString);
} else {
// Present default string to normal app.
detailInfoString =
context.getString(
R.string.manager_battery_usage_allow_background_usage_summary);
}
mAllowBackgroundUsagePreference.setSummary(detailInfoString);
}
@Override
public int getMetricsCategory() {
return SettingsEnums.FUELGAUGE_POWER_USAGE_DETAIL;
}
@Override
protected String getLogTag() {
return TAG;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.power_usage_detail;
}
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
final List<AbstractPreferenceController> controllers = new ArrayList<>();
final Bundle bundle = getArguments();
final int uid = bundle.getInt(EXTRA_UID, 0);
final String packageName = bundle.getString(EXTRA_PACKAGE_NAME);
mAppButtonsPreferenceController =
new AppButtonsPreferenceController(
(SettingsActivity) getActivity(),
this,
getSettingsLifecycle(),
packageName,
mState,
REQUEST_UNINSTALL,
REQUEST_REMOVE_DEVICE_ADMIN);
if (bundle.getBoolean(EXTRA_SHOW_TIME_INFO, false)) {
mPowerUsageTimeController = new PowerUsageTimeController(getContext());
controllers.add(mPowerUsageTimeController);
}
controllers.add(mAppButtonsPreferenceController);
controllers.add(new AllowBackgroundPreferenceController(context, uid, packageName));
return controllers;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (mAppButtonsPreferenceController != null) {
mAppButtonsPreferenceController.handleActivityResult(requestCode, resultCode, data);
}
}
@Override
public void handleDialogClick(int id) {
if (mAppButtonsPreferenceController != null) {
mAppButtonsPreferenceController.handleDialogClick(id);
}
}
@Override
public boolean onPreferenceClick(Preference preference) {
if (!(preference instanceof PrimarySwitchPreference)
|| !TextUtils.equals(preference.getKey(), KEY_ALLOW_BACKGROUND_USAGE)) {
return false;
}
PowerBackgroundUsageDetail.startPowerBackgroundUsageDetailPage(
getContext(), getArguments());
return true;
}
@Override
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
if (!(preference instanceof PrimarySwitchPreference)
|| !TextUtils.equals(preference.getKey(), KEY_ALLOW_BACKGROUND_USAGE)) {
return false;
}
if (newValue instanceof Boolean) {
final boolean isAllowBackgroundUsage = (boolean) newValue;
mBatteryOptimizeUtils.setAppUsageState(
isAllowBackgroundUsage
? BatteryOptimizeUtils.MODE_OPTIMIZED
: BatteryOptimizeUtils.MODE_RESTRICTED,
Action.APPLY);
}
return true;
}
private void logMetricCategory(int currentOptimizeMode) {
if (currentOptimizeMode == mOptimizationMode) {
return;
}
int metricCategory = 0;
switch (currentOptimizeMode) {
case BatteryOptimizeUtils.MODE_UNRESTRICTED:
case BatteryOptimizeUtils.MODE_OPTIMIZED:
metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_ALLOW_BACKGROUND;
break;
case BatteryOptimizeUtils.MODE_RESTRICTED:
metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_DISABLE_BACKGROUND;
break;
}
if (metricCategory == 0) {
return;
}
int finalMetricCategory = metricCategory;
mExecutor.execute(
() -> {
String packageName =
BatteryUtils.getLoggingPackageName(
getContext(), mBatteryOptimizeUtils.getPackageName());
FeatureFactory.getFeatureFactory()
.getMetricsFeatureProvider()
.action(
/* attribution */ SettingsEnums.LEAVE_APP_BATTERY_USAGE,
/* action */ finalMetricCategory,
/* pageId */ SettingsEnums.FUELGAUGE_POWER_USAGE_DETAIL,
packageName,
getArguments().getInt(EXTRA_POWER_USAGE_AMOUNT));
});
}
private void onCreateBackgroundUsageState(String packageName) {
mAllowBackgroundUsagePreference = findPreference(KEY_ALLOW_BACKGROUND_USAGE);
if (mAllowBackgroundUsagePreference != null) {
mAllowBackgroundUsagePreference.setOnPreferenceClickListener(this);
mAllowBackgroundUsagePreference.setOnPreferenceChangeListener(this);
}
mBatteryOptimizeUtils =
new BatteryOptimizeUtils(
getContext(), getArguments().getInt(EXTRA_UID), packageName);
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2019 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 static com.android.settings.fuelgauge.AdvancedPowerUsageDetail.EXTRA_PACKAGE_NAME;
import static com.android.settings.fuelgauge.AdvancedPowerUsageDetail.EXTRA_POWER_USAGE_PERCENT;
import static com.android.settings.fuelgauge.AdvancedPowerUsageDetail.EXTRA_UID;
import android.app.settings.SettingsEnums;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.core.SubSettingLauncher;
/** Trampoline activity for launching the {@link AdvancedPowerUsageDetail} fragment. */
public class AdvancedPowerUsageDetailActivity extends AppCompatActivity {
private static final String TAG = "AdvancedPowerDetailActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Intent intent = getIntent();
final Uri data = intent == null ? null : intent.getData();
final String packageName = data == null ? null : data.getSchemeSpecificPart();
if (packageName != null) {
final Bundle args = new Bundle(4);
final PackageManager packageManager = getPackageManager();
args.putString(EXTRA_PACKAGE_NAME, packageName);
args.putString(EXTRA_POWER_USAGE_PERCENT, Utils.formatPercentage(0));
if (intent.getBooleanExtra("request_ignore_background_restriction", false)) {
args.putString(":settings:fragment_args_key", "background_activity");
}
try {
args.putInt(EXTRA_UID, packageManager.getPackageUid(packageName, 0 /* no flag */));
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Cannot find package: " + packageName, e);
}
new SubSettingLauncher(this)
.setDestination(AdvancedPowerUsageDetail.class.getName())
.setTitleRes(R.string.battery_details_title)
.setArguments(args)
.setSourceMetricsCategory(SettingsEnums.APPLICATIONS_INSTALLED_APP_DETAILS)
.addFlags(intent.getFlags())
.launch();
}
finish();
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2023 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.content.Context;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settingslib.PrimarySwitchPreference;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.widget.MainSwitchPreference;
/** Controller to update the app background usage state */
public class AllowBackgroundPreferenceController extends AbstractPreferenceController
implements PreferenceControllerMixin {
private static final String TAG = "AllowBackgroundPreferenceController";
@VisibleForTesting static final String KEY_ALLOW_BACKGROUND_USAGE = "allow_background_usage";
@VisibleForTesting BatteryOptimizeUtils mBatteryOptimizeUtils;
public AllowBackgroundPreferenceController(Context context, int uid, String packageName) {
super(context);
mBatteryOptimizeUtils = new BatteryOptimizeUtils(context, uid, packageName);
}
private void setChecked(Preference preference, boolean checked) {
if (preference instanceof PrimarySwitchPreference) {
((PrimarySwitchPreference) preference).setChecked(checked);
} else if (preference instanceof MainSwitchPreference) {
((MainSwitchPreference) preference).setChecked(checked);
}
}
private void setEnabled(Preference preference, boolean enabled) {
if (preference instanceof PrimarySwitchPreference) {
((PrimarySwitchPreference) preference).setEnabled(enabled);
((PrimarySwitchPreference) preference).setSwitchEnabled(enabled);
} else if (preference instanceof MainSwitchPreference) {
((MainSwitchPreference) preference).setEnabled(enabled);
}
}
@Override
public void updateState(Preference preference) {
setEnabled(preference, mBatteryOptimizeUtils.isOptimizeModeMutable());
final boolean isAllowBackground =
mBatteryOptimizeUtils.getAppOptimizationMode()
!= BatteryOptimizeUtils.MODE_RESTRICTED;
setChecked(preference, isAllowBackground);
}
@Override
public boolean isAvailable() {
return true;
}
@Override
public String getPreferenceKey() {
return KEY_ALLOW_BACKGROUND_USAGE;
}
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
return getPreferenceKey().equals(preference.getKey());
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2018 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.content.Context;
import android.provider.Settings;
import androidx.preference.Preference;
import androidx.preference.TwoStatePreference;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.overlay.FeatureFactory;
/** Controller to change and update the auto restriction toggle */
public class AutoRestrictionPreferenceController extends BasePreferenceController
implements Preference.OnPreferenceChangeListener {
private static final String KEY_SMART_BATTERY = "auto_restriction";
private static final int ON = 1;
private static final int OFF = 0;
private final PowerUsageFeatureProvider mPowerUsageFeatureProvider;
public AutoRestrictionPreferenceController(Context context) {
super(context, KEY_SMART_BATTERY);
mPowerUsageFeatureProvider =
FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider();
}
@Override
public int getAvailabilityStatus() {
return mPowerUsageFeatureProvider.isSmartBatterySupported()
? UNSUPPORTED_ON_DEVICE
: AVAILABLE;
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
final boolean smartBatteryOn =
Settings.Global.getInt(
mContext.getContentResolver(),
Settings.Global.APP_AUTO_RESTRICTION_ENABLED,
ON)
== ON;
((TwoStatePreference) preference).setChecked(smartBatteryOn);
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final boolean smartBatteryOn = (Boolean) newValue;
Settings.Global.putInt(
mContext.getContentResolver(),
Settings.Global.APP_AUTO_RESTRICTION_ENABLED,
smartBatteryOn ? ON : OFF);
return true;
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2016 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.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.SparseIntArray;
import android.view.View;
import androidx.annotation.Nullable;
public class BatteryActiveView extends View {
private final Paint mPaint = new Paint();
private BatteryActiveProvider mProvider;
public BatteryActiveView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public void setProvider(BatteryActiveProvider provider) {
mProvider = provider;
if (getWidth() != 0) {
postInvalidate();
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (getWidth() != 0) {
postInvalidate();
}
}
@Override
protected void onDraw(Canvas canvas) {
if (mProvider == null) {
return;
}
SparseIntArray array = mProvider.getColorArray();
float period = mProvider.getPeriod();
for (int i = 0; i < array.size() - 1; i++) {
drawColor(canvas, array.keyAt(i), array.keyAt(i + 1), array.valueAt(i), period);
}
}
private void drawColor(Canvas canvas, int start, int end, int color, float period) {
if (color == 0) {
return;
}
mPaint.setColor(color);
canvas.drawRect(
start / period * getWidth(), 0, end / period * getWidth(), getHeight(), mPaint);
}
public interface BatteryActiveProvider {
boolean hasData();
long getPeriod();
SparseIntArray getColorArray();
}
}

View File

@@ -0,0 +1,398 @@
/*
* Copyright (C) 2021 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.AppGlobals;
import android.app.AppOpsManager;
import android.app.backup.BackupDataInputStream;
import android.app.backup.BackupDataOutput;
import android.app.backup.BackupHelper;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.os.Build;
import android.os.IDeviceIdleController;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.fuelgauge.PowerAllowlistBackend;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/** An implementation to backup and restore battery configurations. */
public final class BatteryBackupHelper implements BackupHelper {
/** An inditifier for {@link BackupHelper}. */
public static final String TAG = "BatteryBackupHelper";
// Definition for the device build information.
public static final String KEY_BUILD_BRAND = "device_build_brand";
public static final String KEY_BUILD_PRODUCT = "device_build_product";
public static final String KEY_BUILD_MANUFACTURER = "device_build_manufacture";
public static final String KEY_BUILD_FINGERPRINT = "device_build_fingerprint";
// Customized fields for device extra information.
public static final String KEY_BUILD_METADATA_1 = "device_build_metadata_1";
public static final String KEY_BUILD_METADATA_2 = "device_build_metadata_2";
private static final String DEVICE_IDLE_SERVICE = "deviceidle";
private static final String BATTERY_OPTIMIZE_BACKUP_FILE_NAME =
"battery_optimize_backup_historical_logs";
private static final int DEVICE_BUILD_INFO_SIZE = 6;
static final String DELIMITER = ",";
static final String DELIMITER_MODE = ":";
static final String KEY_OPTIMIZATION_LIST = "optimization_mode_list";
@VisibleForTesting ArraySet<ApplicationInfo> mTestApplicationInfoList = null;
@VisibleForTesting PowerAllowlistBackend mPowerAllowlistBackend;
@VisibleForTesting IDeviceIdleController mIDeviceIdleController;
@VisibleForTesting IPackageManager mIPackageManager;
@VisibleForTesting BatteryOptimizeUtils mBatteryOptimizeUtils;
private byte[] mOptimizationModeBytes;
private boolean mVerifyMigrateConfiguration = false;
private final Context mContext;
// Device information map from the restoreEntity() method.
private final ArrayMap<String, String> mDeviceBuildInfoMap =
new ArrayMap<>(DEVICE_BUILD_INFO_SIZE);
public BatteryBackupHelper(Context context) {
mContext = context.getApplicationContext();
}
@Override
public void performBackup(
ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) {
if (!isOwner() || data == null) {
Log.w(TAG, "ignore performBackup() for non-owner or empty data");
return;
}
final List<String> allowlistedApps = getFullPowerList();
if (allowlistedApps == null) {
return;
}
writeBackupData(data, KEY_BUILD_BRAND, Build.BRAND);
writeBackupData(data, KEY_BUILD_PRODUCT, Build.PRODUCT);
writeBackupData(data, KEY_BUILD_MANUFACTURER, Build.MANUFACTURER);
writeBackupData(data, KEY_BUILD_FINGERPRINT, Build.FINGERPRINT);
// Add customized device build metadata fields.
final PowerUsageFeatureProvider provider =
FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider();
writeBackupData(data, KEY_BUILD_METADATA_1, provider.getBuildMetadata1(mContext));
writeBackupData(data, KEY_BUILD_METADATA_2, provider.getBuildMetadata2(mContext));
backupOptimizationMode(data, allowlistedApps);
}
@Override
public void restoreEntity(BackupDataInputStream data) {
// Ensure we only verify the migrate configuration one time.
if (!mVerifyMigrateConfiguration) {
mVerifyMigrateConfiguration = true;
BatterySettingsMigrateChecker.verifySaverConfiguration(mContext);
}
if (!isOwner() || data == null || data.size() == 0) {
Log.w(TAG, "ignore restoreEntity() for non-owner or empty data");
return;
}
final String dataKey = data.getKey();
switch (dataKey) {
case KEY_BUILD_BRAND:
case KEY_BUILD_PRODUCT:
case KEY_BUILD_MANUFACTURER:
case KEY_BUILD_FINGERPRINT:
case KEY_BUILD_METADATA_1:
case KEY_BUILD_METADATA_2:
restoreBackupData(dataKey, data);
break;
case KEY_OPTIMIZATION_LIST:
// Hold the optimization mode data until all conditions are matched.
mOptimizationModeBytes = getBackupData(dataKey, data);
break;
}
performRestoreIfNeeded();
}
@Override
public void writeNewStateDescription(ParcelFileDescriptor newState) {}
private List<String> getFullPowerList() {
final long timestamp = System.currentTimeMillis();
String[] allowlistedApps;
try {
allowlistedApps = getIDeviceIdleController().getFullPowerWhitelist();
} catch (RemoteException e) {
Log.e(TAG, "backupFullPowerList() failed", e);
return null;
}
// Ignores unexpected empty result case.
if (allowlistedApps == null || allowlistedApps.length == 0) {
Log.w(TAG, "no data found in the getFullPowerList()");
return new ArrayList<>();
}
Log.d(
TAG,
String.format(
"getFullPowerList() size=%d in %d/ms",
allowlistedApps.length, (System.currentTimeMillis() - timestamp)));
return Arrays.asList(allowlistedApps);
}
@VisibleForTesting
void backupOptimizationMode(BackupDataOutput data, List<String> allowlistedApps) {
final long timestamp = System.currentTimeMillis();
final ArraySet<ApplicationInfo> applications = getInstalledApplications();
if (applications == null || applications.isEmpty()) {
Log.w(TAG, "no data found in the getInstalledApplications()");
return;
}
int backupCount = 0;
final StringBuilder builder = new StringBuilder();
final AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class);
final SharedPreferences sharedPreferences = getSharedPreferences(mContext);
// Converts application into the AppUsageState.
for (ApplicationInfo info : applications) {
final int mode = BatteryOptimizeUtils.getMode(appOps, info.uid, info.packageName);
@BatteryOptimizeUtils.OptimizationMode
final int optimizationMode =
BatteryOptimizeUtils.getAppOptimizationMode(
mode, allowlistedApps.contains(info.packageName));
// Ignores default optimized/unknown state or system/default apps.
if (optimizationMode == BatteryOptimizeUtils.MODE_OPTIMIZED
|| optimizationMode == BatteryOptimizeUtils.MODE_UNKNOWN
|| isSystemOrDefaultApp(info.packageName, info.uid)) {
continue;
}
final String packageOptimizeMode = info.packageName + DELIMITER_MODE + optimizationMode;
builder.append(packageOptimizeMode + DELIMITER);
Log.d(TAG, "backupOptimizationMode: " + packageOptimizeMode);
BatteryOptimizeLogUtils.writeLog(
sharedPreferences,
Action.BACKUP,
info.packageName,
/* actionDescription */ "mode: " + optimizationMode);
backupCount++;
}
writeBackupData(data, KEY_OPTIMIZATION_LIST, builder.toString());
Log.d(
TAG,
String.format(
"backup getInstalledApplications():%d count=%d in %d/ms",
applications.size(),
backupCount,
(System.currentTimeMillis() - timestamp)));
}
@VisibleForTesting
int restoreOptimizationMode(byte[] dataBytes) {
final long timestamp = System.currentTimeMillis();
final String dataContent = new String(dataBytes, StandardCharsets.UTF_8);
if (dataContent == null || dataContent.isEmpty()) {
Log.w(TAG, "no data found in the restoreOptimizationMode()");
return 0;
}
final String[] appConfigurations = dataContent.split(BatteryBackupHelper.DELIMITER);
if (appConfigurations == null || appConfigurations.length == 0) {
Log.w(TAG, "no data found from the split() processing");
return 0;
}
int restoreCount = 0;
for (int index = 0; index < appConfigurations.length; index++) {
final String[] results =
appConfigurations[index].split(BatteryBackupHelper.DELIMITER_MODE);
// Example format: com.android.systemui:2 we should have length=2
if (results == null || results.length != 2) {
Log.w(TAG, "invalid raw data found:" + appConfigurations[index]);
continue;
}
final String packageName = results[0];
final int uid = BatteryUtils.getInstance(mContext).getPackageUid(packageName);
// Ignores system/default apps.
if (isSystemOrDefaultApp(packageName, uid)) {
Log.w(TAG, "ignore from isSystemOrDefaultApp():" + packageName);
continue;
}
@BatteryOptimizeUtils.OptimizationMode
int optimizationMode = BatteryOptimizeUtils.MODE_UNKNOWN;
try {
optimizationMode = Integer.parseInt(results[1]);
} catch (NumberFormatException e) {
Log.e(TAG, "failed to parse the optimization mode: " + appConfigurations[index], e);
continue;
}
restoreOptimizationMode(packageName, optimizationMode);
restoreCount++;
}
Log.d(
TAG,
String.format(
"restoreOptimizationMode() count=%d in %d/ms",
restoreCount, (System.currentTimeMillis() - timestamp)));
return restoreCount;
}
private void performRestoreIfNeeded() {
if (mOptimizationModeBytes == null || mOptimizationModeBytes.length == 0) {
return;
}
final PowerUsageFeatureProvider provider =
FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider();
if (!provider.isValidToRestoreOptimizationMode(mDeviceBuildInfoMap)) {
return;
}
// Start to restore the app optimization mode data.
final int restoreCount = restoreOptimizationMode(mOptimizationModeBytes);
if (restoreCount > 0) {
BatterySettingsMigrateChecker.verifyBatteryOptimizeModes(mContext);
}
mOptimizationModeBytes = null; // clear data
}
/** Dump the app optimization mode backup history data. */
public static void dumpHistoricalData(Context context, PrintWriter writer) {
BatteryOptimizeLogUtils.printBatteryOptimizeHistoricalLog(
getSharedPreferences(context), writer);
}
static boolean isOwner() {
return UserHandle.myUserId() == UserHandle.USER_SYSTEM;
}
static BatteryOptimizeUtils newBatteryOptimizeUtils(
Context context, String packageName, BatteryOptimizeUtils testOptimizeUtils) {
final int uid = BatteryUtils.getInstance(context).getPackageUid(packageName);
if (uid == BatteryUtils.UID_NULL) {
return null;
}
final BatteryOptimizeUtils batteryOptimizeUtils =
testOptimizeUtils != null
? testOptimizeUtils /*testing only*/
: new BatteryOptimizeUtils(context, uid, packageName);
return batteryOptimizeUtils;
}
@VisibleForTesting
static SharedPreferences getSharedPreferences(Context context) {
return context.getSharedPreferences(
BATTERY_OPTIMIZE_BACKUP_FILE_NAME, Context.MODE_PRIVATE);
}
private void restoreOptimizationMode(
String packageName, @BatteryOptimizeUtils.OptimizationMode int mode) {
final BatteryOptimizeUtils batteryOptimizeUtils =
newBatteryOptimizeUtils(mContext, packageName, mBatteryOptimizeUtils);
if (batteryOptimizeUtils == null) {
return;
}
batteryOptimizeUtils.setAppUsageState(
mode, BatteryOptimizeHistoricalLogEntry.Action.RESTORE);
Log.d(TAG, String.format("restore:%s mode=%d", packageName, mode));
}
// Provides an opportunity to inject mock IDeviceIdleController for testing.
private IDeviceIdleController getIDeviceIdleController() {
if (mIDeviceIdleController != null) {
return mIDeviceIdleController;
}
mIDeviceIdleController =
IDeviceIdleController.Stub.asInterface(
ServiceManager.getService(DEVICE_IDLE_SERVICE));
return mIDeviceIdleController;
}
private IPackageManager getIPackageManager() {
if (mIPackageManager != null) {
return mIPackageManager;
}
mIPackageManager = AppGlobals.getPackageManager();
return mIPackageManager;
}
private PowerAllowlistBackend getPowerAllowlistBackend() {
if (mPowerAllowlistBackend != null) {
return mPowerAllowlistBackend;
}
mPowerAllowlistBackend = PowerAllowlistBackend.getInstance(mContext);
return mPowerAllowlistBackend;
}
private boolean isSystemOrDefaultApp(String packageName, int uid) {
return BatteryOptimizeUtils.isSystemOrDefaultApp(
mContext, getPowerAllowlistBackend(), packageName, uid);
}
private ArraySet<ApplicationInfo> getInstalledApplications() {
if (mTestApplicationInfoList != null) {
return mTestApplicationInfoList;
}
return BatteryOptimizeUtils.getInstalledApplications(mContext, getIPackageManager());
}
private void restoreBackupData(String dataKey, BackupDataInputStream data) {
final byte[] dataBytes = getBackupData(dataKey, data);
if (dataBytes == null || dataBytes.length == 0) {
return;
}
final String dataContent = new String(dataBytes, StandardCharsets.UTF_8);
mDeviceBuildInfoMap.put(dataKey, dataContent);
Log.d(TAG, String.format("restore:%s:%s", dataKey, dataContent));
}
private static byte[] getBackupData(String dataKey, BackupDataInputStream data) {
final int dataSize = data.size();
final byte[] dataBytes = new byte[dataSize];
try {
data.read(dataBytes, 0 /*offset*/, dataSize);
} catch (IOException e) {
Log.e(TAG, "failed to getBackupData() " + dataKey, e);
return null;
}
return dataBytes;
}
private static void writeBackupData(BackupDataOutput data, String dataKey, String dataContent) {
if (dataContent == null || dataContent.isEmpty()) {
return;
}
final byte[] dataContentBytes = dataContent.getBytes();
try {
data.writeEntityHeader(dataKey, dataContentBytes.length);
data.writeEntityData(dataContentBytes, dataContentBytes.length);
} catch (IOException e) {
Log.e(TAG, "writeBackupData() is failed for " + dataKey, e);
}
Log.d(TAG, String.format("backup:%s:%s", dataKey, dataContent));
}
}

View File

@@ -0,0 +1,164 @@
/*
* 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.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbManager;
import android.os.BatteryManager;
import android.os.PowerManager;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import com.android.settings.Utils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Use this broadcastReceiver to listen to the battery change and it will invoke {@link
* OnBatteryChangedListener}
*/
public class BatteryBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "BatteryBroadcastRcvr";
/**
* Callback if any of the monitored fields has been changed: <br>
* <br>
* Battery level(e.g. 100%->99%) Battery status(e.g. plugged->unplugged) <br>
* Battery saver(e.g.off->on) <br>
* Battery health(e.g. good->overheat) <br>
* Battery charging status(e.g. default->long life)
*/
public interface OnBatteryChangedListener {
void onBatteryChanged(@BatteryUpdateType int type);
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({
BatteryUpdateType.MANUAL,
BatteryUpdateType.BATTERY_LEVEL,
BatteryUpdateType.BATTERY_SAVER,
BatteryUpdateType.BATTERY_STATUS,
BatteryUpdateType.BATTERY_HEALTH,
BatteryUpdateType.CHARGING_STATUS,
BatteryUpdateType.BATTERY_NOT_PRESENT
})
public @interface BatteryUpdateType {
int MANUAL = 0;
int BATTERY_LEVEL = 1;
int BATTERY_SAVER = 2;
int BATTERY_STATUS = 3;
int BATTERY_HEALTH = 4;
int CHARGING_STATUS = 5;
int BATTERY_NOT_PRESENT = 6;
}
@VisibleForTesting String mBatteryLevel;
@VisibleForTesting String mBatteryStatus;
@VisibleForTesting int mChargingStatus;
@VisibleForTesting int mBatteryHealth;
private OnBatteryChangedListener mBatteryListener;
private Context mContext;
public BatteryBroadcastReceiver(Context context) {
mContext = context;
}
@Override
public void onReceive(Context context, Intent intent) {
updateBatteryStatus(intent, false /* forceUpdate */);
}
public void setBatteryChangedListener(OnBatteryChangedListener lsn) {
mBatteryListener = lsn;
}
public void register() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
intentFilter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
intentFilter.addAction(BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION);
intentFilter.addAction(UsbManager.ACTION_USB_PORT_COMPLIANCE_CHANGED);
final Intent intent =
mContext.registerReceiver(this, intentFilter, Context.RECEIVER_EXPORTED);
updateBatteryStatus(intent, true /* forceUpdate */);
}
public void unRegister() {
mContext.unregisterReceiver(this);
}
private void updateBatteryStatus(Intent intent, boolean forceUpdate) {
if (intent == null || mBatteryListener == null) {
return;
}
final String action = intent.getAction();
Log.d(TAG, "updateBatteryStatus: action=" + action);
if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
final String batteryLevel = Utils.getBatteryPercentage(intent);
final String batteryStatus =
Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false);
final int chargingStatus =
intent.getIntExtra(
BatteryManager.EXTRA_CHARGING_STATUS,
BatteryManager.CHARGING_POLICY_DEFAULT);
final int batteryHealth =
intent.getIntExtra(
BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_UNKNOWN);
Log.d(
TAG,
"Battery changed: level: "
+ batteryLevel
+ "| status: "
+ batteryStatus
+ "| chargingStatus: "
+ chargingStatus
+ "| health: "
+ batteryHealth);
if (!Utils.isBatteryPresent(intent)) {
Log.w(TAG, "Problem reading the battery meter.");
mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_NOT_PRESENT);
} else if (forceUpdate) {
mBatteryListener.onBatteryChanged(BatteryUpdateType.MANUAL);
} else if (chargingStatus != mChargingStatus) {
mBatteryListener.onBatteryChanged(BatteryUpdateType.CHARGING_STATUS);
} else if (batteryHealth != mBatteryHealth) {
mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_HEALTH);
} else if (!batteryLevel.equals(mBatteryLevel)) {
mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_LEVEL);
} else if (!batteryStatus.equals(mBatteryStatus)) {
mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_STATUS);
}
mBatteryLevel = batteryLevel;
mBatteryStatus = batteryStatus;
mChargingStatus = chargingStatus;
mBatteryHealth = batteryHealth;
} else if (PowerManager.ACTION_POWER_SAVE_MODE_CHANGED.equals(action)) {
mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_SAVER);
} else if (BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION.equals(action)
|| UsbManager.ACTION_USB_PORT_COMPLIANCE_CHANGED.equals(action)) {
mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_STATUS);
}
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2016 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.os.BatteryStats.HistoryItem;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import com.android.settings.fuelgauge.BatteryActiveView.BatteryActiveProvider;
public class BatteryFlagParser implements BatteryInfo.BatteryDataParser, BatteryActiveProvider {
private final SparseBooleanArray mData = new SparseBooleanArray();
private final int mFlag;
private final boolean mState2;
private final int mAccentColor;
private boolean mLastSet;
private long mLength;
private long mLastTime;
public BatteryFlagParser(int accent, boolean state2, int flag) {
mAccentColor = accent;
mFlag = flag;
mState2 = state2;
}
protected boolean isSet(HistoryItem record) {
return ((mState2 ? record.states2 : record.states) & mFlag) != 0;
}
@Override
public void onParsingStarted(long startTime, long endTime) {
mLength = endTime - startTime;
}
@Override
public void onDataPoint(long time, HistoryItem record) {
boolean isSet = isSet(record);
if (isSet != mLastSet) {
mData.put((int) time, isSet);
mLastSet = isSet;
}
mLastTime = time;
}
@Override
public void onDataGap() {
if (mLastSet) {
mData.put((int) mLastTime, false);
mLastSet = false;
}
}
@Override
public void onParsingDone() {
if (mLastSet) {
mData.put((int) mLastTime, false);
mLastSet = false;
}
}
@Override
public long getPeriod() {
return mLength;
}
@Override
public boolean hasData() {
return mData.size() > 1;
}
@Override
public SparseIntArray getColorArray() {
SparseIntArray ret = new SparseIntArray();
for (int i = 0; i < mData.size(); i++) {
ret.put(mData.keyAt(i), getColor(mData.valueAt(i)));
}
return ret;
}
private int getColor(boolean b) {
if (b) {
return mAccentColor;
}
return 0;
}
}

View File

@@ -0,0 +1,152 @@
/*
* 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.content.Context;
import android.content.Intent;
import android.icu.text.NumberFormat;
import android.os.BatteryManager;
import android.os.PowerManager;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.Utils;
import com.android.settingslib.widget.UsageProgressBarPreference;
/** Controller that update the battery header view */
public class BatteryHeaderPreferenceController extends BasePreferenceController
implements PreferenceControllerMixin, BatteryPreferenceController {
private static final String TAG = "BatteryHeaderPreferenceController";
@VisibleForTesting static final String KEY_BATTERY_HEADER = "battery_header";
private static final int BATTERY_MAX_LEVEL = 100;
@VisibleForTesting BatteryStatusFeatureProvider mBatteryStatusFeatureProvider;
@VisibleForTesting UsageProgressBarPreference mBatteryUsageProgressBarPref;
private BatteryTip mBatteryTip;
private final PowerManager mPowerManager;
public BatteryHeaderPreferenceController(Context context, String key) {
super(context, key);
mPowerManager = context.getSystemService(PowerManager.class);
mBatteryStatusFeatureProvider =
FeatureFactory.getFeatureFactory().getBatteryStatusFeatureProvider();
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mBatteryUsageProgressBarPref = screen.findPreference(getPreferenceKey());
// Set up loading text first to prevent layout flaky before info loaded.
mBatteryUsageProgressBarPref.setBottomSummary(
mContext.getString(R.string.settings_license_activity_loading));
if (com.android.settings.Utils.isBatteryPresent(mContext)) {
quickUpdateHeaderPreference();
} else {
mBatteryUsageProgressBarPref.setVisible(false);
}
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE_UNSEARCHABLE;
}
private CharSequence generateLabel(BatteryInfo info) {
if (Utils.containsIncompatibleChargers(mContext, TAG)) {
return mContext.getString(
com.android.settingslib.R.string.battery_info_status_not_charging);
} else if (BatteryUtils.isBatteryDefenderOn(info)) {
return mContext.getString(
com.android.settingslib.R.string.battery_info_status_charging_on_hold);
} else if (info.remainingLabel == null
|| info.batteryStatus == BatteryManager.BATTERY_STATUS_NOT_CHARGING) {
// Present status only if no remaining time or status anomalous
return info.statusLabel;
} else if (info.statusLabel != null && !info.discharging) {
// Charging state
return mContext.getString(
R.string.battery_state_and_duration, info.statusLabel, info.remainingLabel);
} else if (mPowerManager.isPowerSaveMode()) {
// Power save mode is on
final String powerSaverOn =
mContext.getString(R.string.battery_tip_early_heads_up_done_title);
return mContext.getString(
R.string.battery_state_and_duration, powerSaverOn, info.remainingLabel);
} else if (mBatteryTip != null && mBatteryTip.getType() == BatteryTip.TipType.LOW_BATTERY) {
// Low battery state
final String lowBattery = mContext.getString(R.string.low_battery_summary);
return mContext.getString(
R.string.battery_state_and_duration, lowBattery, info.remainingLabel);
} else {
// Discharging state
return info.remainingLabel;
}
}
public void updateHeaderPreference(BatteryInfo info) {
if (!mBatteryStatusFeatureProvider.triggerBatteryStatusUpdate(this, info)) {
mBatteryUsageProgressBarPref.setBottomSummary(generateLabel(info));
}
mBatteryUsageProgressBarPref.setUsageSummary(
formatBatteryPercentageText(info.batteryLevel));
mBatteryUsageProgressBarPref.setPercent(info.batteryLevel, BATTERY_MAX_LEVEL);
}
/** Callback which receives text for the summary line. */
public void updateBatteryStatus(String label, BatteryInfo info) {
final CharSequence summary = label != null ? label : generateLabel(info);
mBatteryUsageProgressBarPref.setBottomSummary(summary);
Log.d(TAG, "updateBatteryStatus: " + label + " summary: " + summary);
}
public void quickUpdateHeaderPreference() {
Intent batteryBroadcast =
com.android.settingslib.fuelgauge.BatteryUtils.getBatteryIntent(mContext);
final int batteryLevel = Utils.getBatteryLevel(batteryBroadcast);
final boolean discharging =
batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) == 0;
mBatteryUsageProgressBarPref.setUsageSummary(formatBatteryPercentageText(batteryLevel));
mBatteryUsageProgressBarPref.setPercent(batteryLevel, BATTERY_MAX_LEVEL);
}
/** Update summary when battery tips changed. */
public void updateHeaderByBatteryTips(BatteryTip batteryTip, BatteryInfo batteryInfo) {
mBatteryTip = batteryTip;
if (mBatteryTip != null && batteryInfo != null) {
updateHeaderPreference(batteryInfo);
}
}
private CharSequence formatBatteryPercentageText(int batteryLevel) {
return TextUtils.expandTemplate(
mContext.getText(R.string.battery_header_title_alternate),
NumberFormat.getIntegerInstance().format(batteryLevel));
}
}

View File

@@ -0,0 +1,528 @@
/*
* Copyright (C) 2016 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.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.os.AsyncTask;
import android.os.BatteryManager;
import android.os.BatteryStats.HistoryItem;
import android.os.BatteryStatsManager;
import android.os.BatteryUsageStats;
import android.os.SystemClock;
import android.provider.Settings;
import android.text.format.Formatter;
import android.util.Log;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.android.internal.os.BatteryStatsHistoryIterator;
import com.android.settings.Utils;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.UsageView;
import com.android.settingslib.fuelgauge.Estimate;
import com.android.settingslib.fuelgauge.EstimateKt;
import com.android.settingslib.utils.PowerUtil;
import com.android.settingslib.utils.StringUtil;
public class BatteryInfo {
private static final String TAG = "BatteryInfo";
public CharSequence chargeLabel;
public CharSequence remainingLabel;
public int batteryLevel;
public int batteryStatus;
public int pluggedStatus;
public boolean discharging = true;
public boolean isBatteryDefender;
public long remainingTimeUs = 0;
public long averageTimeToDischarge = EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN;
public String batteryPercentString;
public String statusLabel;
public String suggestionLabel;
private boolean mCharging;
private BatteryUsageStats mBatteryUsageStats;
private static final String LOG_TAG = "BatteryInfo";
private long timePeriod;
public interface Callback {
void onBatteryInfoLoaded(BatteryInfo info);
}
public void bindHistory(final UsageView view, BatteryDataParser... parsers) {
final Context context = view.getContext();
BatteryDataParser parser =
new BatteryDataParser() {
SparseIntArray mPoints = new SparseIntArray();
long mStartTime;
int mLastTime = -1;
byte mLastLevel;
@Override
public void onParsingStarted(long startTime, long endTime) {
this.mStartTime = startTime;
timePeriod = endTime - startTime;
view.clearPaths();
// Initially configure the graph for history only.
view.configureGraph((int) timePeriod, 100);
}
@Override
public void onDataPoint(long time, HistoryItem record) {
mLastTime = (int) time;
mLastLevel = record.batteryLevel;
mPoints.put(mLastTime, mLastLevel);
}
@Override
public void onDataGap() {
if (mPoints.size() > 1) {
view.addPath(mPoints);
}
mPoints.clear();
}
@Override
public void onParsingDone() {
onDataGap();
// Add projection if we have an estimate.
if (remainingTimeUs != 0) {
PowerUsageFeatureProvider provider =
FeatureFactory.getFeatureFactory()
.getPowerUsageFeatureProvider();
if (!mCharging
&& provider.isEnhancedBatteryPredictionEnabled(context)) {
mPoints =
provider.getEnhancedBatteryPredictionCurve(
context, mStartTime);
} else {
// Linear extrapolation.
if (mLastTime >= 0) {
mPoints.put(mLastTime, mLastLevel);
mPoints.put(
(int)
(timePeriod
+ PowerUtil.convertUsToMs(
remainingTimeUs)),
mCharging ? 100 : 0);
}
}
}
// If we have a projection, reconfigure the graph to show it.
if (mPoints != null && mPoints.size() > 0) {
int maxTime = mPoints.keyAt(mPoints.size() - 1);
view.configureGraph(maxTime, 100);
view.addProjectedPath(mPoints);
}
}
};
BatteryDataParser[] parserList = new BatteryDataParser[parsers.length + 1];
for (int i = 0; i < parsers.length; i++) {
parserList[i] = parsers[i];
}
parserList[parsers.length] = parser;
parseBatteryHistory(parserList);
String timeString =
context.getString(
com.android.settingslib.R.string.charge_length_format,
Formatter.formatShortElapsedTime(context, timePeriod));
String remaining = "";
if (remainingTimeUs != 0) {
remaining =
context.getString(
com.android.settingslib.R.string.remaining_length_format,
Formatter.formatShortElapsedTime(context, remainingTimeUs / 1000));
}
view.setBottomLabels(new CharSequence[] {timeString, remaining});
}
/** Gets battery info */
public static void getBatteryInfo(
final Context context, final Callback callback, boolean shortString) {
BatteryInfo.getBatteryInfo(context, callback, /* batteryUsageStats */ null, shortString);
}
static long getSettingsChargeTimeRemaining(final Context context) {
return Settings.Global.getLong(
context.getContentResolver(),
com.android.settingslib.fuelgauge.BatteryUtils.GLOBAL_TIME_TO_FULL_MILLIS,
-1);
}
/** Gets battery info */
public static void getBatteryInfo(
final Context context,
final Callback callback,
@Nullable final BatteryUsageStats batteryUsageStats,
boolean shortString) {
new AsyncTask<Void, Void, BatteryInfo>() {
@Override
protected BatteryInfo doInBackground(Void... params) {
boolean shouldCloseBatteryUsageStats = false;
BatteryUsageStats stats;
if (batteryUsageStats != null) {
stats = batteryUsageStats;
} else {
try {
stats =
context.getSystemService(BatteryStatsManager.class)
.getBatteryUsageStats();
shouldCloseBatteryUsageStats = true;
} catch (RuntimeException e) {
Log.e(TAG, "getBatteryInfo() from getBatteryUsageStats()", e);
// Use default BatteryUsageStats.
stats = new BatteryUsageStats.Builder(new String[0]).build();
}
}
final BatteryInfo batteryInfo = getBatteryInfo(context, stats, shortString);
if (shouldCloseBatteryUsageStats) {
try {
stats.close();
} catch (Exception e) {
Log.e(TAG, "BatteryUsageStats.close() failed", e);
}
}
return batteryInfo;
}
@Override
protected void onPostExecute(BatteryInfo batteryInfo) {
final long startTime = System.currentTimeMillis();
callback.onBatteryInfoLoaded(batteryInfo);
BatteryUtils.logRuntime(LOG_TAG, "time for callback", startTime);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/** Creates a BatteryInfo based on BatteryUsageStats */
@WorkerThread
public static BatteryInfo getBatteryInfo(
final Context context,
@NonNull final BatteryUsageStats batteryUsageStats,
boolean shortString) {
final long batteryStatsTime = System.currentTimeMillis();
BatteryUtils.logRuntime(LOG_TAG, "time for getStats", batteryStatsTime);
final long startTime = System.currentTimeMillis();
PowerUsageFeatureProvider provider =
FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider();
final long elapsedRealtimeUs = PowerUtil.convertMsToUs(SystemClock.elapsedRealtime());
final Intent batteryBroadcast =
context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
// 0 means we are discharging, anything else means charging
final boolean discharging =
batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) == 0;
if (discharging && provider.isEnhancedBatteryPredictionEnabled(context)) {
Estimate estimate = provider.getEnhancedBatteryPrediction(context);
if (estimate != null) {
Estimate.storeCachedEstimate(context, estimate);
BatteryUtils.logRuntime(LOG_TAG, "time for enhanced BatteryInfo", startTime);
return BatteryInfo.getBatteryInfo(
context,
batteryBroadcast,
batteryUsageStats,
estimate,
elapsedRealtimeUs,
shortString);
}
}
final long prediction = discharging ? batteryUsageStats.getBatteryTimeRemainingMs() : 0;
final Estimate estimate =
new Estimate(
prediction,
false, /* isBasedOnUsage */
EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN);
BatteryUtils.logRuntime(LOG_TAG, "time for regular BatteryInfo", startTime);
return BatteryInfo.getBatteryInfo(
context,
batteryBroadcast,
batteryUsageStats,
estimate,
elapsedRealtimeUs,
shortString);
}
@WorkerThread
public static BatteryInfo getBatteryInfoOld(
Context context,
Intent batteryBroadcast,
BatteryUsageStats batteryUsageStats,
long elapsedRealtimeUs,
boolean shortString) {
Estimate estimate =
new Estimate(
batteryUsageStats.getBatteryTimeRemainingMs(),
false,
EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN);
return getBatteryInfo(
context,
batteryBroadcast,
batteryUsageStats,
estimate,
elapsedRealtimeUs,
shortString);
}
@WorkerThread
public static BatteryInfo getBatteryInfo(
Context context,
Intent batteryBroadcast,
@NonNull BatteryUsageStats batteryUsageStats,
Estimate estimate,
long elapsedRealtimeUs,
boolean shortString) {
final long startTime = System.currentTimeMillis();
final boolean isCompactStatus =
context.getResources()
.getBoolean(com.android.settings.R.bool.config_use_compact_battery_status);
BatteryInfo info = new BatteryInfo();
info.mBatteryUsageStats = batteryUsageStats;
info.batteryLevel = Utils.getBatteryLevel(batteryBroadcast);
info.batteryPercentString = Utils.formatPercentage(info.batteryLevel);
info.pluggedStatus = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
info.mCharging = info.pluggedStatus != 0;
info.averageTimeToDischarge = estimate.getAverageDischargeTime();
info.isBatteryDefender =
batteryBroadcast.getIntExtra(
BatteryManager.EXTRA_CHARGING_STATUS,
BatteryManager.CHARGING_POLICY_DEFAULT)
== BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE;
info.statusLabel = Utils.getBatteryStatus(context, batteryBroadcast, isCompactStatus);
info.batteryStatus =
batteryBroadcast.getIntExtra(
BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN);
if (!info.mCharging) {
updateBatteryInfoDischarging(context, shortString, estimate, info);
} else {
updateBatteryInfoCharging(
context, batteryBroadcast, batteryUsageStats, info, isCompactStatus);
}
BatteryUtils.logRuntime(LOG_TAG, "time for getBatteryInfo", startTime);
return info;
}
private static void updateBatteryInfoCharging(
Context context,
Intent batteryBroadcast,
BatteryUsageStats stats,
BatteryInfo info,
boolean compactStatus) {
final Resources resources = context.getResources();
final long chargeTimeMs = stats.getChargeTimeRemainingMs();
if (getSettingsChargeTimeRemaining(context) != chargeTimeMs) {
Settings.Global.putLong(
context.getContentResolver(),
com.android.settingslib.fuelgauge.BatteryUtils.GLOBAL_TIME_TO_FULL_MILLIS,
chargeTimeMs);
}
final int status =
batteryBroadcast.getIntExtra(
BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN);
info.discharging = false;
info.suggestionLabel = null;
int dockDefenderMode = BatteryUtils.getCurrentDockDefenderMode(context, info);
if ((info.isBatteryDefender
&& status != BatteryManager.BATTERY_STATUS_FULL
&& dockDefenderMode == BatteryUtils.DockDefenderMode.DISABLED)
|| dockDefenderMode == BatteryUtils.DockDefenderMode.ACTIVE) {
// Battery defender active, battery charging paused
info.remainingLabel = null;
int chargingLimitedResId = com.android.settingslib.R.string.power_charging_limited;
info.chargeLabel = context.getString(chargingLimitedResId, info.batteryPercentString);
} else if ((chargeTimeMs > 0
&& status != BatteryManager.BATTERY_STATUS_FULL
&& dockDefenderMode == BatteryUtils.DockDefenderMode.DISABLED)
|| dockDefenderMode == BatteryUtils.DockDefenderMode.TEMPORARILY_BYPASSED) {
// Battery is charging to full
info.remainingTimeUs = PowerUtil.convertMsToUs(chargeTimeMs);
final CharSequence timeString =
StringUtil.formatElapsedTime(
context,
(double) PowerUtil.convertUsToMs(info.remainingTimeUs),
false /* withSeconds */,
true /* collapseTimeUnit */);
int resId = com.android.settingslib.R.string.power_charging_duration;
info.remainingLabel =
chargeTimeMs <= 0
? null
: context.getString(
com.android.settingslib.R.string
.power_remaining_charging_duration_only,
timeString);
info.chargeLabel =
chargeTimeMs <= 0
? info.batteryPercentString
: context.getString(resId, info.batteryPercentString, timeString);
} else if (dockDefenderMode == BatteryUtils.DockDefenderMode.FUTURE_BYPASS) {
// Dock defender will be triggered in the future, charging will be optimized.
info.chargeLabel =
context.getString(
com.android.settingslib.R.string.power_charging_future_paused,
info.batteryPercentString);
} else {
final String chargeStatusLabel =
Utils.getBatteryStatus(context, batteryBroadcast, compactStatus);
info.remainingLabel = null;
info.chargeLabel =
info.batteryLevel == 100
? info.batteryPercentString
: resources.getString(
com.android.settingslib.R.string.power_charging,
info.batteryPercentString,
chargeStatusLabel);
}
}
private static void updateBatteryInfoDischarging(
Context context, boolean shortString, Estimate estimate, BatteryInfo info) {
final long drainTimeUs = PowerUtil.convertMsToUs(estimate.getEstimateMillis());
if (drainTimeUs > 0) {
info.remainingTimeUs = drainTimeUs;
info.remainingLabel =
PowerUtil.getBatteryRemainingShortStringFormatted(
context, PowerUtil.convertUsToMs(drainTimeUs));
info.chargeLabel = info.remainingLabel;
info.suggestionLabel =
PowerUtil.getBatteryTipStringFormatted(
context, PowerUtil.convertUsToMs(drainTimeUs));
} else {
info.remainingLabel = null;
info.suggestionLabel = null;
info.chargeLabel = info.batteryPercentString;
}
}
public interface BatteryDataParser {
void onParsingStarted(long startTime, long endTime);
void onDataPoint(long time, HistoryItem record);
void onDataGap();
void onParsingDone();
}
/**
* Iterates over battery history included in the BatteryUsageStats that this object was
* initialized with.
*/
public void parseBatteryHistory(BatteryDataParser... parsers) {
long startWalltime = 0;
long endWalltime = 0;
long historyStart = 0;
long historyEnd = 0;
long curWalltime = startWalltime;
long lastWallTime = 0;
long lastRealtime = 0;
int lastInteresting = 0;
int pos = 0;
boolean first = true;
final BatteryStatsHistoryIterator iterator1 =
mBatteryUsageStats.iterateBatteryStatsHistory();
HistoryItem rec;
while ((rec = iterator1.next()) != null) {
pos++;
if (first) {
first = false;
historyStart = rec.time;
}
if (rec.cmd == HistoryItem.CMD_CURRENT_TIME || rec.cmd == HistoryItem.CMD_RESET) {
// If there is a ridiculously large jump in time, then we won't be
// able to create a good chart with that data, so just ignore the
// times we got before and pretend like our data extends back from
// the time we have now.
// Also, if we are getting a time change and we are less than 5 minutes
// since the start of the history real time, then also use this new
// time to compute the base time, since whatever time we had before is
// pretty much just noise.
if (rec.currentTime > (lastWallTime + (180 * 24 * 60 * 60 * 1000L))
|| rec.time < (historyStart + (5 * 60 * 1000L))) {
startWalltime = 0;
}
lastWallTime = rec.currentTime;
lastRealtime = rec.time;
if (startWalltime == 0) {
startWalltime = lastWallTime - (lastRealtime - historyStart);
}
}
if (rec.isDeltaData()) {
lastInteresting = pos;
historyEnd = rec.time;
}
}
endWalltime = lastWallTime + historyEnd - lastRealtime;
int i = 0;
final int N = lastInteresting;
for (int j = 0; j < parsers.length; j++) {
parsers[j].onParsingStarted(startWalltime, endWalltime);
}
if (endWalltime > startWalltime) {
final BatteryStatsHistoryIterator iterator2 =
mBatteryUsageStats.iterateBatteryStatsHistory();
while ((rec = iterator2.next()) != null && i < N) {
if (rec.isDeltaData()) {
curWalltime += rec.time - lastRealtime;
lastRealtime = rec.time;
long x = (curWalltime - startWalltime);
if (x < 0) {
x = 0;
}
for (int j = 0; j < parsers.length; j++) {
parsers[j].onDataPoint(x, rec);
}
} else {
long lastWalltime = curWalltime;
if (rec.cmd == HistoryItem.CMD_CURRENT_TIME
|| rec.cmd == HistoryItem.CMD_RESET) {
if (rec.currentTime >= startWalltime) {
curWalltime = rec.currentTime;
} else {
curWalltime = startWalltime + (rec.time - historyStart);
}
lastRealtime = rec.time;
}
if (rec.cmd != HistoryItem.CMD_OVERFLOW
&& (rec.cmd != HistoryItem.CMD_CURRENT_TIME
|| Math.abs(lastWalltime - curWalltime) > (60 * 60 * 1000))) {
for (int j = 0; j < parsers.length; j++) {
parsers[j].onDataGap();
}
}
}
i++;
}
}
for (int j = 0; j < parsers.length; j++) {
parsers[j].onParsingDone();
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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.content.Context;
import androidx.annotation.VisibleForTesting;
import com.android.settingslib.utils.AsyncLoaderCompat;
/**
* Loader that can be used by classes to load BatteryInfo in a background thread. This loader will
* automatically grab enhanced battery estimates if available or fall back to the system estimate
* when not available.
*/
public class BatteryInfoLoader extends AsyncLoaderCompat<BatteryInfo> {
private static final String LOG_TAG = "BatteryInfoLoader";
@VisibleForTesting BatteryUtils mBatteryUtils;
public BatteryInfoLoader(Context context) {
super(context);
mBatteryUtils = BatteryUtils.getInstance(context);
}
@Override
protected void onDiscardResult(BatteryInfo result) {}
@Override
public BatteryInfo loadInBackground() {
return mBatteryUtils.getBatteryInfo(LOG_TAG);
}
}

View File

@@ -0,0 +1,133 @@
/*
* 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.content.Context;
import android.graphics.ColorFilter;
import android.util.AttributeSet;
import android.widget.ImageView;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settingslib.graph.ThemedBatteryDrawable;
public class BatteryMeterView extends ImageView {
@VisibleForTesting BatteryMeterDrawable mDrawable;
@VisibleForTesting ColorFilter mErrorColorFilter;
@VisibleForTesting ColorFilter mAccentColorFilter;
@VisibleForTesting ColorFilter mForegroundColorFilter;
public BatteryMeterView(Context context) {
this(context, null, 0);
}
public BatteryMeterView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public BatteryMeterView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
final int frameColor =
context.getColor(com.android.settingslib.R.color.meter_background_color);
mAccentColorFilter =
Utils.getAlphaInvariantColorFilterForColor(
Utils.getColorAttrDefaultColor(context, android.R.attr.colorAccent));
mErrorColorFilter =
Utils.getAlphaInvariantColorFilterForColor(
context.getColor(R.color.battery_icon_color_error));
mForegroundColorFilter =
Utils.getAlphaInvariantColorFilterForColor(
Utils.getColorAttrDefaultColor(context, android.R.attr.colorForeground));
mDrawable = new BatteryMeterDrawable(context, frameColor);
mDrawable.setColorFilter(mAccentColorFilter);
setImageDrawable(mDrawable);
}
public void setBatteryLevel(int level) {
mDrawable.setBatteryLevel(level);
updateColorFilter();
}
public void setPowerSave(boolean powerSave) {
mDrawable.setPowerSaveEnabled(powerSave);
updateColorFilter();
}
public boolean getPowerSave() {
return mDrawable.getPowerSaveEnabled();
}
public int getBatteryLevel() {
return mDrawable.getBatteryLevel();
}
public void setCharging(boolean charging) {
mDrawable.setCharging(charging);
postInvalidate();
}
public boolean getCharging() {
return mDrawable.getCharging();
}
private void updateColorFilter() {
final boolean powerSaveEnabled = mDrawable.getPowerSaveEnabled();
final int level = mDrawable.getBatteryLevel();
if (powerSaveEnabled) {
mDrawable.setColorFilter(mForegroundColorFilter);
} else if (level < mDrawable.getCriticalLevel()) {
mDrawable.setColorFilter(mErrorColorFilter);
} else {
mDrawable.setColorFilter(mAccentColorFilter);
}
}
public static class BatteryMeterDrawable extends ThemedBatteryDrawable {
private final int mIntrinsicWidth;
private final int mIntrinsicHeight;
public BatteryMeterDrawable(Context context, int frameColor) {
super(context, frameColor);
mIntrinsicWidth =
context.getResources().getDimensionPixelSize(R.dimen.battery_meter_width);
mIntrinsicHeight =
context.getResources().getDimensionPixelSize(R.dimen.battery_meter_height);
}
public BatteryMeterDrawable(Context context, int frameColor, int width, int height) {
super(context, frameColor);
mIntrinsicWidth = width;
mIntrinsicHeight = height;
}
@Override
public int getIntrinsicWidth() {
return mIntrinsicWidth;
}
@Override
public int getIntrinsicHeight() {
return mIntrinsicHeight;
}
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright (C) 2022 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.content.Context;
import android.content.SharedPreferences;
import android.util.Base64;
import androidx.annotation.VisibleForTesting;
import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
import com.android.settings.fuelgauge.batteryusage.ConvertUtils;
import java.io.PrintWriter;
import java.util.List;
/** Writes and reads a historical log of battery related state change events. */
public final class BatteryOptimizeLogUtils {
private static final String TAG = "BatteryOptimizeLogUtils";
private static final String BATTERY_OPTIMIZE_FILE_NAME = "battery_optimize_historical_logs";
private static final String LOGS_KEY = "battery_optimize_logs_key";
@VisibleForTesting static final int MAX_ENTRIES = 40;
private BatteryOptimizeLogUtils() {}
/** Writes a log entry for battery optimization mode. */
static void writeLog(
Context context, Action action, String packageName, String actionDescription) {
writeLog(getSharedPreferences(context), action, packageName, actionDescription);
}
static void writeLog(
SharedPreferences sharedPreferences,
Action action,
String packageName,
String actionDescription) {
writeLog(
sharedPreferences,
BatteryOptimizeHistoricalLogEntry.newBuilder()
.setPackageName(packageName)
.setAction(action)
.setActionDescription(actionDescription)
.setTimestamp(System.currentTimeMillis())
.build());
}
private static void writeLog(
SharedPreferences sharedPreferences, BatteryOptimizeHistoricalLogEntry logEntry) {
BatteryOptimizeHistoricalLog existingLog =
parseLogFromString(sharedPreferences.getString(LOGS_KEY, ""));
BatteryOptimizeHistoricalLog.Builder newLogBuilder = existingLog.toBuilder();
// Prune old entries to limit the max logging data count.
if (existingLog.getLogEntryCount() >= MAX_ENTRIES) {
newLogBuilder.removeLogEntry(0);
}
newLogBuilder.addLogEntry(logEntry);
String loggingContent =
Base64.encodeToString(newLogBuilder.build().toByteArray(), Base64.DEFAULT);
sharedPreferences.edit().putString(LOGS_KEY, loggingContent).apply();
}
private static BatteryOptimizeHistoricalLog parseLogFromString(String storedLogs) {
return BatteryUtils.parseProtoFromString(
storedLogs, BatteryOptimizeHistoricalLog.getDefaultInstance());
}
/** Prints the historical log that has previously been stored by this utility. */
public static void printBatteryOptimizeHistoricalLog(Context context, PrintWriter writer) {
printBatteryOptimizeHistoricalLog(getSharedPreferences(context), writer);
}
/** Prints the historical log that has previously been stored by this utility. */
public static void printBatteryOptimizeHistoricalLog(
SharedPreferences sharedPreferences, PrintWriter writer) {
writer.println("Battery optimize state history:");
BatteryOptimizeHistoricalLog existingLog =
parseLogFromString(sharedPreferences.getString(LOGS_KEY, ""));
List<BatteryOptimizeHistoricalLogEntry> logEntryList = existingLog.getLogEntryList();
if (logEntryList.isEmpty()) {
writer.println("\tnothing to dump");
} else {
writer.println("0:UNKNOWN 1:RESTRICTED 2:UNRESTRICTED 3:OPTIMIZED");
logEntryList.forEach(entry -> writer.println(toString(entry)));
}
}
/** Gets the unique key for logging. */
static String getPackageNameWithUserId(String packageName, int userId) {
return packageName + ":" + userId;
}
private static String toString(BatteryOptimizeHistoricalLogEntry entry) {
return String.format(
"%s\t%s\taction:%s\tevent:%s",
ConvertUtils.utcToLocalTimeForLogging(entry.getTimestamp()),
entry.getPackageName(),
entry.getAction(),
entry.getActionDescription());
}
@VisibleForTesting
static SharedPreferences getSharedPreferences(Context context) {
return context.getApplicationContext()
.getSharedPreferences(BATTERY_OPTIMIZE_FILE_NAME, Context.MODE_PRIVATE);
}
}

View File

@@ -0,0 +1,365 @@
/*
* Copyright (C) 2021 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.annotation.IntDef;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.pm.UserInfo;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
import com.android.settingslib.fuelgauge.PowerAllowlistBackend;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.List;
/** A utility class for application usage operation. */
public class BatteryOptimizeUtils {
private static final String TAG = "BatteryOptimizeUtils";
private static final String UNKNOWN_PACKAGE = "unknown";
// Avoid reload the data again since it is predefined in the resource/config.
private static List<String> sBatteryOptimizeModeList = null;
private static List<String> sBatteryUnrestrictModeList = null;
@VisibleForTesting AppOpsManager mAppOpsManager;
@VisibleForTesting BatteryUtils mBatteryUtils;
@VisibleForTesting PowerAllowlistBackend mPowerAllowListBackend;
@VisibleForTesting int mMode;
@VisibleForTesting boolean mAllowListed;
private final String mPackageName;
private final Context mContext;
private final int mUid;
// If current user is admin, match apps from all users. Otherwise, only match the currect user.
private static final int RETRIEVE_FLAG_ADMIN =
PackageManager.MATCH_ANY_USER
| PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
private static final int RETRIEVE_FLAG =
PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
// Optimization modes.
public static final int MODE_UNKNOWN = 0;
public static final int MODE_RESTRICTED = 1;
public static final int MODE_UNRESTRICTED = 2;
public static final int MODE_OPTIMIZED = 3;
@IntDef(
prefix = {"MODE_"},
value = {
MODE_UNKNOWN,
MODE_RESTRICTED,
MODE_UNRESTRICTED,
MODE_OPTIMIZED,
})
@Retention(RetentionPolicy.SOURCE)
static @interface OptimizationMode {}
public BatteryOptimizeUtils(Context context, int uid, String packageName) {
mUid = uid;
mContext = context;
mPackageName = packageName;
mAppOpsManager = context.getSystemService(AppOpsManager.class);
mBatteryUtils = BatteryUtils.getInstance(context);
mPowerAllowListBackend = PowerAllowlistBackend.getInstance(context);
mMode = getMode(mAppOpsManager, mUid, mPackageName);
mAllowListed = mPowerAllowListBackend.isAllowlisted(mPackageName, mUid);
}
/** Gets the {@link OptimizationMode} based on mode and allowed list. */
@OptimizationMode
public static int getAppOptimizationMode(int mode, boolean isAllowListed) {
if (!isAllowListed && mode == AppOpsManager.MODE_IGNORED) {
return MODE_RESTRICTED;
} else if (isAllowListed && mode == AppOpsManager.MODE_ALLOWED) {
return MODE_UNRESTRICTED;
} else if (!isAllowListed && mode == AppOpsManager.MODE_ALLOWED) {
return MODE_OPTIMIZED;
} else {
return MODE_UNKNOWN;
}
}
/** Gets the {@link OptimizationMode} for associated app. */
@OptimizationMode
public int getAppOptimizationMode(boolean refreshList) {
if (refreshList) {
mPowerAllowListBackend.refreshList();
}
mAllowListed = mPowerAllowListBackend.isAllowlisted(mPackageName, mUid);
mMode =
mAppOpsManager.checkOpNoThrow(
AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mUid, mPackageName);
Log.d(
TAG,
String.format(
"refresh %s state, allowlisted = %s, mode = %d",
mPackageName, mAllowListed, mMode));
return getAppOptimizationMode(mMode, mAllowListed);
}
/** Gets the {@link OptimizationMode} for associated app. */
@OptimizationMode
public int getAppOptimizationMode() {
return getAppOptimizationMode(true);
}
/** Resets optimization mode for all applications. */
public static void resetAppOptimizationMode(
Context context, IPackageManager ipm, AppOpsManager aom) {
resetAppOptimizationMode(
context,
ipm,
aom,
PowerAllowlistBackend.getInstance(context),
BatteryUtils.getInstance(context));
}
/** Sets the {@link OptimizationMode} for associated app. */
public void setAppUsageState(@OptimizationMode int mode, Action action) {
if (getAppOptimizationMode() == mode) {
Log.w(TAG, "set the same optimization mode for: " + mPackageName);
return;
}
setAppUsageStateInternal(
mContext, mode, mUid, mPackageName, mBatteryUtils, mPowerAllowListBackend, action);
}
/** Return {@code true} if it is disabled for default optimized mode only. */
public boolean isDisabledForOptimizeModeOnly() {
return getForceBatteryOptimizeModeList(mContext).contains(mPackageName)
|| mBatteryUtils.getPackageUid(mPackageName) == BatteryUtils.UID_NULL;
}
/** Return {@code true} if this package is system or default active app. */
public boolean isSystemOrDefaultApp() {
mPowerAllowListBackend.refreshList();
return isSystemOrDefaultApp(mContext, mPowerAllowListBackend, mPackageName, mUid);
}
/** Return {@code true} if the optimization mode of this package can be changed */
public boolean isOptimizeModeMutable() {
return !isDisabledForOptimizeModeOnly() && !isSystemOrDefaultApp();
}
/**
* Return {@code true} if the optimization mode is mutable and current state is not restricted
*/
public boolean isSelectorPreferenceEnabled() {
// Enable the preference if apps are not set into restricted mode, otherwise disable it
return isOptimizeModeMutable()
&& getAppOptimizationMode() != BatteryOptimizeUtils.MODE_RESTRICTED;
}
/** Gets the list of installed applications. */
public static ArraySet<ApplicationInfo> getInstalledApplications(
Context context, IPackageManager ipm) {
final ArraySet<ApplicationInfo> applications = new ArraySet<>();
final UserManager um = context.getSystemService(UserManager.class);
for (UserInfo userInfo : um.getProfiles(UserHandle.myUserId())) {
try {
@SuppressWarnings("unchecked")
final ParceledListSlice<ApplicationInfo> infoList =
ipm.getInstalledApplications(
userInfo.isAdmin() ? RETRIEVE_FLAG_ADMIN : RETRIEVE_FLAG,
userInfo.id);
if (infoList != null) {
applications.addAll(infoList.getList());
}
} catch (Exception e) {
Log.e(TAG, "getInstalledApplications() is failed", e);
return null;
}
}
// Removes the application which is disabled by the system.
applications.removeIf(
info ->
info.enabledSetting != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
&& !info.enabled);
return applications;
}
@VisibleForTesting
static void resetAppOptimizationMode(
Context context,
IPackageManager ipm,
AppOpsManager aom,
PowerAllowlistBackend allowlistBackend,
BatteryUtils batteryUtils) {
final ArraySet<ApplicationInfo> applications = getInstalledApplications(context, ipm);
if (applications == null || applications.isEmpty()) {
Log.w(TAG, "no data found in the getInstalledApplications()");
return;
}
allowlistBackend.refreshList();
// Resets optimization mode for each application.
for (ApplicationInfo info : applications) {
final int mode =
aom.checkOpNoThrow(
AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, info.uid, info.packageName);
@OptimizationMode
final int optimizationMode =
getAppOptimizationMode(
mode, allowlistBackend.isAllowlisted(info.packageName, info.uid));
// Ignores default optimized/unknown state or system/default apps.
if (optimizationMode == MODE_OPTIMIZED
|| optimizationMode == MODE_UNKNOWN
|| isSystemOrDefaultApp(
context, allowlistBackend, info.packageName, info.uid)) {
continue;
}
// Resets to the default mode: MODE_OPTIMIZED.
setAppUsageStateInternal(
context,
MODE_OPTIMIZED,
info.uid,
info.packageName,
batteryUtils,
allowlistBackend,
Action.RESET);
}
}
String getPackageName() {
return mPackageName == null ? UNKNOWN_PACKAGE : mPackageName;
}
static int getMode(AppOpsManager appOpsManager, int uid, String packageName) {
return appOpsManager.checkOpNoThrow(
AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName);
}
static boolean isSystemOrDefaultApp(
Context context,
PowerAllowlistBackend powerAllowlistBackend,
String packageName,
int uid) {
return powerAllowlistBackend.isSysAllowlisted(packageName)
// Always forced unrestricted apps are one type of system important apps.
|| getForceBatteryUnrestrictModeList(context).contains(packageName)
|| powerAllowlistBackend.isDefaultActiveApp(packageName, uid);
}
static List<String> getForceBatteryOptimizeModeList(Context context) {
if (sBatteryOptimizeModeList == null) {
sBatteryOptimizeModeList =
Arrays.asList(
context.getResources()
.getStringArray(
R.array.config_force_battery_optimize_mode_apps));
}
return sBatteryOptimizeModeList;
}
static List<String> getForceBatteryUnrestrictModeList(Context context) {
if (sBatteryUnrestrictModeList == null) {
sBatteryUnrestrictModeList =
Arrays.asList(
context.getResources()
.getStringArray(
R.array.config_force_battery_unrestrict_mode_apps));
}
return sBatteryUnrestrictModeList;
}
private static void setAppUsageStateInternal(
Context context,
@OptimizationMode int mode,
int uid,
String packageName,
BatteryUtils batteryUtils,
PowerAllowlistBackend powerAllowlistBackend,
Action action) {
if (mode == MODE_UNKNOWN) {
Log.d(TAG, "set unknown app optimization mode.");
return;
}
// MODE_RESTRICTED = AppOpsManager.MODE_IGNORED + !allowListed
// MODE_UNRESTRICTED = AppOpsManager.MODE_ALLOWED + allowListed
// MODE_OPTIMIZED = AppOpsManager.MODE_ALLOWED + !allowListed
final int appOpsManagerMode =
mode == MODE_RESTRICTED ? AppOpsManager.MODE_IGNORED : AppOpsManager.MODE_ALLOWED;
final boolean allowListed = mode == MODE_UNRESTRICTED;
setAppOptimizationModeInternal(
context,
appOpsManagerMode,
allowListed,
uid,
packageName,
batteryUtils,
powerAllowlistBackend,
action);
}
private static void setAppOptimizationModeInternal(
Context context,
int appStandbyMode,
boolean allowListed,
int uid,
String packageName,
BatteryUtils batteryUtils,
PowerAllowlistBackend powerAllowlistBackend,
Action action) {
final String packageNameKey =
BatteryOptimizeLogUtils.getPackageNameWithUserId(
packageName, UserHandle.myUserId());
try {
batteryUtils.setForceAppStandby(uid, packageName, appStandbyMode);
if (allowListed) {
powerAllowlistBackend.addApp(packageName);
} else {
powerAllowlistBackend.removeApp(packageName);
}
} catch (Exception e) {
// Error cases, set standby mode as -1 for logging.
appStandbyMode = -1;
Log.e(TAG, "set OPTIMIZATION MODE failed for " + packageName, e);
}
BatteryOptimizeLogUtils.writeLog(
context, action, packageNameKey, createLogEvent(appStandbyMode, allowListed));
}
private static String createLogEvent(int appStandbyMode, boolean allowListed) {
return appStandbyMode < 0
? "Apply optimize setting ERROR"
: String.format(
"\tStandbyMode: %s, allowListed: %s, mode: %s",
appStandbyMode,
allowListed,
getAppOptimizationMode(appStandbyMode, allowListed));
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2020 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;
/** Common interface for a preference controller that updates battery status */
public interface BatteryPreferenceController {
/**
* Updates the label for the preference controller. If the label is null, the implementation
* should revert back to the original label based on the battery info.
*/
void updateBatteryStatus(String label, BatteryInfo info);
}

View File

@@ -0,0 +1,136 @@
/*
* 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.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.provider.Settings;
import android.provider.Settings.Global;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
import com.android.settingslib.fuelgauge.BatterySaverUtils;
/** Controller to update the battery saver entry preference. */
public class BatterySaverController extends BasePreferenceController
implements LifecycleObserver, OnStart, OnStop, BatterySaverReceiver.BatterySaverListener {
private static final String KEY_BATTERY_SAVER = "battery_saver_summary";
private final BatterySaverReceiver mBatteryStateChangeReceiver;
private final PowerManager mPowerManager;
private Preference mBatterySaverPref;
private final ContentObserver mObserver =
new ContentObserver(new Handler(Looper.getMainLooper())) {
@Override
public void onChange(boolean selfChange) {
updateSummary();
}
};
public BatterySaverController(Context context) {
super(context, KEY_BATTERY_SAVER);
mPowerManager = mContext.getSystemService(PowerManager.class);
mBatteryStateChangeReceiver = new BatterySaverReceiver(context);
mBatteryStateChangeReceiver.setBatterySaverListener(this);
BatterySaverUtils.revertScheduleToNoneIfNeeded(context);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@Override
public String getPreferenceKey() {
return KEY_BATTERY_SAVER;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mBatterySaverPref = screen.findPreference(KEY_BATTERY_SAVER);
}
@Override
public void onStart() {
mContext.getContentResolver()
.registerContentObserver(
Settings.Global.getUriFor(Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL),
true /* notifyForDescendants */,
mObserver);
mBatteryStateChangeReceiver.setListening(true);
updateSummary();
}
@Override
public void onStop() {
mContext.getContentResolver().unregisterContentObserver(mObserver);
mBatteryStateChangeReceiver.setListening(false);
}
@Override
public CharSequence getSummary() {
final boolean isPowerSaveOn = mPowerManager.isPowerSaveMode();
if (isPowerSaveOn) {
return mContext.getString(R.string.battery_saver_on_summary);
}
final ContentResolver resolver = mContext.getContentResolver();
final int mode =
Settings.Global.getInt(
resolver,
Global.AUTOMATIC_POWER_SAVE_MODE,
PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
if (mode == PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE) {
final int percent =
Settings.Global.getInt(
resolver, Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
return percent != 0
? mContext.getString(
R.string.battery_saver_off_scheduled_summary,
Utils.formatPercentage(percent))
: mContext.getString(R.string.battery_saver_off_summary);
} else {
return mContext.getString(R.string.battery_saver_pref_auto_routine_summary);
}
}
private void updateSummary() {
if (mBatterySaverPref != null) {
mBatterySaverPref.setSummary(getSummary());
}
}
@Override
public void onPowerSaveModeChanged() {
updateSummary();
}
@Override
public void onBatteryChanged(boolean pluggedIn) {}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2018 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.content.Context;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import com.android.settingslib.Utils;
import com.android.settingslib.graph.BatteryMeterDrawableBase;
/** Drawable that shows a static battery saver icon - a full battery symbol and a plus sign. */
public class BatterySaverDrawable extends BatteryMeterDrawableBase {
private static final int MAX_BATTERY = 100;
public BatterySaverDrawable(Context context, int frameColor) {
super(context, frameColor);
// Show as full so it's always uniform color
setBatteryLevel(MAX_BATTERY);
setPowerSave(true);
setCharging(false);
setPowerSaveAsColorError(false);
final int tintColor = Utils.getColorAttrDefaultColor(context, android.R.attr.colorAccent);
setColorFilter(new PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN));
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2015 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 static android.provider.Settings.EXTRA_BATTERY_SAVER_MODE_ENABLED;
import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_VOICE;
import android.content.Intent;
import android.util.Log;
import com.android.settings.utils.VoiceSettingsActivity;
import com.android.settingslib.fuelgauge.BatterySaverUtils;
/**
* Activity for modifying the {@link android.os.PowerManager} power save mode setting using the
* Voice Interaction API.
*/
public class BatterySaverModeVoiceActivity extends VoiceSettingsActivity {
private static final String TAG = "BatterySaverModeVoiceActivity";
@Override
protected boolean onVoiceSettingInteraction(Intent intent) {
if (intent.hasExtra(EXTRA_BATTERY_SAVER_MODE_ENABLED)) {
if (BatterySaverUtils.setPowerSaveMode(
this,
intent.getBooleanExtra(EXTRA_BATTERY_SAVER_MODE_ENABLED, false),
/* needFirstTimeWarning= */ true,
SAVER_ENABLED_VOICE)) {
notifySuccess(null);
} else {
Log.v(TAG, "Unable to set power mode");
notifyFailure(null);
}
} else {
Log.v(TAG, "Missing battery saver mode extra");
}
return true;
}
}

View File

@@ -0,0 +1,77 @@
/*
* 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.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.PowerManager;
import android.util.Log;
public class BatterySaverReceiver extends BroadcastReceiver {
private static final String TAG = "BatterySaverReceiver";
private static final boolean DEBUG = false;
private boolean mRegistered;
private Context mContext;
private BatterySaverListener mBatterySaverListener;
public BatterySaverReceiver(Context context) {
mContext = context;
}
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG) Log.d(TAG, "Received " + intent.getAction());
String action = intent.getAction();
if (PowerManager.ACTION_POWER_SAVE_MODE_CHANGED.equals(action)) {
if (mBatterySaverListener != null) {
mBatterySaverListener.onPowerSaveModeChanged();
}
} else if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
// disable BSM switch if phone is plugged in
if (mBatterySaverListener != null) {
final boolean pluggedIn = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0;
mBatterySaverListener.onBatteryChanged(pluggedIn);
}
}
}
public void setListening(boolean listening) {
if (listening && !mRegistered) {
final IntentFilter ifilter = new IntentFilter();
ifilter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
ifilter.addAction(Intent.ACTION_BATTERY_CHANGED);
mContext.registerReceiver(this, ifilter);
mRegistered = true;
} else if (!listening && mRegistered) {
mContext.unregisterReceiver(this);
mRegistered = false;
}
}
public void setBatterySaverListener(BatterySaverListener lsn) {
mBatterySaverListener = lsn;
}
public interface BatterySaverListener {
void onPowerSaveModeChanged();
void onBatteryChanged(boolean pluggedIn);
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright (C) 2021 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.content.Context;
import com.android.settings.fuelgauge.batterytip.BatteryTipPolicy;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import java.util.List;
/** Feature provider for battery settings usage. */
public interface BatterySettingsFeatureProvider {
/** Returns true if manufacture date should be shown */
boolean isManufactureDateAvailable(Context context, long manufactureDateMs);
/** Returns true if first use date should be shown */
boolean isFirstUseDateAvailable(Context context, long firstUseDateMs);
/** Check whether the battery information page is enabled in the About phone page */
boolean isBatteryInfoEnabled(Context context);
/** A way to add more battery tip detectors. */
void addBatteryTipDetector(
Context context,
List<BatteryTip> batteryTips,
BatteryInfo batteryInfo,
BatteryTipPolicy batteryTipPolicy);
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright (C) 2021 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.content.Context;
import com.android.settings.fuelgauge.batterytip.BatteryTipPolicy;
import com.android.settings.fuelgauge.batterytip.detectors.LowBatteryDetector;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import java.util.List;
/** Feature provider implementation for battery settings usage. */
public class BatterySettingsFeatureProviderImpl implements BatterySettingsFeatureProvider {
@Override
public boolean isManufactureDateAvailable(Context context, long manufactureDateMs) {
return false;
}
@Override
public boolean isFirstUseDateAvailable(Context context, long firstUseDateMs) {
return false;
}
@Override
public boolean isBatteryInfoEnabled(Context context) {
return false;
}
@Override
public void addBatteryTipDetector(
Context context,
List<BatteryTip> batteryTips,
BatteryInfo batteryInfo,
BatteryTipPolicy batteryTipPolicy) {
batteryTips.add(new LowBatteryDetector(context, batteryTipPolicy, batteryInfo).detect());
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright (C) 2022 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.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.provider.Settings;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.settings.fuelgauge.batterysaver.BatterySaverScheduleRadioButtonsController;
import com.android.settings.fuelgauge.datasaver.DynamicDenylistManager;
import com.android.settingslib.fuelgauge.BatterySaverUtils;
import java.util.List;
/** Execute battery settings migration tasks in the device booting stage. */
public final class BatterySettingsMigrateChecker extends BroadcastReceiver {
private static final String TAG = "BatterySettingsMigrateChecker";
@VisibleForTesting static BatteryOptimizeUtils sBatteryOptimizeUtils = null;
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "onReceive: " + intent + " owner: " + BatteryBackupHelper.isOwner());
if (intent != null
&& Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())
&& BatteryBackupHelper.isOwner()) {
verifyConfiguration(context);
}
}
static void verifyConfiguration(Context context) {
context = context.getApplicationContext();
verifySaverConfiguration(context);
verifyBatteryOptimizeModes(context);
DynamicDenylistManager.getInstance(context).onBootComplete();
}
/** Avoid users set important apps into the unexpected battery optimize modes */
static void verifyBatteryOptimizeModes(Context context) {
Log.d(TAG, "invoke verifyOptimizationModes()");
verifyBatteryOptimizeModeApps(
context,
BatteryOptimizeUtils.MODE_OPTIMIZED,
BatteryOptimizeUtils.getForceBatteryOptimizeModeList(context));
verifyBatteryOptimizeModeApps(
context,
BatteryOptimizeUtils.MODE_UNRESTRICTED,
BatteryOptimizeUtils.getForceBatteryUnrestrictModeList(context));
}
@VisibleForTesting
static void verifyBatteryOptimizeModeApps(
Context context,
@BatteryOptimizeUtils.OptimizationMode int optimizationMode,
List<String> allowList) {
allowList.forEach(
packageName -> {
final BatteryOptimizeUtils batteryOptimizeUtils =
BatteryBackupHelper.newBatteryOptimizeUtils(
context,
packageName,
/* testOptimizeUtils */ sBatteryOptimizeUtils);
if (batteryOptimizeUtils == null) {
return;
}
if (batteryOptimizeUtils.getAppOptimizationMode() != optimizationMode) {
Log.w(
TAG,
"Reset " + packageName + " battery mode into " + optimizationMode);
batteryOptimizeUtils.setAppUsageState(
optimizationMode,
BatteryOptimizeHistoricalLogEntry.Action.FORCE_RESET);
}
});
}
static void verifySaverConfiguration(Context context) {
Log.d(TAG, "invoke verifySaverConfiguration()");
final ContentResolver resolver = context.getContentResolver();
final int threshold =
Settings.Global.getInt(resolver, Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
// Force refine the invalid scheduled battery level.
if (threshold < BatterySaverScheduleRadioButtonsController.TRIGGER_LEVEL_MIN
&& threshold > 0) {
Settings.Global.putInt(
resolver,
Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL,
BatterySaverScheduleRadioButtonsController.TRIGGER_LEVEL_MIN);
Log.w(TAG, "Reset invalid scheduled battery level from: " + threshold);
}
// Force removing the 'schedule by routine' state.
BatterySaverUtils.revertScheduleToNoneIfNeeded(context);
}
}

View File

@@ -0,0 +1,370 @@
/*
* Copyright (C) 2024 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 static java.nio.charset.StandardCharsets.UTF_8;
import android.app.AppGlobals;
import android.app.AppOpsManager;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.os.IDeviceIdleController;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.datastore.BackupContext;
import com.android.settingslib.datastore.BackupRestoreEntity;
import com.android.settingslib.datastore.BackupRestoreStorageManager;
import com.android.settingslib.datastore.EntityBackupResult;
import com.android.settingslib.datastore.ObservableBackupRestoreStorage;
import com.android.settingslib.datastore.RestoreContext;
import com.android.settingslib.fuelgauge.PowerAllowlistBackend;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/** An implementation to backup and restore battery configurations. */
public final class BatterySettingsStorage extends ObservableBackupRestoreStorage {
public static final String TAG = "BatteryBackupHelper";
// Definition for the device build information.
public static final String KEY_BUILD_BRAND = "device_build_brand";
public static final String KEY_BUILD_PRODUCT = "device_build_product";
public static final String KEY_BUILD_MANUFACTURER = "device_build_manufacture";
public static final String KEY_BUILD_FINGERPRINT = "device_build_fingerprint";
// Customized fields for device extra information.
public static final String KEY_BUILD_METADATA_1 = "device_build_metadata_1";
public static final String KEY_BUILD_METADATA_2 = "device_build_metadata_2";
private static final String DEVICE_IDLE_SERVICE = "deviceidle";
private static final String BATTERY_OPTIMIZE_BACKUP_FILE_NAME =
"battery_optimize_backup_historical_logs";
private static final int DEVICE_BUILD_INFO_SIZE = 6;
static final String DELIMITER = ",";
static final String DELIMITER_MODE = ":";
static final String KEY_OPTIMIZATION_LIST = "optimization_mode_list";
@Nullable private byte[] mOptimizationModeBytes;
private final Application mApplication;
// Device information map from restore.
private final ArrayMap<String, String> mDeviceBuildInfoMap =
new ArrayMap<>(DEVICE_BUILD_INFO_SIZE);
/**
* Returns the {@link BatterySettingsStorage} registered to {@link BackupRestoreStorageManager}.
*/
public static @NonNull BatterySettingsStorage get(@NonNull Context context) {
return (BatterySettingsStorage)
BackupRestoreStorageManager.getInstance(context).getOrThrow(TAG);
}
public BatterySettingsStorage(@NonNull Context context) {
mApplication = (Application) context.getApplicationContext();
}
@NonNull
@Override
public String getName() {
return TAG;
}
@Override
public boolean enableBackup(@NonNull BackupContext backupContext) {
return isOwner();
}
@Override
public boolean enableRestore() {
return isOwner();
}
static boolean isOwner() {
return UserHandle.myUserId() == UserHandle.USER_SYSTEM;
}
@NonNull
@Override
public List<BackupRestoreEntity> createBackupRestoreEntities() {
List<String> allowlistedApps = getFullPowerList();
if (allowlistedApps == null) {
return Collections.emptyList();
}
PowerUsageFeatureProvider provider =
FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider();
return Arrays.asList(
new StringEntity(KEY_BUILD_BRAND, Build.BRAND),
new StringEntity(KEY_BUILD_PRODUCT, Build.PRODUCT),
new StringEntity(KEY_BUILD_MANUFACTURER, Build.MANUFACTURER),
new StringEntity(KEY_BUILD_FINGERPRINT, Build.FINGERPRINT),
new StringEntity(KEY_BUILD_METADATA_1, provider.getBuildMetadata1(mApplication)),
new StringEntity(KEY_BUILD_METADATA_2, provider.getBuildMetadata2(mApplication)),
new OptimizationModeEntity(allowlistedApps));
}
private @Nullable List<String> getFullPowerList() {
final long timestamp = System.currentTimeMillis();
String[] allowlistedApps;
try {
IDeviceIdleController deviceIdleController =
IDeviceIdleController.Stub.asInterface(
ServiceManager.getService(DEVICE_IDLE_SERVICE));
allowlistedApps = deviceIdleController.getFullPowerWhitelist();
} catch (RemoteException e) {
Log.e(TAG, "backupFullPowerList() failed", e);
return null;
}
// Ignores unexpected empty result case.
if (allowlistedApps == null || allowlistedApps.length == 0) {
Log.w(TAG, "no data found in the getFullPowerList()");
return Collections.emptyList();
}
Log.d(
TAG,
String.format(
"getFullPowerList() size=%d in %d/ms",
allowlistedApps.length, (System.currentTimeMillis() - timestamp)));
return Arrays.asList(allowlistedApps);
}
@Override
public void writeNewStateDescription(@NonNull ParcelFileDescriptor newState) {
BatterySettingsMigrateChecker.verifySaverConfiguration(mApplication);
performRestoreIfNeeded();
}
private void performRestoreIfNeeded() {
byte[] bytes = mOptimizationModeBytes;
mOptimizationModeBytes = null; // clear data
if (bytes == null || bytes.length == 0) {
return;
}
final PowerUsageFeatureProvider provider =
FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider();
if (!provider.isValidToRestoreOptimizationMode(mDeviceBuildInfoMap)) {
return;
}
// Start to restore the app optimization mode data.
final int restoreCount = restoreOptimizationMode(bytes);
if (restoreCount > 0) {
BatterySettingsMigrateChecker.verifyBatteryOptimizeModes(mApplication);
}
}
int restoreOptimizationMode(byte[] dataBytes) {
final long timestamp = System.currentTimeMillis();
final String dataContent = new String(dataBytes, UTF_8);
if (dataContent.isEmpty()) {
Log.w(TAG, "no data found in the restoreOptimizationMode()");
return 0;
}
final String[] appConfigurations = dataContent.split(BatteryBackupHelper.DELIMITER);
if (appConfigurations.length == 0) {
Log.w(TAG, "no data found from the split() processing");
return 0;
}
int restoreCount = 0;
for (String appConfiguration : appConfigurations) {
final String[] results = appConfiguration.split(BatteryBackupHelper.DELIMITER_MODE);
// Example format: com.android.systemui:2 we should have length=2
if (results.length != 2) {
Log.w(TAG, "invalid raw data found:" + appConfiguration);
continue;
}
final String packageName = results[0];
final int uid = BatteryUtils.getInstance(mApplication).getPackageUid(packageName);
// Ignores system/default apps.
if (isSystemOrDefaultApp(packageName, uid)) {
Log.w(TAG, "ignore from isSystemOrDefaultApp():" + packageName);
continue;
}
@BatteryOptimizeUtils.OptimizationMode int optimizationMode;
try {
optimizationMode = Integer.parseInt(results[1]);
} catch (NumberFormatException e) {
Log.e(TAG, "failed to parse the optimization mode: " + appConfiguration, e);
continue;
}
restoreOptimizationMode(packageName, optimizationMode);
restoreCount++;
}
Log.d(
TAG,
String.format(
"restoreOptimizationMode() count=%d in %d/ms",
restoreCount, (System.currentTimeMillis() - timestamp)));
return restoreCount;
}
private void restoreOptimizationMode(
String packageName, @BatteryOptimizeUtils.OptimizationMode int mode) {
final BatteryOptimizeUtils batteryOptimizeUtils =
newBatteryOptimizeUtils(mApplication, packageName);
if (batteryOptimizeUtils == null) {
return;
}
batteryOptimizeUtils.setAppUsageState(
mode, BatteryOptimizeHistoricalLogEntry.Action.RESTORE);
Log.d(TAG, String.format("restore:%s mode=%d", packageName, mode));
}
@Nullable
static BatteryOptimizeUtils newBatteryOptimizeUtils(Context context, String packageName) {
final int uid = BatteryUtils.getInstance(context).getPackageUid(packageName);
return uid == BatteryUtils.UID_NULL
? null
: new BatteryOptimizeUtils(context, uid, packageName);
}
private boolean isSystemOrDefaultApp(String packageName, int uid) {
return BatteryOptimizeUtils.isSystemOrDefaultApp(
mApplication, PowerAllowlistBackend.getInstance(mApplication), packageName, uid);
}
private class StringEntity implements BackupRestoreEntity {
private final String mKey;
private final String mValue;
StringEntity(String key, String value) {
this.mKey = key;
this.mValue = value;
}
@NonNull
@Override
public String getKey() {
return mKey;
}
@Override
public @NonNull EntityBackupResult backup(
@NonNull BackupContext backupContext, @NonNull OutputStream outputStream)
throws IOException {
Log.d(TAG, String.format("backup:%s:%s", mKey, mValue));
outputStream.write(mValue.getBytes(UTF_8));
return EntityBackupResult.UPDATE;
}
@Override
public void restore(
@NonNull RestoreContext restoreContext, @NonNull InputStream inputStream)
throws IOException {
String dataContent = new String(inputStream.readAllBytes(), UTF_8);
mDeviceBuildInfoMap.put(mKey, dataContent);
Log.d(TAG, String.format("restore:%s:%s", mKey, dataContent));
}
}
private class OptimizationModeEntity implements BackupRestoreEntity {
private final List<String> mAllowlistedApps;
private OptimizationModeEntity(List<String> allowlistedApps) {
this.mAllowlistedApps = allowlistedApps;
}
@NonNull
@Override
public String getKey() {
return KEY_OPTIMIZATION_LIST;
}
@Override
public @NonNull EntityBackupResult backup(
@NonNull BackupContext backupContext, @NonNull OutputStream outputStream)
throws IOException {
final long timestamp = System.currentTimeMillis();
final ArraySet<ApplicationInfo> applications = getInstalledApplications();
if (applications == null || applications.isEmpty()) {
Log.w(TAG, "no data found in the getInstalledApplications()");
return EntityBackupResult.DELETE;
}
int backupCount = 0;
final StringBuilder builder = new StringBuilder();
final AppOpsManager appOps = mApplication.getSystemService(AppOpsManager.class);
final SharedPreferences sharedPreferences = getSharedPreferences(mApplication);
// Converts application into the AppUsageState.
for (ApplicationInfo info : applications) {
final int mode = BatteryOptimizeUtils.getMode(appOps, info.uid, info.packageName);
@BatteryOptimizeUtils.OptimizationMode
final int optimizationMode =
BatteryOptimizeUtils.getAppOptimizationMode(
mode, mAllowlistedApps.contains(info.packageName));
// Ignores default optimized/unknown state or system/default apps.
if (optimizationMode == BatteryOptimizeUtils.MODE_OPTIMIZED
|| optimizationMode == BatteryOptimizeUtils.MODE_UNKNOWN
|| isSystemOrDefaultApp(info.packageName, info.uid)) {
continue;
}
final String packageOptimizeMode =
info.packageName + DELIMITER_MODE + optimizationMode;
builder.append(packageOptimizeMode).append(DELIMITER);
Log.d(TAG, "backupOptimizationMode: " + packageOptimizeMode);
BatteryOptimizeLogUtils.writeLog(
sharedPreferences,
Action.BACKUP,
info.packageName,
/* actionDescription */ "mode: " + optimizationMode);
backupCount++;
}
outputStream.write(builder.toString().getBytes(UTF_8));
Log.d(
TAG,
String.format(
"backup getInstalledApplications():%d count=%d in %d/ms",
applications.size(),
backupCount,
(System.currentTimeMillis() - timestamp)));
return EntityBackupResult.UPDATE;
}
private @Nullable ArraySet<ApplicationInfo> getInstalledApplications() {
return BatteryOptimizeUtils.getInstalledApplications(
mApplication, AppGlobals.getPackageManager());
}
static @NonNull SharedPreferences getSharedPreferences(Context context) {
return context.getSharedPreferences(
BATTERY_OPTIMIZE_BACKUP_FILE_NAME, Context.MODE_PRIVATE);
}
@Override
public void restore(
@NonNull RestoreContext restoreContext, @NonNull InputStream inputStream)
throws IOException {
mOptimizationModeBytes = inputStream.readAllBytes();
}
}
}

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) 2020 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;
/** Feature Provider used to retrieve battery status */
public interface BatteryStatusFeatureProvider {
/** Trigger a battery status update; return false if built-in status should be used. */
boolean triggerBatteryStatusUpdate(BatteryPreferenceController controller, BatteryInfo info);
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2020 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.content.Context;
/** Used to override battery status string in Battery Header. */
public class BatteryStatusFeatureProviderImpl implements BatteryStatusFeatureProvider {
protected Context mContext;
public BatteryStatusFeatureProviderImpl(Context context) {
mContext = context.getApplicationContext();
}
@Override
public boolean triggerBatteryStatusUpdate(
BatteryPreferenceController controller, BatteryInfo info) {
return false;
}
}

View File

@@ -0,0 +1,684 @@
/*
* 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.AppOpsManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.InstallSourceInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.BatteryManager;
import android.os.BatteryStats;
import android.os.BatteryStatsManager;
import android.os.BatteryUsageStats;
import android.os.BatteryUsageStatsQuery;
import android.os.Build;
import android.os.SystemClock;
import android.os.UidBatteryConsumer;
import android.provider.Settings;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Base64;
import android.util.Log;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import com.android.internal.util.ArrayUtils;
import com.android.settings.R;
import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper;
import com.android.settings.fuelgauge.batterytip.BatteryDatabaseManager;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.applications.AppUtils;
import com.android.settingslib.fuelgauge.Estimate;
import com.android.settingslib.fuelgauge.EstimateKt;
import com.android.settingslib.utils.PowerUtil;
import com.android.settingslib.utils.StringUtil;
import com.android.settingslib.utils.ThreadUtils;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.MessageLite;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
/** Utils for battery operation */
public class BatteryUtils {
public static final int UID_ZERO = 0;
public static final int UID_NULL = -1;
public static final int SDK_NULL = -1;
/** Special UID value for data usage by removed apps. */
public static final int UID_REMOVED_APPS = -4;
/** Special UID value for data usage by tethering. */
public static final int UID_TETHERING = -5;
/** Flag to check if the dock defender mode has been temporarily bypassed */
public static final String SETTINGS_GLOBAL_DOCK_DEFENDER_BYPASS = "dock_defender_bypass";
public static final String BYPASS_DOCK_DEFENDER_ACTION = "battery.dock.defender.bypass";
private static final String GOOGLE_PLAY_STORE_PACKAGE = "com.android.vending";
private static final String PACKAGE_NAME_NONE = "none";
@Retention(RetentionPolicy.SOURCE)
@IntDef({StatusType.SCREEN_USAGE, StatusType.FOREGROUND, StatusType.BACKGROUND, StatusType.ALL})
public @interface StatusType {
int SCREEN_USAGE = 0;
int FOREGROUND = 1;
int BACKGROUND = 2;
int ALL = 3;
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DockDefenderMode.FUTURE_BYPASS,
DockDefenderMode.ACTIVE,
DockDefenderMode.TEMPORARILY_BYPASSED,
DockDefenderMode.DISABLED
})
public @interface DockDefenderMode {
int FUTURE_BYPASS = 0;
int ACTIVE = 1;
int TEMPORARILY_BYPASSED = 2;
int DISABLED = 3;
}
private static final String TAG = "BatteryUtils";
private static BatteryUtils sInstance;
private PackageManager mPackageManager;
private AppOpsManager mAppOpsManager;
private Context mContext;
@VisibleForTesting PowerUsageFeatureProvider mPowerUsageFeatureProvider;
public static BatteryUtils getInstance(Context context) {
if (sInstance == null || sInstance.isDataCorrupted()) {
sInstance = new BatteryUtils(context.getApplicationContext());
}
return sInstance;
}
@VisibleForTesting
public BatteryUtils(Context context) {
mContext = context;
mPackageManager = context.getPackageManager();
mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
mPowerUsageFeatureProvider =
FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider();
}
/** For test to reset single instance. */
@VisibleForTesting
public void reset() {
sInstance = null;
}
/** Gets the process time */
public long getProcessTimeMs(@StatusType int type, @Nullable BatteryStats.Uid uid, int which) {
if (uid == null) {
return 0;
}
switch (type) {
case StatusType.SCREEN_USAGE:
return getScreenUsageTimeMs(uid, which);
case StatusType.FOREGROUND:
return getProcessForegroundTimeMs(uid, which);
case StatusType.BACKGROUND:
return getProcessBackgroundTimeMs(uid, which);
case StatusType.ALL:
return getProcessForegroundTimeMs(uid, which)
+ getProcessBackgroundTimeMs(uid, which);
}
return 0;
}
private long getScreenUsageTimeMs(BatteryStats.Uid uid, int which, long rawRealTimeUs) {
final int foregroundTypes[] = {BatteryStats.Uid.PROCESS_STATE_TOP};
Log.v(TAG, "package: " + mPackageManager.getNameForUid(uid.getUid()));
long timeUs = 0;
for (int type : foregroundTypes) {
final long localTime = uid.getProcessStateTime(type, rawRealTimeUs, which);
Log.v(TAG, "type: " + type + " time(us): " + localTime);
timeUs += localTime;
}
Log.v(TAG, "foreground time(us): " + timeUs);
// Return the min value of STATE_TOP time and foreground activity time, since both of these
// time have some errors
return PowerUtil.convertUsToMs(
Math.min(timeUs, getForegroundActivityTotalTimeUs(uid, rawRealTimeUs)));
}
private long getScreenUsageTimeMs(BatteryStats.Uid uid, int which) {
final long rawRealTimeUs = PowerUtil.convertMsToUs(SystemClock.elapsedRealtime());
return getScreenUsageTimeMs(uid, which, rawRealTimeUs);
}
private long getProcessBackgroundTimeMs(BatteryStats.Uid uid, int which) {
final long rawRealTimeUs = PowerUtil.convertMsToUs(SystemClock.elapsedRealtime());
final long timeUs =
uid.getProcessStateTime(
BatteryStats.Uid.PROCESS_STATE_BACKGROUND, rawRealTimeUs, which);
Log.v(TAG, "package: " + mPackageManager.getNameForUid(uid.getUid()));
Log.v(TAG, "background time(us): " + timeUs);
return PowerUtil.convertUsToMs(timeUs);
}
private long getProcessForegroundTimeMs(BatteryStats.Uid uid, int which) {
final long rawRealTimeUs = PowerUtil.convertMsToUs(SystemClock.elapsedRealtime());
return getScreenUsageTimeMs(uid, which, rawRealTimeUs)
+ PowerUtil.convertUsToMs(getForegroundServiceTotalTimeUs(uid, rawRealTimeUs));
}
/**
* Returns true if the specified battery consumer should be excluded from the summary battery
* consumption list.
*/
public boolean shouldHideUidBatteryConsumer(UidBatteryConsumer consumer) {
return shouldHideUidBatteryConsumer(
consumer, mPackageManager.getPackagesForUid(consumer.getUid()));
}
/**
* Returns true if the specified battery consumer should be excluded from the summary battery
* consumption list.
*/
public boolean shouldHideUidBatteryConsumer(UidBatteryConsumer consumer, String[] packages) {
return mPowerUsageFeatureProvider.isTypeSystem(consumer.getUid(), packages)
|| shouldHideUidBatteryConsumerUnconditionally(consumer, packages);
}
/**
* Returns true if the specified battery consumer should be excluded from battery consumption
* lists, either short or full.
*/
public boolean shouldHideUidBatteryConsumerUnconditionally(
UidBatteryConsumer consumer, String[] packages) {
final int uid = consumer.getUid();
return uid == UID_TETHERING ? false : uid < 0 || isHiddenSystemModule(packages);
}
/** Returns true if one the specified packages belongs to a hidden system module. */
public boolean isHiddenSystemModule(String[] packages) {
if (packages != null) {
for (int i = 0, length = packages.length; i < length; i++) {
if (AppUtils.isHiddenSystemModule(mContext, packages[i])) {
return true;
}
}
}
return false;
}
/**
* Calculate the power usage percentage for an app
*
* @param powerUsageMah power used by the app
* @param totalPowerMah total power used in the system
* @param dischargeAmount The discharge amount calculated by {@link BatteryStats}
* @return A percentage value scaled by {@paramref dischargeAmount}
* @see BatteryStats#getDischargeAmount(int)
*/
public double calculateBatteryPercent(
double powerUsageMah, double totalPowerMah, int dischargeAmount) {
if (totalPowerMah == 0) {
return 0;
}
return (powerUsageMah / totalPowerMah) * dischargeAmount;
}
/**
* Find the package name for a {@link android.os.BatteryStats.Uid}
*
* @param uid id to get the package name
* @return the package name. If there are multiple packages related to given id, return the
* first one. Or return null if there are no known packages with the given id
* @see PackageManager#getPackagesForUid(int)
*/
public String getPackageName(int uid) {
final String[] packageNames = mPackageManager.getPackagesForUid(uid);
return ArrayUtils.isEmpty(packageNames) ? null : packageNames[0];
}
/**
* Find the targetSdkVersion for package with name {@code packageName}
*
* @return the targetSdkVersion, or {@link #SDK_NULL} if {@code packageName} doesn't exist
*/
public int getTargetSdkVersion(final String packageName) {
try {
ApplicationInfo info =
mPackageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
return info.targetSdkVersion;
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Cannot find package: " + packageName, e);
}
return SDK_NULL;
}
/** Check whether background restriction is enabled */
public boolean isBackgroundRestrictionEnabled(
final int targetSdkVersion, final int uid, final String packageName) {
if (targetSdkVersion >= Build.VERSION_CODES.O) {
return true;
}
final int mode =
mAppOpsManager.checkOpNoThrow(AppOpsManager.OP_RUN_IN_BACKGROUND, uid, packageName);
return mode == AppOpsManager.MODE_IGNORED || mode == AppOpsManager.MODE_ERRORED;
}
/**
* Calculate the time since last full charge, including the device off time
*
* @param batteryUsageStats class that contains the data
* @param currentTimeMs current wall time
* @return time in millis
*/
public long calculateLastFullChargeTime(
BatteryUsageStats batteryUsageStats, long currentTimeMs) {
return currentTimeMs - batteryUsageStats.getStatsStartTimestamp();
}
public static void logRuntime(String tag, String message, long startTime) {
Log.d(tag, message + ": " + (System.currentTimeMillis() - startTime) + "ms");
}
/** Return {@code true} if battery defender is on and charging. */
public static boolean isBatteryDefenderOn(BatteryInfo batteryInfo) {
return batteryInfo.isBatteryDefender && !batteryInfo.discharging;
}
/**
* Find package uid from package name
*
* @param packageName used to find the uid
* @return uid for packageName, or {@link #UID_NULL} if exception happens or {@code packageName}
* is null
*/
public int getPackageUid(String packageName) {
try {
return packageName == null
? UID_NULL
: mPackageManager.getPackageUid(packageName, PackageManager.GET_META_DATA);
} catch (PackageManager.NameNotFoundException e) {
return UID_NULL;
}
}
/**
* Find package uid from package name
*
* @param packageName used to find the uid
* @param userId The user handle identifier to look up the package under
* @return uid for packageName, or {@link #UID_NULL} if exception happens or {@code packageName}
* is null
*/
public int getPackageUidAsUser(String packageName, int userId) {
try {
return packageName == null
? UID_NULL
: mPackageManager.getPackageUidAsUser(
packageName, PackageManager.GET_META_DATA, userId);
} catch (PackageManager.NameNotFoundException e) {
return UID_NULL;
}
}
/**
* Parses proto object from string.
*
* @param serializedProto the serialized proto string
* @param protoClass class of the proto
* @return instance of the proto class parsed from the string
*/
@SuppressWarnings("unchecked")
public static <T extends MessageLite> T parseProtoFromString(
String serializedProto, T protoClass) {
if (serializedProto == null || serializedProto.isEmpty()) {
return (T) protoClass.getDefaultInstanceForType();
}
try {
return (T)
protoClass
.getParserForType()
.parseFrom(Base64.decode(serializedProto, Base64.DEFAULT));
} catch (InvalidProtocolBufferException e) {
Log.e(TAG, "Failed to deserialize proto class", e);
return (T) protoClass.getDefaultInstanceForType();
}
}
/** Sets force app standby mode */
public void setForceAppStandby(int uid, String packageName, int mode) {
final boolean isPreOApp = isPreOApp(packageName);
if (isPreOApp) {
// Control whether app could run in the background if it is pre O app
mAppOpsManager.setMode(AppOpsManager.OP_RUN_IN_BACKGROUND, uid, packageName, mode);
}
// Control whether app could run jobs in the background
mAppOpsManager.setMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName, mode);
ThreadUtils.postOnBackgroundThread(
() -> {
final BatteryDatabaseManager batteryDatabaseManager =
BatteryDatabaseManager.getInstance(mContext);
if (mode == AppOpsManager.MODE_IGNORED) {
batteryDatabaseManager.insertAction(
AnomalyDatabaseHelper.ActionType.RESTRICTION,
uid,
packageName,
System.currentTimeMillis());
} else if (mode == AppOpsManager.MODE_ALLOWED) {
batteryDatabaseManager.deleteAction(
AnomalyDatabaseHelper.ActionType.RESTRICTION, uid, packageName);
}
});
}
public boolean isForceAppStandbyEnabled(int uid, String packageName) {
return mAppOpsManager.checkOpNoThrow(
AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uid, packageName)
== AppOpsManager.MODE_IGNORED;
}
public boolean clearForceAppStandby(String packageName) {
final int uid = getPackageUid(packageName);
if (uid != UID_NULL && isForceAppStandbyEnabled(uid, packageName)) {
setForceAppStandby(uid, packageName, AppOpsManager.MODE_ALLOWED);
return true;
} else {
return false;
}
}
@WorkerThread
public BatteryInfo getBatteryInfo(final String tag) {
final BatteryStatsManager systemService =
mContext.getSystemService(BatteryStatsManager.class);
BatteryUsageStats batteryUsageStats;
try {
batteryUsageStats =
systemService.getBatteryUsageStats(
new BatteryUsageStatsQuery.Builder().includeBatteryHistory().build());
} catch (RuntimeException e) {
Log.e(TAG, "getBatteryInfo() error from getBatteryUsageStats()", e);
// Use default BatteryUsageStats.
batteryUsageStats = new BatteryUsageStats.Builder(new String[0]).build();
}
final long startTime = System.currentTimeMillis();
// Stuff we always need to get BatteryInfo
final Intent batteryBroadcast = getBatteryIntent(mContext);
final long elapsedRealtimeUs = PowerUtil.convertMsToUs(SystemClock.elapsedRealtime());
BatteryInfo batteryInfo;
Estimate estimate = getEnhancedEstimate();
// couldn't get estimate from cache or provider, use fallback
if (estimate == null) {
estimate =
new Estimate(
batteryUsageStats.getBatteryTimeRemainingMs(),
false /* isBasedOnUsage */,
EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN);
}
BatteryUtils.logRuntime(tag, "BatteryInfoLoader post query", startTime);
batteryInfo =
BatteryInfo.getBatteryInfo(
mContext,
batteryBroadcast,
batteryUsageStats,
estimate,
elapsedRealtimeUs,
false /* shortString */);
BatteryUtils.logRuntime(tag, "BatteryInfoLoader.loadInBackground", startTime);
try {
batteryUsageStats.close();
} catch (Exception e) {
Log.e(TAG, "BatteryUsageStats.close() failed", e);
}
return batteryInfo;
}
@VisibleForTesting
Estimate getEnhancedEstimate() {
// Align the same logic in the BatteryControllerImpl.updateEstimate()
Estimate estimate = Estimate.getCachedEstimateIfAvailable(mContext);
if (estimate == null
&& mPowerUsageFeatureProvider != null
&& mPowerUsageFeatureProvider.isEnhancedBatteryPredictionEnabled(mContext)) {
estimate = mPowerUsageFeatureProvider.getEnhancedBatteryPrediction(mContext);
if (estimate != null) {
Estimate.storeCachedEstimate(mContext, estimate);
}
}
return estimate;
}
private boolean isDataCorrupted() {
return mPackageManager == null || mAppOpsManager == null;
}
@VisibleForTesting
long getForegroundActivityTotalTimeUs(BatteryStats.Uid uid, long rawRealtimeUs) {
final BatteryStats.Timer timer = uid.getForegroundActivityTimer();
if (timer != null) {
return timer.getTotalTimeLocked(rawRealtimeUs, BatteryStats.STATS_SINCE_CHARGED);
}
return 0;
}
@VisibleForTesting
long getForegroundServiceTotalTimeUs(BatteryStats.Uid uid, long rawRealtimeUs) {
final BatteryStats.Timer timer = uid.getForegroundServiceTimer();
if (timer != null) {
return timer.getTotalTimeLocked(rawRealtimeUs, BatteryStats.STATS_SINCE_CHARGED);
}
return 0;
}
public boolean isPreOApp(final String packageName) {
try {
ApplicationInfo info =
mPackageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
return info.targetSdkVersion < Build.VERSION_CODES.O;
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Cannot find package: " + packageName, e);
}
return false;
}
public boolean isPreOApp(final String[] packageNames) {
if (ArrayUtils.isEmpty(packageNames)) {
return false;
}
for (String packageName : packageNames) {
if (isPreOApp(packageName)) {
return true;
}
}
return false;
}
/**
* Return version number of an app represented by {@code packageName}, and return -1 if not
* found.
*/
public long getAppLongVersionCode(String packageName) {
try {
final PackageInfo packageInfo =
mPackageManager.getPackageInfo(packageName, 0 /* flags */);
return packageInfo.getLongVersionCode();
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Cannot find package: " + packageName, e);
}
return -1L;
}
/** Whether the package is installed from Google Play Store or not */
public static boolean isAppInstalledFromGooglePlayStore(Context context, String packageName) {
if (TextUtils.isEmpty(packageName)) {
return false;
}
InstallSourceInfo installSourceInfo;
try {
installSourceInfo = context.getPackageManager().getInstallSourceInfo(packageName);
} catch (PackageManager.NameNotFoundException e) {
return false;
}
return installSourceInfo != null
&& GOOGLE_PLAY_STORE_PACKAGE.equals(installSourceInfo.getInitiatingPackageName());
}
/** Gets the logging package name. */
public static String getLoggingPackageName(Context context, String originalPackingName) {
return BatteryUtils.isAppInstalledFromGooglePlayStore(context, originalPackingName)
? originalPackingName
: PACKAGE_NAME_NONE;
}
/** Gets the latest sticky battery intent from the Android system. */
public static Intent getBatteryIntent(Context context) {
return com.android.settingslib.fuelgauge.BatteryUtils.getBatteryIntent(context);
}
/** Gets the current dock defender mode */
public static int getCurrentDockDefenderMode(Context context, BatteryInfo batteryInfo) {
if (batteryInfo.pluggedStatus == BatteryManager.BATTERY_PLUGGED_DOCK) {
if (Settings.Global.getInt(
context.getContentResolver(), SETTINGS_GLOBAL_DOCK_DEFENDER_BYPASS, 0)
== 1) {
return DockDefenderMode.TEMPORARILY_BYPASSED;
} else if (batteryInfo.isBatteryDefender
&& FeatureFactory.getFeatureFactory()
.getPowerUsageFeatureProvider()
.isExtraDefend()) {
return DockDefenderMode.ACTIVE;
} else if (!batteryInfo.isBatteryDefender) {
return DockDefenderMode.FUTURE_BYPASS;
}
}
return DockDefenderMode.DISABLED;
}
/** Formats elapsed time without commas in between. */
public static CharSequence formatElapsedTimeWithoutComma(
Context context, double millis, boolean withSeconds, boolean collapseTimeUnit) {
return StringUtil.formatElapsedTime(context, millis, withSeconds, collapseTimeUnit)
.toString()
.replaceAll(",", "");
}
/** Builds the battery usage time summary. */
public static String buildBatteryUsageTimeSummary(
final Context context,
final boolean isSystem,
final long foregroundUsageTimeInMs,
final long backgroundUsageTimeInMs,
final long screenOnTimeInMs) {
StringBuilder summary = new StringBuilder();
if (isSystem) {
final long totalUsageTimeInMs = foregroundUsageTimeInMs + backgroundUsageTimeInMs;
if (totalUsageTimeInMs != 0) {
summary.append(
buildBatteryUsageTimeInfo(
context,
totalUsageTimeInMs,
R.string.battery_usage_total_less_than_one_minute,
R.string.battery_usage_for_total_time));
}
} else {
if (screenOnTimeInMs != 0) {
summary.append(
buildBatteryUsageTimeInfo(
context,
screenOnTimeInMs,
R.string.battery_usage_screen_time_less_than_one_minute,
R.string.battery_usage_screen_time));
}
if (screenOnTimeInMs != 0 && backgroundUsageTimeInMs != 0) {
summary.append('\n');
}
if (backgroundUsageTimeInMs != 0) {
summary.append(
buildBatteryUsageTimeInfo(
context,
backgroundUsageTimeInMs,
R.string.battery_usage_background_less_than_one_minute,
R.string.battery_usage_for_background_time));
}
}
return summary.toString();
}
/** Format the date of battery related info */
public static CharSequence getBatteryInfoFormattedDate(long dateInMs) {
final Instant instant = Instant.ofEpochMilli(dateInMs);
final String localDate =
instant.atZone(ZoneId.systemDefault())
.toLocalDate()
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG));
return localDate;
}
/** Builds the battery usage time information for one timestamp. */
private static String buildBatteryUsageTimeInfo(
final Context context,
long timeInMs,
final int lessThanOneMinuteResId,
final int normalResId) {
if (timeInMs < DateUtils.MINUTE_IN_MILLIS) {
return context.getString(lessThanOneMinuteResId);
}
final CharSequence timeSequence =
formatElapsedTimeWithoutComma(
context,
(double) timeInMs,
/* withSeconds= */ false,
/* collapseTimeUnit= */ false);
return context.getString(normalResId, timeSequence);
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2016 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.os.BatteryStats.HistoryItem;
import android.os.BatteryStatsManager;
public class BatteryWifiParser extends BatteryFlagParser {
public BatteryWifiParser(int accentColor) {
super(accentColor, false, 0);
}
@Override
protected boolean isSet(HistoryItem record) {
switch ((record.states2 & HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK)
>> HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT) {
case BatteryStatsManager.WIFI_SUPPL_STATE_DISCONNECTED:
case BatteryStatsManager.WIFI_SUPPL_STATE_DORMANT:
case BatteryStatsManager.WIFI_SUPPL_STATE_INACTIVE:
case BatteryStatsManager.WIFI_SUPPL_STATE_INTERFACE_DISABLED:
case BatteryStatsManager.WIFI_SUPPL_STATE_INVALID:
case BatteryStatsManager.WIFI_SUPPL_STATE_UNINITIALIZED:
return false;
}
return true;
}
}

View File

@@ -0,0 +1,97 @@
/*
* 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.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryStatsManager;
import android.os.BatteryUsageStats;
import android.os.SystemClock;
import android.util.Log;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.fuelgauge.Estimate;
import com.android.settingslib.fuelgauge.EstimateKt;
import com.android.settingslib.utils.AsyncLoaderCompat;
import com.android.settingslib.utils.PowerUtil;
import java.util.ArrayList;
import java.util.List;
public class DebugEstimatesLoader extends AsyncLoaderCompat<List<BatteryInfo>> {
private static final String TAG = "DebugEstimatesLoader";
public DebugEstimatesLoader(Context context) {
super(context);
}
@Override
protected void onDiscardResult(List<BatteryInfo> result) {}
@Override
public List<BatteryInfo> loadInBackground() {
Context context = getContext();
PowerUsageFeatureProvider powerUsageFeatureProvider =
FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider();
// get stuff we'll need for both BatteryInfo
final long elapsedRealtimeUs = PowerUtil.convertMsToUs(SystemClock.elapsedRealtime());
Intent batteryBroadcast =
getContext()
.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
BatteryUsageStats batteryUsageStats;
try {
batteryUsageStats =
context.getSystemService(BatteryStatsManager.class).getBatteryUsageStats();
} catch (RuntimeException e) {
Log.e(TAG, "getBatteryInfo() from getBatteryUsageStats()", e);
// Use default BatteryUsageStats.
batteryUsageStats = new BatteryUsageStats.Builder(new String[0]).build();
}
BatteryInfo oldinfo =
BatteryInfo.getBatteryInfoOld(
getContext(),
batteryBroadcast,
batteryUsageStats,
elapsedRealtimeUs,
false);
Estimate estimate = powerUsageFeatureProvider.getEnhancedBatteryPrediction(context);
if (estimate == null) {
estimate = new Estimate(0, false, EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN);
}
BatteryInfo newInfo =
BatteryInfo.getBatteryInfo(
getContext(),
batteryBroadcast,
batteryUsageStats,
estimate,
elapsedRealtimeUs,
false);
List<BatteryInfo> infos = new ArrayList<>();
infos.add(oldinfo);
infos.add(newInfo);
try {
batteryUsageStats.close();
} catch (Exception e) {
Log.e(TAG, "BatteryUsageStats.close() failed", e);
}
return infos;
}
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright (C) 2015 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.AppOpsManager;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.view.View;
import android.widget.Checkable;
import android.widget.TextView;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import com.android.settings.R;
import com.android.settings.applications.AppInfoBase;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
import com.android.settingslib.fuelgauge.PowerAllowlistBackend;
public class HighPowerDetail extends InstrumentedDialogFragment
implements OnClickListener, View.OnClickListener {
private static final String ARG_DEFAULT_ON = "default_on";
@VisibleForTesting PowerAllowlistBackend mBackend;
@VisibleForTesting BatteryUtils mBatteryUtils;
@VisibleForTesting String mPackageName;
@VisibleForTesting int mPackageUid;
private CharSequence mLabel;
private boolean mDefaultOn;
@VisibleForTesting boolean mIsEnabled;
private Checkable mOptionOn;
private Checkable mOptionOff;
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_HIGH_POWER_DETAILS;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = getContext();
mBatteryUtils = BatteryUtils.getInstance(context);
mBackend = PowerAllowlistBackend.getInstance(context);
mPackageName = getArguments().getString(AppInfoBase.ARG_PACKAGE_NAME);
mPackageUid = getArguments().getInt(AppInfoBase.ARG_PACKAGE_UID);
final PackageManager pm = context.getPackageManager();
try {
mLabel = pm.getApplicationInfo(mPackageName, 0).loadLabel(pm);
} catch (NameNotFoundException e) {
mLabel = mPackageName;
}
mDefaultOn = getArguments().getBoolean(ARG_DEFAULT_ON);
mIsEnabled = mDefaultOn || mBackend.isAllowlisted(mPackageName, mPackageUid);
}
public Checkable setup(View view, boolean on) {
((TextView) view.findViewById(android.R.id.title))
.setText(on ? R.string.ignore_optimizations_on : R.string.ignore_optimizations_off);
((TextView) view.findViewById(android.R.id.summary))
.setText(
on
? R.string.ignore_optimizations_on_desc
: R.string.ignore_optimizations_off_desc);
view.setClickable(true);
view.setOnClickListener(this);
if (!on && mBackend.isSysAllowlisted(mPackageName)) {
view.setEnabled(false);
}
return (Checkable) view;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder b =
new AlertDialog.Builder(getContext())
.setTitle(mLabel)
.setNegativeButton(R.string.cancel, null)
.setView(R.layout.ignore_optimizations_content);
if (!mBackend.isSysAllowlisted(mPackageName)) {
b.setPositiveButton(R.string.done, this);
}
return b.create();
}
@Override
public void onStart() {
super.onStart();
mOptionOn = setup(getDialog().findViewById(R.id.ignore_on), true);
mOptionOff = setup(getDialog().findViewById(R.id.ignore_off), false);
updateViews();
}
private void updateViews() {
mOptionOn.setChecked(mIsEnabled);
mOptionOff.setChecked(!mIsEnabled);
}
@Override
public void onClick(View v) {
if (v == mOptionOn) {
mIsEnabled = true;
updateViews();
} else if (v == mOptionOff) {
mIsEnabled = false;
updateViews();
}
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
boolean newValue = mIsEnabled;
boolean oldValue = mBackend.isAllowlisted(mPackageName, mPackageUid);
if (newValue != oldValue) {
logSpecialPermissionChange(newValue, mPackageName, getContext());
if (newValue) {
mBatteryUtils.setForceAppStandby(
mPackageUid, mPackageName, AppOpsManager.MODE_ALLOWED);
mBackend.addApp(mPackageName);
} else {
mBackend.removeApp(mPackageName);
}
}
}
}
@VisibleForTesting
static void logSpecialPermissionChange(boolean allowlist, String packageName, Context context) {
int logCategory =
allowlist
? SettingsEnums.APP_SPECIAL_PERMISSION_BATTERY_DENY
: SettingsEnums.APP_SPECIAL_PERMISSION_BATTERY_ALLOW;
FeatureFactory.getFeatureFactory()
.getMetricsFeatureProvider()
.action(context, logCategory, packageName);
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
Fragment target = getTargetFragment();
if (target != null && target.getActivity() != null) {
target.onActivityResult(getTargetRequestCode(), 0, null);
}
}
public static CharSequence getSummary(Context context, AppEntry entry) {
return getSummary(context, entry.info.packageName, entry.info.uid);
}
public static CharSequence getSummary(Context context, String pkg, int uid) {
return getSummary(context, PowerAllowlistBackend.getInstance(context), pkg, uid);
}
@VisibleForTesting
static CharSequence getSummary(
Context context, PowerAllowlistBackend powerAllowlist, String pkg, int uid) {
return context.getString(
powerAllowlist.isSysAllowlisted(pkg) || powerAllowlist.isDefaultActiveApp(pkg, uid)
? R.string.high_power_system
: powerAllowlist.isAllowlisted(pkg, uid)
? R.string.high_power_on
: R.string.high_power_off);
}
public static void show(Fragment caller, int uid, String packageName, int requestCode) {
HighPowerDetail fragment = new HighPowerDetail();
Bundle args = new Bundle();
args.putString(AppInfoBase.ARG_PACKAGE_NAME, packageName);
args.putInt(AppInfoBase.ARG_PACKAGE_UID, uid);
fragment.setArguments(args);
fragment.setTargetFragment(caller, requestCode);
fragment.show(caller.getFragmentManager(), HighPowerDetail.class.getSimpleName());
}
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright (C) 2015 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 static android.app.usage.UsageStatsManager.STANDBY_BUCKET_ACTIVE;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_EXEMPTED;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_FREQUENT;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_NEVER;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RARE;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_RESTRICTED;
import static android.app.usage.UsageStatsManager.STANDBY_BUCKET_WORKING_SET;
import android.app.settings.SettingsEnums;
import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.TextUtils;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragment;
import java.util.Arrays;
import java.util.List;
public class InactiveApps extends SettingsPreferenceFragment
implements Preference.OnPreferenceChangeListener {
private static final CharSequence[] FULL_SETTABLE_BUCKETS_NAMES = {
"ACTIVE", "WORKING_SET", "FREQUENT", "RARE", "RESTRICTED"
};
private static final CharSequence[] FULL_SETTABLE_BUCKETS_VALUES = {
Integer.toString(STANDBY_BUCKET_ACTIVE),
Integer.toString(STANDBY_BUCKET_WORKING_SET),
Integer.toString(STANDBY_BUCKET_FREQUENT),
Integer.toString(STANDBY_BUCKET_RARE),
Integer.toString(STANDBY_BUCKET_RESTRICTED)
};
private UsageStatsManager mUsageStats;
@Override
public int getMetricsCategory() {
return SettingsEnums.FUELGAUGE_INACTIVE_APPS;
}
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
mUsageStats = getActivity().getSystemService(UsageStatsManager.class);
addPreferencesFromResource(R.xml.placeholder_preference_screen);
getActivity().setTitle(com.android.settingslib.R.string.inactive_apps_title);
}
@Override
public void onResume() {
super.onResume();
init();
}
private void init() {
PreferenceGroup screen = getPreferenceScreen();
screen.removeAll();
screen.setOrderingAsAdded(false);
final Context context = getActivity();
final PackageManager pm = context.getPackageManager();
final String settingsPackage = context.getPackageName();
final CharSequence[] bucketNames = FULL_SETTABLE_BUCKETS_NAMES;
final CharSequence[] bucketValues = FULL_SETTABLE_BUCKETS_VALUES;
Intent launcherIntent = new Intent(Intent.ACTION_MAIN);
launcherIntent.addCategory(Intent.CATEGORY_LAUNCHER);
List<ResolveInfo> apps = pm.queryIntentActivities(launcherIntent, 0);
for (ResolveInfo app : apps) {
String packageName = app.activityInfo.applicationInfo.packageName;
ListPreference p = new ListPreference(getPrefContext());
p.setTitle(app.loadLabel(pm));
p.setIcon(app.loadIcon(pm));
p.setKey(packageName);
p.setEntries(getAllowableBuckets(packageName, bucketNames));
p.setEntryValues(getAllowableBuckets(packageName, bucketValues));
updateSummary(p);
// Don't allow Settings to change its own standby bucket.
if (TextUtils.equals(packageName, settingsPackage)) {
p.setEnabled(false);
}
p.setOnPreferenceChangeListener(this);
screen.addPreference(p);
}
}
private CharSequence[] getAllowableBuckets(String packageName, CharSequence[] possibleBuckets) {
final int minBucket = mUsageStats.getAppMinStandbyBucket(packageName);
if (minBucket > STANDBY_BUCKET_RESTRICTED) {
return possibleBuckets;
}
if (minBucket < STANDBY_BUCKET_ACTIVE) {
return new CharSequence[] {};
}
// Use FULL_SETTABLE_BUCKETS_VALUES since we're searching using the int value. The index
// should apply no matter which array we're going to copy from.
final int idx =
Arrays.binarySearch(FULL_SETTABLE_BUCKETS_VALUES, Integer.toString(minBucket));
if (idx < 0) {
// Include everything
return possibleBuckets;
}
return Arrays.copyOfRange(possibleBuckets, 0, idx + 1);
}
static String bucketToName(int bucket) {
switch (bucket) {
case STANDBY_BUCKET_EXEMPTED:
return "EXEMPTED";
case STANDBY_BUCKET_ACTIVE:
return "ACTIVE";
case STANDBY_BUCKET_WORKING_SET:
return "WORKING_SET";
case STANDBY_BUCKET_FREQUENT:
return "FREQUENT";
case STANDBY_BUCKET_RARE:
return "RARE";
case STANDBY_BUCKET_RESTRICTED:
return "RESTRICTED";
case STANDBY_BUCKET_NEVER:
return "NEVER";
}
return "";
}
private void updateSummary(ListPreference p) {
final Resources res = getActivity().getResources();
final int appBucket = mUsageStats.getAppStandbyBucket(p.getKey());
final String bucketName = bucketToName(appBucket);
p.setSummary(
res.getString(com.android.settingslib.R.string.standby_bucket_summary, bucketName));
// Buckets outside of the range of the dynamic ones are only used for special
// purposes and can either not be changed out of, or might have undesirable
// side-effects in combination with other assumptions.
final boolean changeable =
appBucket >= STANDBY_BUCKET_ACTIVE && appBucket <= STANDBY_BUCKET_RESTRICTED;
if (changeable) {
p.setValue(Integer.toString(appBucket));
}
p.setEnabled(changeable);
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
mUsageStats.setAppStandbyBucket(preference.getKey(), Integer.parseInt((String) newValue));
updateSummary((ListPreference) preference);
return false;
}
}

View File

@@ -0,0 +1,7 @@
# Default reviewers for this and subdirectories.
tifn@google.com
wesleycwwang@google.com
ykhung@google.com
# BatteryStats
per-file FakeUid.java = file:platform/frameworks/base:/BATTERY_STATS_OWNERS

View File

@@ -0,0 +1,66 @@
/*
* Copyright (C) 2021 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.content.Context;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.widget.SelectorWithWidgetPreference;
public class OptimizedPreferenceController extends AbstractPreferenceController
implements PreferenceControllerMixin {
private static final String TAG = "OPTIMIZED_PREF";
@VisibleForTesting static final String KEY_OPTIMIZED_PREF = "optimized_preference";
@VisibleForTesting BatteryOptimizeUtils mBatteryOptimizeUtils;
public OptimizedPreferenceController(Context context, int uid, String packageName) {
super(context);
mBatteryOptimizeUtils = new BatteryOptimizeUtils(context, uid, packageName);
}
@Override
public boolean isAvailable() {
return true;
}
@Override
public void updateState(Preference preference) {
preference.setEnabled(mBatteryOptimizeUtils.isSelectorPreferenceEnabled());
final boolean isOptimized =
mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()
|| mBatteryOptimizeUtils.getAppOptimizationMode()
== BatteryOptimizeUtils.MODE_OPTIMIZED;
((SelectorWithWidgetPreference) preference).setChecked(isOptimized);
}
@Override
public String getPreferenceKey() {
return KEY_OPTIMIZED_PREF;
}
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
return getPreferenceKey().equals(preference.getKey());
}
}

View File

@@ -0,0 +1,335 @@
/*
* Copyright (C) 2023 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 static com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
import android.app.Activity;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.EntityHeaderController;
import com.android.settingslib.HelpUtils;
import com.android.settingslib.applications.AppUtils;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.datastore.ChangeReason;
import com.android.settingslib.widget.FooterPreference;
import com.android.settingslib.widget.LayoutPreference;
import com.android.settingslib.widget.MainSwitchPreference;
import com.android.settingslib.widget.SelectorWithWidgetPreference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/** Allow background usage fragment for each app */
public class PowerBackgroundUsageDetail extends DashboardFragment
implements SelectorWithWidgetPreference.OnClickListener, OnCheckedChangeListener {
private static final String TAG = "PowerBackgroundUsageDetail";
public static final String EXTRA_UID = "extra_uid";
public static final String EXTRA_PACKAGE_NAME = "extra_package_name";
public static final String EXTRA_LABEL = "extra_label";
public static final String EXTRA_POWER_USAGE_AMOUNT = "extra_power_usage_amount";
public static final String EXTRA_ICON_ID = "extra_icon_id";
private static final String KEY_PREF_HEADER = "header_view";
private static final String KEY_PREF_UNRESTRICTED = "unrestricted_preference";
private static final String KEY_PREF_OPTIMIZED = "optimized_preference";
private static final String KEY_ALLOW_BACKGROUND_USAGE = "allow_background_usage";
private static final String KEY_FOOTER_PREFERENCE = "app_usage_footer_preference";
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
@VisibleForTesting LayoutPreference mHeaderPreference;
@VisibleForTesting ApplicationsState mState;
@VisibleForTesting ApplicationsState.AppEntry mAppEntry;
@VisibleForTesting BatteryOptimizeUtils mBatteryOptimizeUtils;
@VisibleForTesting SelectorWithWidgetPreference mOptimizePreference;
@VisibleForTesting SelectorWithWidgetPreference mUnrestrictedPreference;
@VisibleForTesting MainSwitchPreference mMainSwitchPreference;
@VisibleForTesting FooterPreference mFooterPreference;
@VisibleForTesting StringBuilder mLogStringBuilder;
@VisibleForTesting @BatteryOptimizeUtils.OptimizationMode
int mOptimizationMode = BatteryOptimizeUtils.MODE_UNKNOWN;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mState = ApplicationsState.getInstance(getActivity().getApplication());
}
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
final String packageName = getArguments().getString(EXTRA_PACKAGE_NAME);
onCreateBackgroundUsageState(packageName);
mHeaderPreference = findPreference(KEY_PREF_HEADER);
if (packageName != null) {
mAppEntry = mState.getEntry(packageName, UserHandle.myUserId());
}
}
@Override
public void onResume() {
super.onResume();
initHeader();
mOptimizationMode = mBatteryOptimizeUtils.getAppOptimizationMode();
initFooter();
mLogStringBuilder = new StringBuilder("onResume mode = ").append(mOptimizationMode);
}
@Override
public void onPause() {
super.onPause();
notifyBackupManager();
final int currentOptimizeMode = mBatteryOptimizeUtils.getAppOptimizationMode();
mLogStringBuilder.append(", onPause mode = ").append(currentOptimizeMode);
logMetricCategory(currentOptimizeMode);
mExecutor.execute(
() -> {
BatteryOptimizeLogUtils.writeLog(
getContext().getApplicationContext(),
Action.LEAVE,
BatteryOptimizeLogUtils.getPackageNameWithUserId(
mBatteryOptimizeUtils.getPackageName(), UserHandle.myUserId()),
mLogStringBuilder.toString());
});
Log.d(TAG, "Leave with mode: " + currentOptimizeMode);
}
@Override
public void onRadioButtonClicked(SelectorWithWidgetPreference selected) {
final String selectedKey = selected == null ? null : selected.getKey();
updateSelectorPreferenceState(mUnrestrictedPreference, selectedKey);
updateSelectorPreferenceState(mOptimizePreference, selectedKey);
mBatteryOptimizeUtils.setAppUsageState(getSelectedPreference(), Action.APPLY);
}
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mMainSwitchPreference.setChecked(isChecked);
updateSelectorPreference(isChecked);
}
@Override
public int getMetricsCategory() {
return SettingsEnums.FUELGAUGE_POWER_USAGE_MANAGE_BACKGROUND;
}
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
final List<AbstractPreferenceController> controllers = new ArrayList<>();
final Bundle bundle = getArguments();
final int uid = bundle.getInt(EXTRA_UID, 0);
final String packageName = bundle.getString(EXTRA_PACKAGE_NAME);
controllers.add(new AllowBackgroundPreferenceController(context, uid, packageName));
controllers.add(new OptimizedPreferenceController(context, uid, packageName));
controllers.add(new UnrestrictedPreferenceController(context, uid, packageName));
return controllers;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.power_background_usage_detail;
}
@Override
protected String getLogTag() {
return TAG;
}
@VisibleForTesting
void updateSelectorPreference(boolean isEnabled) {
mOptimizePreference.setEnabled(isEnabled);
mUnrestrictedPreference.setEnabled(isEnabled);
onRadioButtonClicked(isEnabled ? mOptimizePreference : null);
}
@VisibleForTesting
void notifyBackupManager() {
if (mOptimizationMode != mBatteryOptimizeUtils.getAppOptimizationMode()) {
BatterySettingsStorage.get(getContext()).notifyChange(ChangeReason.UPDATE);
}
}
@VisibleForTesting
int getSelectedPreference() {
if (!mMainSwitchPreference.isChecked()) {
return BatteryOptimizeUtils.MODE_RESTRICTED;
} else if (mUnrestrictedPreference.isChecked()) {
return BatteryOptimizeUtils.MODE_UNRESTRICTED;
} else if (mOptimizePreference.isChecked()) {
return BatteryOptimizeUtils.MODE_OPTIMIZED;
} else {
return BatteryOptimizeUtils.MODE_UNKNOWN;
}
}
static void startPowerBackgroundUsageDetailPage(Context context, Bundle args) {
new SubSettingLauncher(context)
.setDestination(PowerBackgroundUsageDetail.class.getName())
.setArguments(args)
.setSourceMetricsCategory(SettingsEnums.FUELGAUGE_POWER_USAGE_MANAGE_BACKGROUND)
.launch();
}
@VisibleForTesting
void initHeader() {
final View appSnippet = mHeaderPreference.findViewById(R.id.entity_header);
final Activity context = getActivity();
final Bundle bundle = getArguments();
EntityHeaderController controller =
EntityHeaderController.newInstance(context, this, appSnippet)
.setButtonActions(
EntityHeaderController.ActionType.ACTION_NONE,
EntityHeaderController.ActionType.ACTION_NONE);
if (mAppEntry == null) {
controller.setLabel(bundle.getString(EXTRA_LABEL));
final int iconId = bundle.getInt(EXTRA_ICON_ID, 0);
if (iconId == 0) {
controller.setIcon(context.getPackageManager().getDefaultActivityIcon());
} else {
controller.setIcon(context.getDrawable(bundle.getInt(EXTRA_ICON_ID)));
}
} else {
mState.ensureIcon(mAppEntry);
controller.setLabel(mAppEntry);
controller.setIcon(mAppEntry);
controller.setIsInstantApp(AppUtils.isInstant(mAppEntry.info));
}
controller.done(true /* rebindActions */);
}
@VisibleForTesting
void initFooter() {
final String stateString;
final String footerString;
final Context context = getContext();
if (mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()) {
// Present optimized only string when the package name is invalid.
stateString = context.getString(R.string.manager_battery_usage_optimized_only);
footerString =
context.getString(R.string.manager_battery_usage_footer_limited, stateString);
} else if (mBatteryOptimizeUtils.isSystemOrDefaultApp()) {
// Present unrestricted only string when the package is system or default active app.
stateString = context.getString(R.string.manager_battery_usage_unrestricted_only);
footerString =
context.getString(R.string.manager_battery_usage_footer_limited, stateString);
} else {
// Present default string to normal app.
footerString = context.getString(R.string.manager_battery_usage_footer);
}
mFooterPreference.setTitle(footerString);
final Intent helpIntent =
HelpUtils.getHelpIntent(
context,
context.getString(R.string.help_url_app_usage_settings),
/* backupContext= */ "");
if (helpIntent != null) {
mFooterPreference.setLearnMoreAction(
v -> startActivityForResult(helpIntent, /* requestCode= */ 0));
mFooterPreference.setLearnMoreText(
context.getString(R.string.manager_battery_usage_link_a11y));
}
}
private void onCreateBackgroundUsageState(String packageName) {
mOptimizePreference = findPreference(KEY_PREF_OPTIMIZED);
mUnrestrictedPreference = findPreference(KEY_PREF_UNRESTRICTED);
mMainSwitchPreference = findPreference(KEY_ALLOW_BACKGROUND_USAGE);
mFooterPreference = findPreference(KEY_FOOTER_PREFERENCE);
mOptimizePreference.setOnClickListener(this);
mUnrestrictedPreference.setOnClickListener(this);
mMainSwitchPreference.addOnSwitchChangeListener(this);
mBatteryOptimizeUtils =
new BatteryOptimizeUtils(
getContext(), getArguments().getInt(EXTRA_UID), packageName);
}
private void updateSelectorPreferenceState(
SelectorWithWidgetPreference preference, String selectedKey) {
preference.setChecked(TextUtils.equals(selectedKey, preference.getKey()));
}
private void logMetricCategory(int currentOptimizeMode) {
if (currentOptimizeMode == mOptimizationMode) {
return;
}
int metricCategory = 0;
switch (currentOptimizeMode) {
case BatteryOptimizeUtils.MODE_UNRESTRICTED:
metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_UNRESTRICTED;
break;
case BatteryOptimizeUtils.MODE_OPTIMIZED:
metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_OPTIMIZED;
break;
case BatteryOptimizeUtils.MODE_RESTRICTED:
metricCategory = SettingsEnums.ACTION_APP_BATTERY_USAGE_RESTRICTED;
break;
}
if (metricCategory == 0) {
return;
}
int finalMetricCategory = metricCategory;
mExecutor.execute(
() -> {
String packageName =
BatteryUtils.getLoggingPackageName(
getContext(), mBatteryOptimizeUtils.getPackageName());
FeatureFactory.getFeatureFactory()
.getMetricsFeatureProvider()
.action(
/* attribution */ SettingsEnums
.LEAVE_POWER_USAGE_MANAGE_BACKGROUND,
/* action */ finalMetricCategory,
/* pageId */ SettingsEnums
.FUELGAUGE_POWER_USAGE_MANAGE_BACKGROUND,
packageName,
getArguments().getInt(EXTRA_POWER_USAGE_AMOUNT));
});
}
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright (C) 2022 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.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.SparseIntArray;
import com.android.settings.fuelgauge.batteryusage.DetectRequestSourceType;
import com.android.settings.fuelgauge.batteryusage.PowerAnomalyEventList;
import com.android.settingslib.fuelgauge.Estimate;
import java.util.List;
import java.util.Set;
/** Feature Provider used in power usage */
public interface PowerUsageFeatureProvider {
/** Check whether the battery usage button is enabled in the battery page */
boolean isBatteryUsageEnabled();
/** Check whether the battery tips card is enabled in the battery usage page */
boolean isBatteryTipsEnabled();
/**
* Returns a threshold (in milliseconds) for the minimal screen on time in battery usage list
*/
double getBatteryUsageListScreenOnTimeThresholdInMs();
/** Returns a threshold (mA) for the minimal comsume power in battery usage list */
double getBatteryUsageListConsumePowerThreshold();
/** Returns an allowlist of app names combined into the system-apps item */
List<String> getSystemAppsAllowlist();
/** Check whether location setting is enabled */
boolean isLocationSettingEnabled(String[] packages);
/** Gets an {@link Intent} to show additional battery info */
Intent getAdditionalBatteryInfoIntent();
/** Check whether it is type service */
boolean isTypeService(int uid);
/** Check whether it is type system */
boolean isTypeSystem(int uid, String[] packages);
/** Returns an improved prediction for battery time remaining */
Estimate getEnhancedBatteryPrediction(Context context);
/**
* Returns an improved projection curve for future battery level
*
* @param zeroTime timestamps (array keys) are shifted by this amount
*/
SparseIntArray getEnhancedBatteryPredictionCurve(Context context, long zeroTime);
/** Checks whether the toggle for enhanced battery predictions is enabled */
boolean isEnhancedBatteryPredictionEnabled(Context context);
/** Checks whether debugging should be enabled for battery estimates */
boolean isEstimateDebugEnabled();
/**
* Converts the provided string containing the remaining time into a debug string for enhanced
* estimates
*
* @return A string containing the estimate and a label indicating it is an enhanced estimate
*/
String getEnhancedEstimateDebugString(String timeRemaining);
/**
* Converts the provided string containing the remaining time into a debug string
*
* @return A string containing the estimate and a label indicating it is a normal estimate
*/
String getOldEstimateDebugString(String timeRemaining);
/** Checks whether smart battery feature is supported in this device */
boolean isSmartBatterySupported();
/** Checks whether we should show usage information by slots or not */
boolean isChartGraphSlotsEnabled(Context context);
/** Checks whether adaptive charging feature is supported in this device */
boolean isAdaptiveChargingSupported();
/** Checks whether battery manager feature is supported in this device */
boolean isBatteryManagerSupported();
/** Returns {@code true} if current defender mode is extra defend */
boolean isExtraDefend();
/** Returns {@code true} if delay the hourly job when device is booting */
boolean delayHourlyJobWhenBooting();
/** Returns {@link Bundle} for settings anomaly detection result */
PowerAnomalyEventList detectSettingsAnomaly(
Context context, double displayDrain, DetectRequestSourceType detectRequestSourceType);
/** Gets an intent for one time bypass charge limited to resume charging. */
Intent getResumeChargeIntent(boolean isDockDefender);
/** Returns the intent action used to mark as the full charge start event. */
String getFullChargeIntentAction();
/** Returns {@link Set} for the system component ids which are combined into others */
Set<Integer> getOthersSystemComponentSet();
/** Returns {@link Set} for the custom system component names which are combined into others */
Set<String> getOthersCustomComponentNameSet();
/** Returns {@link Set} for hiding system component ids in the usage screen */
Set<Integer> getHideSystemComponentSet();
/** Returns {@link Set} for hiding application package names in the usage screen */
Set<String> getHideApplicationSet();
/** Returns {@link Set} for hiding applications background usage time */
Set<String> getHideBackgroundUsageTimeSet();
/** Returns {@link Set} for ignoring task root class names for screen on time */
Set<String> getIgnoreScreenOnTimeTaskRootSet();
/** Returns the customized device build information for data backup */
String getBuildMetadata1(Context context);
/** Returns the customized device build information for data backup */
String getBuildMetadata2(Context context);
/** Whether the app optimization mode is valid to restore */
boolean isValidToRestoreOptimizationMode(ArrayMap<String, String> deviceInfoMap);
}

View File

@@ -0,0 +1,231 @@
/*
* Copyright (C) 2022 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 static com.android.settings.Utils.SYSTEMUI_PACKAGE_NAME;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Process;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.SparseIntArray;
import com.android.internal.util.ArrayUtils;
import com.android.settings.fuelgauge.batteryusage.DetectRequestSourceType;
import com.android.settings.fuelgauge.batteryusage.PowerAnomalyEventList;
import com.android.settingslib.fuelgauge.Estimate;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/** Implementation of {@code PowerUsageFeatureProvider} */
public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider {
private static final String PACKAGE_CALENDAR_PROVIDER = "com.android.providers.calendar";
private static final String PACKAGE_MEDIA_PROVIDER = "com.android.providers.media";
private static final String[] PACKAGES_SYSTEM = {
PACKAGE_MEDIA_PROVIDER, PACKAGE_CALENDAR_PROVIDER, SYSTEMUI_PACKAGE_NAME
};
protected PackageManager mPackageManager;
protected Context mContext;
public PowerUsageFeatureProviderImpl(Context context) {
mPackageManager = context.getPackageManager();
mContext = context.getApplicationContext();
}
@Override
public boolean isTypeService(int uid) {
return false;
}
@Override
public boolean isTypeSystem(int uid, String[] packages) {
// Classify all the sippers to type system if the range of uid is 0...FIRST_APPLICATION_UID
if (uid >= Process.ROOT_UID && uid < Process.FIRST_APPLICATION_UID) {
return true;
} else if (packages != null) {
for (final String packageName : packages) {
if (ArrayUtils.contains(PACKAGES_SYSTEM, packageName)) {
return true;
}
}
}
return false;
}
@Override
public boolean isBatteryUsageEnabled() {
return true;
}
@Override
public boolean isBatteryTipsEnabled() {
return false;
}
@Override
public double getBatteryUsageListScreenOnTimeThresholdInMs() {
return 0;
}
@Override
public double getBatteryUsageListConsumePowerThreshold() {
return 0;
}
@Override
public List<String> getSystemAppsAllowlist() {
return new ArrayList<>();
}
@Override
public boolean isLocationSettingEnabled(String[] packages) {
return false;
}
@Override
public Intent getAdditionalBatteryInfoIntent() {
return null;
}
@Override
public Estimate getEnhancedBatteryPrediction(Context context) {
return null;
}
@Override
public SparseIntArray getEnhancedBatteryPredictionCurve(Context context, long zeroTime) {
return null;
}
@Override
public boolean isEnhancedBatteryPredictionEnabled(Context context) {
return false;
}
@Override
public String getEnhancedEstimateDebugString(String timeRemaining) {
return null;
}
@Override
public boolean isEstimateDebugEnabled() {
return false;
}
@Override
public String getOldEstimateDebugString(String timeRemaining) {
return null;
}
@Override
public boolean isSmartBatterySupported() {
return mContext.getResources()
.getBoolean(com.android.internal.R.bool.config_smart_battery_available);
}
@Override
public boolean isChartGraphSlotsEnabled(Context context) {
return false;
}
@Override
public boolean isAdaptiveChargingSupported() {
return false;
}
@Override
public boolean isBatteryManagerSupported() {
return true;
}
@Override
public Intent getResumeChargeIntent(boolean isDockDefender) {
return null;
}
@Override
public String getFullChargeIntentAction() {
return Intent.ACTION_BATTERY_LEVEL_CHANGED;
}
@Override
public boolean isExtraDefend() {
return false;
}
@Override
public boolean delayHourlyJobWhenBooting() {
return true;
}
@Override
public PowerAnomalyEventList detectSettingsAnomaly(
Context context, double displayDrain, DetectRequestSourceType detectRequestSourceType) {
return null;
}
@Override
public Set<Integer> getOthersSystemComponentSet() {
return new ArraySet<>();
}
@Override
public Set<String> getOthersCustomComponentNameSet() {
return new ArraySet<>();
}
@Override
public Set<Integer> getHideSystemComponentSet() {
return new ArraySet<>();
}
@Override
public Set<String> getHideApplicationSet() {
return new ArraySet<>();
}
@Override
public Set<String> getHideBackgroundUsageTimeSet() {
return new ArraySet<>();
}
@Override
public Set<String> getIgnoreScreenOnTimeTaskRootSet() {
return new ArraySet<>();
}
@Override
public String getBuildMetadata1(Context context) {
return null;
}
@Override
public String getBuildMetadata2(Context context) {
return null;
}
@Override
public boolean isValidToRestoreOptimizationMode(ArrayMap<String, String> deviceInfoMap) {
return false;
}
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright (C) 2023 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 static com.android.settings.fuelgauge.BatteryUtils.formatElapsedTimeWithoutComma;
import android.content.Context;
import android.text.TextUtils;
import android.text.format.DateUtils;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
public class PowerUsageTimeController extends BasePreferenceController {
private static final String TAG = "PowerUsageTimeController";
private static final String KEY_POWER_USAGE_TIME = "battery_usage_time_category";
private static final String KEY_SCREEN_TIME_PREF = "battery_usage_screen_time";
private static final String KEY_BACKGROUND_TIME_PREF = "battery_usage_background_time";
@VisibleForTesting PreferenceCategory mPowerUsageTimeCategory;
@VisibleForTesting PowerUsageTimePreference mScreenTimePreference;
@VisibleForTesting PowerUsageTimePreference mBackgroundTimePreference;
public PowerUsageTimeController(Context context) {
super(context, KEY_POWER_USAGE_TIME);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPowerUsageTimeCategory = screen.findPreference(KEY_POWER_USAGE_TIME);
mScreenTimePreference = screen.findPreference(KEY_SCREEN_TIME_PREF);
mBackgroundTimePreference = screen.findPreference(KEY_BACKGROUND_TIME_PREF);
mPowerUsageTimeCategory.setVisible(false);
}
void handleScreenTimeUpdated(
final String slotTime,
final long screenOnTimeInMs,
final long backgroundTimeInMs,
final String anomalyHintPrefKey,
final String anomalyHintText) {
final boolean isShowScreenOnTime =
showTimePreference(
mScreenTimePreference,
R.string.power_usage_detail_screen_time,
screenOnTimeInMs,
anomalyHintPrefKey,
anomalyHintText);
final boolean isShowBackgroundTime =
showTimePreference(
mBackgroundTimePreference,
R.string.power_usage_detail_background_time,
backgroundTimeInMs,
anomalyHintPrefKey,
anomalyHintText);
if (isShowScreenOnTime || isShowBackgroundTime) {
showCategoryTitle(slotTime);
}
}
boolean showTimePreference(
PowerUsageTimePreference preference,
int titleResId,
long summaryTimeMs,
String anomalyHintKey,
String anomalyHintText) {
if (preference == null
|| (summaryTimeMs == 0 && !TextUtils.equals(anomalyHintKey, preference.getKey()))) {
return false;
}
preference.setTimeTitle(mContext.getString(titleResId));
preference.setTimeSummary(getPowerUsageTimeInfo(summaryTimeMs));
if (TextUtils.equals(anomalyHintKey, preference.getKey())) {
preference.setAnomalyHint(anomalyHintText);
}
preference.setVisible(true);
return true;
}
private CharSequence getPowerUsageTimeInfo(long timeInMs) {
if (timeInMs < DateUtils.MINUTE_IN_MILLIS) {
return mContext.getString(R.string.power_usage_time_less_than_one_minute);
}
return formatElapsedTimeWithoutComma(
mContext,
(double) timeInMs,
/* withSeconds= */ false,
/* collapseTimeUnit= */ false);
}
@VisibleForTesting
void showCategoryTitle(String slotTimestamp) {
mPowerUsageTimeCategory.setTitle(
slotTimestamp == null
? mContext.getString(R.string.battery_app_usage)
: mContext.getString(R.string.battery_app_usage_for, slotTimestamp));
mPowerUsageTimeCategory.setVisible(true);
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright (C) 2023 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.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
/** Custom preference for displaying the app power usage time. */
public class PowerUsageTimePreference extends Preference {
private static final String TAG = "PowerUsageTimePreference";
@VisibleForTesting CharSequence mTimeTitle;
@VisibleForTesting CharSequence mTimeSummary;
@VisibleForTesting CharSequence mAnomalyHintText;
public PowerUsageTimePreference(Context context, AttributeSet attrs) {
super(context, attrs);
setLayoutResource(R.layout.power_usage_time);
}
void setTimeTitle(CharSequence timeTitle) {
if (!TextUtils.equals(mTimeTitle, timeTitle)) {
mTimeTitle = timeTitle;
notifyChanged();
}
}
void setTimeSummary(CharSequence timeSummary) {
if (!TextUtils.equals(mTimeSummary, timeSummary)) {
mTimeSummary = timeSummary;
notifyChanged();
}
}
void setAnomalyHint(CharSequence anomalyHintText) {
if (!TextUtils.equals(mAnomalyHintText, anomalyHintText)) {
mAnomalyHintText = anomalyHintText;
notifyChanged();
}
}
private void showAnomalyHint(PreferenceViewHolder view) {
if (TextUtils.isEmpty(mAnomalyHintText)) {
return;
}
final View anomalyHintView = view.findViewById(R.id.anomaly_hints);
if (anomalyHintView == null) {
return;
}
final TextView warningInfo = anomalyHintView.findViewById(R.id.warning_info);
if (warningInfo == null) {
return;
}
warningInfo.setText(mAnomalyHintText);
anomalyHintView.setVisibility(View.VISIBLE);
}
@Override
public void onBindViewHolder(PreferenceViewHolder view) {
super.onBindViewHolder(view);
((TextView) view.findViewById(R.id.time_title)).setText(mTimeTitle);
((TextView) view.findViewById(R.id.time_summary)).setText(mTimeSummary);
showAnomalyHint(view);
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright (C) 2015 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.Manifest;
import android.content.DialogInterface;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageItemInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.PowerManager;
import android.os.PowerWhitelistManager;
import android.util.Log;
import com.android.internal.app.AlertActivity;
import com.android.internal.app.AlertController;
import com.android.settings.R;
public class RequestIgnoreBatteryOptimizations extends AlertActivity
implements DialogInterface.OnClickListener {
private static final String TAG = "RequestIgnoreBatteryOptimizations";
private static final boolean DEBUG = false;
private PowerWhitelistManager mPowerWhitelistManager;
private String mPackageName;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow()
.addSystemFlags(
android.view.WindowManager.LayoutParams
.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
mPowerWhitelistManager = getSystemService(PowerWhitelistManager.class);
Uri data = getIntent().getData();
if (data == null) {
debugLog(
"No data supplied for IGNORE_BATTERY_OPTIMIZATION_SETTINGS in: " + getIntent());
finish();
return;
}
mPackageName = data.getSchemeSpecificPart();
if (mPackageName == null) {
debugLog(
"No data supplied for IGNORE_BATTERY_OPTIMIZATION_SETTINGS in: " + getIntent());
finish();
return;
}
PowerManager power = getSystemService(PowerManager.class);
if (power.isIgnoringBatteryOptimizations(mPackageName)) {
debugLog("Not should prompt, already ignoring optimizations: " + mPackageName);
finish();
return;
}
if (getPackageManager()
.checkPermission(
Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
mPackageName)
!= PackageManager.PERMISSION_GRANTED) {
debugLog(
"Requested package "
+ mPackageName
+ " does not hold permission "
+ Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
finish();
return;
}
ApplicationInfo ai;
try {
ai = getPackageManager().getApplicationInfo(mPackageName, 0);
} catch (PackageManager.NameNotFoundException e) {
debugLog("Requested package doesn't exist: " + mPackageName);
finish();
return;
}
final AlertController.AlertParams p = mAlertParams;
final CharSequence appLabel =
ai.loadSafeLabel(
getPackageManager(),
PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX,
PackageItemInfo.SAFE_LABEL_FLAG_TRIM
| PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE);
p.mTitle = getText(R.string.high_power_prompt_title);
p.mMessage = getString(R.string.high_power_prompt_body, appLabel);
p.mPositiveButtonText = getText(R.string.allow);
p.mNegativeButtonText = getText(R.string.deny);
p.mPositiveButtonListener = this;
p.mNegativeButtonListener = this;
setupAlert();
}
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case BUTTON_POSITIVE:
mPowerWhitelistManager.addToWhitelist(mPackageName);
break;
case BUTTON_NEGATIVE:
break;
}
}
private static void debugLog(String debugContent) {
if (DEBUG) Log.w(TAG, debugContent);
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2018 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.AppOpsManager;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.UserManager;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settings.fuelgauge.batterytip.AppInfo;
import com.android.settings.fuelgauge.batterytip.BatteryTipUtils;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.utils.StringUtil;
import java.util.List;
/** Controller to change and update the smart battery toggle */
public class RestrictAppPreferenceController extends BasePreferenceController {
@VisibleForTesting static final String KEY_RESTRICT_APP = "restricted_app";
@VisibleForTesting List<AppInfo> mAppInfos;
private AppOpsManager mAppOpsManager;
private InstrumentedPreferenceFragment mPreferenceFragment;
private UserManager mUserManager;
private boolean mEnableAppBatteryUsagePage;
public RestrictAppPreferenceController(Context context) {
super(context, KEY_RESTRICT_APP);
mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
mUserManager = context.getSystemService(UserManager.class);
mAppInfos = BatteryTipUtils.getRestrictedAppsList(mAppOpsManager, mUserManager);
mEnableAppBatteryUsagePage =
mContext.getResources().getBoolean(R.bool.config_app_battery_usage_list_enabled);
}
public RestrictAppPreferenceController(InstrumentedPreferenceFragment preferenceFragment) {
this(preferenceFragment.getContext());
mPreferenceFragment = preferenceFragment;
}
@Override
public int getAvailabilityStatus() {
return mAppInfos.size() > 0 && !mEnableAppBatteryUsagePage
? AVAILABLE
: CONDITIONALLY_UNAVAILABLE;
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
mAppInfos = BatteryTipUtils.getRestrictedAppsList(mAppOpsManager, mUserManager);
final int num = mAppInfos.size();
// Fragment change RestrictedAppsList after onPause(), UI needs to be updated in onResume()
preference.setVisible(num > 0);
preference.setSummary(
StringUtil.getIcuPluralsString(mContext, num, R.string.restricted_app_summary));
}
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
if (getPreferenceKey().equals(preference.getKey())) {
// start fragment
RestrictedAppDetails.startRestrictedAppDetails(mPreferenceFragment, mAppInfos);
FeatureFactory.getFeatureFactory()
.getMetricsFeatureProvider()
.action(mContext, SettingsEnums.ACTION_APP_RESTRICTED_LIST_MANAGED);
return true;
}
return super.handlePreferenceTreeClick(preference);
}
}

View File

@@ -0,0 +1,223 @@
/*
* Copyright (C) 2018 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.settings.SettingsEnums;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.UserHandle;
import android.util.IconDrawableFactory;
import android.util.Log;
import android.util.SparseLongArray;
import androidx.annotation.VisibleForTesting;
import androidx.preference.CheckBoxPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper;
import com.android.settings.fuelgauge.batterytip.AppInfo;
import com.android.settings.fuelgauge.batterytip.BatteryDatabaseManager;
import com.android.settings.fuelgauge.batterytip.BatteryTipDialogFragment;
import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import com.android.settings.fuelgauge.batterytip.tips.RestrictAppTip;
import com.android.settings.fuelgauge.batterytip.tips.UnrestrictAppTip;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.AppCheckBoxPreference;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.utils.StringUtil;
import java.util.List;
/** Fragment to show a list of anomaly apps, where user could handle these anomalies */
public class RestrictedAppDetails extends DashboardFragment
implements BatteryTipPreferenceController.BatteryTipListener {
public static final String TAG = "RestrictedAppDetails";
@VisibleForTesting static final String EXTRA_APP_INFO_LIST = "app_info_list";
private static final String KEY_PREF_RESTRICTED_APP_LIST = "restrict_app_list";
private static final long TIME_NULL = -1;
@VisibleForTesting List<AppInfo> mAppInfos;
@VisibleForTesting IconDrawableFactory mIconDrawableFactory;
@VisibleForTesting PreferenceGroup mRestrictedAppListGroup;
@VisibleForTesting BatteryUtils mBatteryUtils;
@VisibleForTesting PackageManager mPackageManager;
@VisibleForTesting BatteryDatabaseManager mBatteryDatabaseManager;
private MetricsFeatureProvider mMetricsFeatureProvider;
/** Starts restricted app details page */
public static void startRestrictedAppDetails(
InstrumentedPreferenceFragment fragment, List<AppInfo> appInfos) {
final Bundle args = new Bundle();
args.putParcelableList(EXTRA_APP_INFO_LIST, appInfos);
new SubSettingLauncher(fragment.getContext())
.setDestination(RestrictedAppDetails.class.getName())
.setArguments(args)
.setTitleRes(R.string.restricted_app_title)
.setSourceMetricsCategory(fragment.getMetricsCategory())
.launch();
}
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
final Context context = getContext();
mRestrictedAppListGroup = (PreferenceGroup) findPreference(KEY_PREF_RESTRICTED_APP_LIST);
mAppInfos = getArguments().getParcelableArrayList(EXTRA_APP_INFO_LIST);
mPackageManager = context.getPackageManager();
mIconDrawableFactory = IconDrawableFactory.newInstance(context);
mBatteryUtils = BatteryUtils.getInstance(context);
mBatteryDatabaseManager = BatteryDatabaseManager.getInstance(context);
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
refreshUi();
}
@Override
public boolean onPreferenceTreeClick(Preference preference) {
return super.onPreferenceTreeClick(preference);
}
@Override
protected String getLogTag() {
return TAG;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.restricted_apps_detail;
}
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
return null;
}
@Override
public int getMetricsCategory() {
return SettingsEnums.FUELGAUGE_RESTRICTED_APP_DETAILS;
}
@Override
public int getHelpResource() {
return R.string.help_uri_restricted_apps;
}
@VisibleForTesting
void refreshUi() {
mRestrictedAppListGroup.removeAll();
final Context context = getPrefContext();
final SparseLongArray timestampArray =
mBatteryDatabaseManager.queryActionTime(
AnomalyDatabaseHelper.ActionType.RESTRICTION);
final long now = System.currentTimeMillis();
for (int i = 0, size = mAppInfos.size(); i < size; i++) {
final CheckBoxPreference checkBoxPreference = new AppCheckBoxPreference(context);
final AppInfo appInfo = mAppInfos.get(i);
try {
final ApplicationInfo applicationInfo =
mPackageManager.getApplicationInfoAsUser(
appInfo.packageName,
0 /* flags */,
UserHandle.getUserId(appInfo.uid));
checkBoxPreference.setChecked(
mBatteryUtils.isForceAppStandbyEnabled(appInfo.uid, appInfo.packageName));
checkBoxPreference.setTitle(mPackageManager.getApplicationLabel(applicationInfo));
checkBoxPreference.setIcon(
Utils.getBadgedIcon(
mIconDrawableFactory,
mPackageManager,
appInfo.packageName,
UserHandle.getUserId(appInfo.uid)));
checkBoxPreference.setKey(getKeyFromAppInfo(appInfo));
checkBoxPreference.setOnPreferenceChangeListener(
(pref, value) -> {
final BatteryTipDialogFragment fragment =
createDialogFragment(appInfo, (Boolean) value);
fragment.setTargetFragment(this, 0 /* requestCode */);
fragment.show(getFragmentManager(), TAG);
mMetricsFeatureProvider.action(
getContext(),
SettingsEnums.ACTION_APP_RESTRICTED_LIST_UNCHECKED,
appInfo.packageName);
return false;
});
final long timestamp = timestampArray.get(appInfo.uid, TIME_NULL);
if (timestamp != TIME_NULL) {
checkBoxPreference.setSummary(
getString(
R.string.restricted_app_time_summary,
StringUtil.formatRelativeTime(
context, now - timestamp, false)));
}
final CharSequence test = checkBoxPreference.getSummaryOn();
mRestrictedAppListGroup.addPreference(checkBoxPreference);
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Can't find package: " + appInfo.packageName);
}
}
}
@Override
public void onBatteryTipHandled(BatteryTip batteryTip) {
final AppInfo appInfo;
final boolean isRestricted = batteryTip instanceof RestrictAppTip;
if (isRestricted) {
appInfo = ((RestrictAppTip) batteryTip).getRestrictAppList().get(0);
} else {
appInfo = ((UnrestrictAppTip) batteryTip).getUnrestrictAppInfo();
}
CheckBoxPreference preference =
(CheckBoxPreference)
mRestrictedAppListGroup.findPreference(getKeyFromAppInfo(appInfo));
if (preference != null) {
preference.setChecked(isRestricted);
}
}
@VisibleForTesting
BatteryTipDialogFragment createDialogFragment(AppInfo appInfo, boolean toRestrict) {
final BatteryTip batteryTip =
toRestrict
? new RestrictAppTip(BatteryTip.StateType.NEW, appInfo)
: new UnrestrictAppTip(BatteryTip.StateType.NEW, appInfo);
return BatteryTipDialogFragment.newInstance(batteryTip, getMetricsCategory());
}
@VisibleForTesting
String getKeyFromAppInfo(AppInfo appInfo) {
return appInfo.uid + "," + appInfo.packageName;
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) 2018 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.content.Context;
import android.provider.Settings;
import android.text.TextUtils;
import androidx.preference.Preference;
import androidx.preference.TwoStatePreference;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.overlay.FeatureFactory;
/** Controller to change and update the smart battery toggle */
public class SmartBatteryPreferenceController extends BasePreferenceController
implements Preference.OnPreferenceChangeListener {
private static final String KEY_SMART_BATTERY = "smart_battery";
private static final int ON = 1;
private static final int OFF = 0;
private final PowerUsageFeatureProvider mPowerUsageFeatureProvider;
public SmartBatteryPreferenceController(Context context) {
super(context, KEY_SMART_BATTERY);
mPowerUsageFeatureProvider =
FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider();
}
@Override
public int getAvailabilityStatus() {
return mPowerUsageFeatureProvider.isSmartBatterySupported()
? AVAILABLE
: UNSUPPORTED_ON_DEVICE;
}
@Override
public boolean isSliceable() {
return TextUtils.equals(getPreferenceKey(), "smart_battery");
}
@Override
public boolean isPublicSlice() {
return true;
}
@Override
public int getSliceHighlightMenuRes() {
return R.string.menu_key_battery;
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
final boolean smartBatteryOn =
Settings.Global.getInt(
mContext.getContentResolver(),
Settings.Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED,
ON)
== ON;
((TwoStatePreference) preference).setChecked(smartBatteryOn);
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final boolean smartBatteryOn = (Boolean) newValue;
Settings.Global.putInt(
mContext.getContentResolver(),
Settings.Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED,
smartBatteryOn ? ON : OFF);
return true;
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2018 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.settings.SettingsEnums;
import android.content.Context;
import android.provider.SearchIndexableResource;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.search.SearchIndexable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/** Fragment to show smart battery and restricted app controls */
@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
public class SmartBatterySettings extends DashboardFragment {
public static final String TAG = "SmartBatterySettings";
@Override
public int getMetricsCategory() {
return SettingsEnums.OPEN_BATTERY_ADAPTIVE_PREFERENCES;
}
@Override
protected String getLogTag() {
return TAG;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.smart_battery_detail;
}
@Override
public int getHelpResource() {
return R.string.help_uri_smart_battery_settings;
}
@Override
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
return buildPreferenceControllers(context, (SettingsActivity) getActivity(), this);
}
private static List<AbstractPreferenceController> buildPreferenceControllers(
Context context,
SettingsActivity settingsActivity,
InstrumentedPreferenceFragment fragment) {
final List<AbstractPreferenceController> controllers = new ArrayList<>();
if (settingsActivity != null && fragment != null) {
controllers.add(new RestrictAppPreferenceController(fragment));
} else {
controllers.add(new RestrictAppPreferenceController(context));
}
return controllers;
}
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider() {
@Override
public List<SearchIndexableResource> getXmlResourcesToIndex(
Context context, boolean enabled) {
final SearchIndexableResource sir = new SearchIndexableResource(context);
sir.xmlResId = R.xml.smart_battery_detail;
return Arrays.asList(sir);
}
@Override
public List<AbstractPreferenceController> createPreferenceControllers(
Context context) {
return buildPreferenceControllers(context, null, null);
}
@Override
protected boolean isPageSearchEnabled(Context context) {
return FeatureFactory.getFeatureFactory()
.getPowerUsageFeatureProvider()
.isSmartBatterySupported();
}
};
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright (C) 2018 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.content.ComponentName;
import android.content.Context;
import android.os.BatteryManager;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.Utils;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
import com.android.settingslib.utils.ThreadUtils;
public class TopLevelBatteryPreferenceController extends BasePreferenceController
implements LifecycleObserver, OnStart, OnStop, BatteryPreferenceController {
private static final String TAG = "TopLvBatteryPrefControl";
@VisibleForTesting Preference mPreference;
@VisibleForTesting protected boolean mIsBatteryPresent = true;
private final BatteryBroadcastReceiver mBatteryBroadcastReceiver;
private BatteryInfo mBatteryInfo;
private BatteryStatusFeatureProvider mBatteryStatusFeatureProvider;
private String mBatteryStatusLabel;
public TopLevelBatteryPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
mBatteryBroadcastReceiver = new BatteryBroadcastReceiver(mContext);
mBatteryBroadcastReceiver.setBatteryChangedListener(
type -> {
Log.d(TAG, "onBatteryChanged: type=" + type);
if (type == BatteryBroadcastReceiver.BatteryUpdateType.BATTERY_NOT_PRESENT) {
mIsBatteryPresent = false;
}
BatteryInfo.getBatteryInfo(
mContext,
info -> {
Log.d(TAG, "getBatteryInfo: " + info);
mBatteryInfo = info;
updateState(mPreference);
// Update the preference summary text to the latest state.
setSummaryAsync(info);
},
true /* shortString */);
});
mBatteryStatusFeatureProvider =
FeatureFactory.getFeatureFactory().getBatteryStatusFeatureProvider();
}
@Override
public int getAvailabilityStatus() {
return mContext.getResources().getBoolean(R.bool.config_show_top_level_battery)
? AVAILABLE
: UNSUPPORTED_ON_DEVICE;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPreference = screen.findPreference(getPreferenceKey());
}
@Override
public void onStart() {
mBatteryBroadcastReceiver.register();
}
@Override
public void onStop() {
mBatteryBroadcastReceiver.unRegister();
}
@Override
public CharSequence getSummary() {
return getSummary(true /* batteryStatusUpdate */);
}
private CharSequence getSummary(boolean batteryStatusUpdate) {
// Display help message if battery is not present.
if (!mIsBatteryPresent) {
return mContext.getText(R.string.battery_missing_message);
}
return getDashboardLabel(mContext, mBatteryInfo, batteryStatusUpdate);
}
protected CharSequence getDashboardLabel(
Context context, BatteryInfo info, boolean batteryStatusUpdate) {
if (info == null || context == null) {
return null;
}
Log.d(
TAG,
"getDashboardLabel: "
+ mBatteryStatusLabel
+ " batteryStatusUpdate="
+ batteryStatusUpdate);
if (batteryStatusUpdate) {
setSummaryAsync(info);
}
return mBatteryStatusLabel == null ? generateLabel(info) : mBatteryStatusLabel;
}
private void setSummaryAsync(BatteryInfo info) {
ThreadUtils.postOnBackgroundThread(
() -> {
// Return false if built-in status should be used, will use
// updateBatteryStatus()
// method to inject the customized battery status label.
final boolean triggerBatteryStatusUpdate =
mBatteryStatusFeatureProvider.triggerBatteryStatusUpdate(this, info);
ThreadUtils.postOnMainThread(
() -> {
if (!triggerBatteryStatusUpdate) {
mBatteryStatusLabel = null; // will generateLabel()
}
mPreference.setSummary(
mBatteryStatusLabel == null
? generateLabel(info)
: mBatteryStatusLabel);
});
});
}
private CharSequence generateLabel(BatteryInfo info) {
if (Utils.containsIncompatibleChargers(mContext, TAG)) {
return mContext.getString(
com.android.settingslib.R.string.power_incompatible_charging_settings_home_page,
info.batteryPercentString);
}
if (BatteryUtils.isBatteryDefenderOn(info)) {
return mContext.getString(
com.android.settingslib.R.string.power_charging_on_hold_settings_home_page,
info.batteryPercentString);
}
if (info.batteryStatus == BatteryManager.BATTERY_STATUS_NOT_CHARGING) {
// Present status only if no remaining time or status anomalous
return info.statusLabel;
} else if (!info.discharging && info.chargeLabel != null) {
return info.chargeLabel;
} else if (info.remainingLabel == null) {
return info.batteryPercentString;
} else {
return mContext.getString(
com.android.settingslib.R.string.power_remaining_settings_home_page,
info.batteryPercentString,
info.remainingLabel);
}
}
/** Callback which receives text for the label. */
@Override
public void updateBatteryStatus(String label, BatteryInfo info) {
mBatteryStatusLabel = label; // Null if adaptive charging is not active
if (mPreference == null) {
return;
}
// Do not triggerBatteryStatusUpdate() here to cause infinite loop
final CharSequence summary = getSummary(false /* batteryStatusUpdate */);
if (summary != null) {
mPreference.setSummary(summary);
}
Log.d(TAG, "updateBatteryStatus: " + label + " summary: " + summary);
}
@VisibleForTesting
protected static ComponentName convertClassPathToComponentName(String classPath) {
if (classPath == null || classPath.isEmpty()) {
return null;
}
String[] split = classPath.split("\\.");
int classNameIndex = split.length - 1;
if (classNameIndex < 0) {
return null;
}
int lastPkgIndex = classPath.length() - split[classNameIndex].length() - 1;
String pkgName = lastPkgIndex > 0 ? classPath.substring(0, lastPkgIndex) : "";
return new ComponentName(pkgName, split[classNameIndex]);
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright (C) 2021 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.content.Context;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.widget.SelectorWithWidgetPreference;
public class UnrestrictedPreferenceController extends AbstractPreferenceController
implements PreferenceControllerMixin {
private static final String TAG = "UNRESTRICTED_PREF";
@VisibleForTesting static final String KEY_UNRESTRICTED_PREF = "unrestricted_preference";
@VisibleForTesting BatteryOptimizeUtils mBatteryOptimizeUtils;
public UnrestrictedPreferenceController(Context context, int uid, String packageName) {
super(context);
mBatteryOptimizeUtils = new BatteryOptimizeUtils(context, uid, packageName);
}
@Override
public void updateState(Preference preference) {
preference.setEnabled(mBatteryOptimizeUtils.isSelectorPreferenceEnabled());
final boolean isUnrestricted =
mBatteryOptimizeUtils.getAppOptimizationMode()
== BatteryOptimizeUtils.MODE_UNRESTRICTED;
((SelectorWithWidgetPreference) preference).setChecked(isUnrestricted);
}
@Override
public boolean isAvailable() {
return true;
}
@Override
public String getPreferenceKey() {
return KEY_UNRESTRICTED_PREF;
}
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
return getPreferenceKey().equals(preference.getKey());
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright (C) 2018 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.batterysaver;
import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_SETTINGS;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.provider.SettingsSlicesContract;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.core.TogglePreferenceController;
import com.android.settings.fuelgauge.BatterySaverReceiver;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
import com.android.settingslib.fuelgauge.BatterySaverUtils;
import com.android.settingslib.widget.MainSwitchPreference;
/** Controller to update the battery saver button */
public class BatterySaverButtonPreferenceController extends TogglePreferenceController
implements LifecycleObserver, OnStart, OnStop, BatterySaverReceiver.BatterySaverListener {
private static final long SWITCH_ANIMATION_DURATION = 350L;
private final BatterySaverReceiver mBatterySaverReceiver;
private final PowerManager mPowerManager;
private Handler mHandler;
private MainSwitchPreference mPreference;
public BatterySaverButtonPreferenceController(Context context, String key) {
super(context, key);
mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
mBatterySaverReceiver = new BatterySaverReceiver(context);
mBatterySaverReceiver.setBatterySaverListener(this);
mHandler = new Handler(Looper.getMainLooper());
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@Override
public boolean isPublicSlice() {
return true;
}
@Override
public Uri getSliceUri() {
return new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(SettingsSlicesContract.AUTHORITY)
.appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
.appendPath(SettingsSlicesContract.KEY_BATTERY_SAVER)
.build();
}
@Override
public void onStart() {
mBatterySaverReceiver.setListening(true);
}
@Override
public void onStop() {
mBatterySaverReceiver.setListening(false);
mHandler.removeCallbacksAndMessages(null /* token */);
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPreference = screen.findPreference(getPreferenceKey());
mPreference.updateStatus(isChecked());
}
@Override
public boolean isChecked() {
return mPowerManager.isPowerSaveMode();
}
@Override
public boolean setChecked(boolean stateOn) {
return BatterySaverUtils.setPowerSaveMode(
mContext, stateOn, false /* needFirstTimeWarning */, SAVER_ENABLED_SETTINGS);
}
@Override
public int getSliceHighlightMenuRes() {
return R.string.menu_key_battery;
}
@Override
public void onPowerSaveModeChanged() {
mHandler.postDelayed(() -> onPowerSaveModeChangedInternal(), SWITCH_ANIMATION_DURATION);
}
private void onPowerSaveModeChangedInternal() {
final boolean isChecked = isChecked();
if (mPreference != null && mPreference.isChecked() != isChecked) {
mPreference.setChecked(isChecked);
}
}
@Override
public void onBatteryChanged(boolean pluggedIn) {
if (mPreference != null) {
mPreference.setEnabled(!pluggedIn);
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2018 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.batterysaver;
import static com.android.settingslib.fuelgauge.BatterySaverUtils.KEY_PERCENTAGE;
import android.content.ContentResolver;
import android.content.Context;
import android.provider.Settings;
import android.provider.Settings.Global;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.fuelgauge.BatterySaverUtils;
/**
* Simple controller to navigate users to the scheduling page from "Settings > Battery > Battery
* Saver". Also updates the summary for preference based on the currently selected settings.
*/
public class BatterySaverSchedulePreferenceController extends BasePreferenceController {
@VisibleForTesting Preference mBatterySaverSchedulePreference;
public static final String KEY_BATTERY_SAVER_SCHEDULE = "battery_saver_schedule";
public BatterySaverSchedulePreferenceController(Context context) {
super(context, KEY_BATTERY_SAVER_SCHEDULE);
BatterySaverUtils.revertScheduleToNoneIfNeeded(context);
}
@Override
public String getPreferenceKey() {
return KEY_BATTERY_SAVER_SCHEDULE;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mBatterySaverSchedulePreference = screen.findPreference(KEY_BATTERY_SAVER_SCHEDULE);
}
@Override
public CharSequence getSummary() {
final ContentResolver resolver = mContext.getContentResolver();
final String mode = BatterySaverUtils.getBatterySaverScheduleKey(mContext);
if (KEY_PERCENTAGE.equals(mode)) {
final int threshold =
Settings.Global.getInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
return mContext.getString(
R.string.battery_saver_auto_percentage_summary,
Utils.formatPercentage(threshold));
}
return mContext.getText(R.string.battery_saver_auto_no_schedule);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2018 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.batterysaver;
import static com.android.settingslib.fuelgauge.BatterySaverUtils.KEY_NO_SCHEDULE;
import static com.android.settingslib.fuelgauge.BatterySaverUtils.KEY_PERCENTAGE;
import android.content.ContentResolver;
import android.content.Context;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import android.provider.Settings.Global;
import android.text.TextUtils;
import com.android.settingslib.fuelgauge.BatterySaverUtils;
/**
* Responds to user actions in the Settings > Battery > Set a Schedule Screen
*
* <p>Note that this is not a preference controller since that screen does not inherit from
* DashboardFragment.
*
* <p>Will call the appropriate power manager APIs and modify the correct settings to enable users
* to control their automatic battery saver toggling preferences. See {@link
* Settings.Global#AUTOMATIC_POWER_SAVE_MODE} for more details.
*/
public class BatterySaverScheduleRadioButtonsController {
private static final String TAG = "BatterySaverScheduleRadioButtonsController";
public static final int TRIGGER_LEVEL_MIN = 20;
private Context mContext;
private BatterySaverScheduleSeekBarController mSeekBarController;
public BatterySaverScheduleRadioButtonsController(
Context context, BatterySaverScheduleSeekBarController seekbar) {
mContext = context;
mSeekBarController = seekbar;
}
public boolean setDefaultKey(String key) {
if (key == null) {
return false;
}
final ContentResolver resolver = mContext.getContentResolver();
int mode = PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE;
int triggerLevel = 0;
final Bundle confirmationExtras = new Bundle(3);
switch (key) {
case KEY_NO_SCHEDULE:
break;
case KEY_PERCENTAGE:
triggerLevel = TRIGGER_LEVEL_MIN;
confirmationExtras.putBoolean(BatterySaverUtils.EXTRA_CONFIRM_TEXT_ONLY, true);
confirmationExtras.putInt(
BatterySaverUtils.EXTRA_POWER_SAVE_MODE_TRIGGER,
PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
confirmationExtras.putInt(
BatterySaverUtils.EXTRA_POWER_SAVE_MODE_TRIGGER_LEVEL, triggerLevel);
break;
default:
throw new IllegalStateException(
"Not a valid key for " + this.getClass().getSimpleName());
}
if (!TextUtils.equals(key, KEY_NO_SCHEDULE)
&& BatterySaverUtils.maybeShowBatterySaverConfirmation(
mContext, confirmationExtras)) {
// reset this if we need to show the confirmation message
mode = PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE;
triggerLevel = 0;
}
// Trigger level is intentionally left alone when going between dynamic and percentage modes
// so that a users percentage based schedule is preserved when they toggle between the two.
Settings.Global.putInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE, mode);
if (mode != PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC) {
Settings.Global.putInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, triggerLevel);
}
// Suppress battery saver suggestion notification if enabling scheduling battery saver.
if (mode == PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC || triggerLevel != 0) {
BatterySaverUtils.suppressAutoBatterySaver(mContext);
}
if (mSeekBarController != null) {
mSeekBarController.updateSeekBar();
}
return true;
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright (C) 2018 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.batterysaver;
import static com.android.settingslib.fuelgauge.BatterySaverUtils.KEY_PERCENTAGE;
import android.content.ContentResolver;
import android.content.Context;
import android.provider.Settings;
import android.provider.Settings.Global;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import androidx.preference.Preference;
import androidx.preference.Preference.OnPreferenceChangeListener;
import androidx.preference.PreferenceScreen;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.widget.SeekBarPreference;
import com.android.settingslib.fuelgauge.BatterySaverUtils;
/**
* Responds to user actions in the Settings > Battery > Set a Schedule Screen for the seekbar. Note
* that this seekbar is only visible when the radio button selected is "Percentage".
*
* <p>Note that this is not a preference controller since that screen does not inherit from
* DashboardFragment.
*
* <p>Will call the appropriate power manager APIs and modify the correct settings to enable users
* to control their automatic battery saver toggling preferences. See {@link
* Settings.Global#AUTOMATIC_POWER_SAVE_MODE} for more details.
*/
public class BatterySaverScheduleSeekBarController
implements OnPreferenceChangeListener, OnSeekBarChangeListener {
public static final int MAX_SEEKBAR_VALUE = 15;
public static final int MIN_SEEKBAR_VALUE = 2;
public static final String KEY_BATTERY_SAVER_SEEK_BAR = "battery_saver_seek_bar";
private static final int LEVEL_UNIT_SCALE = 5;
@VisibleForTesting public SeekBarPreference mSeekBarPreference;
private Context mContext;
@VisibleForTesting int mPercentage;
public BatterySaverScheduleSeekBarController(Context context) {
mContext = context;
mSeekBarPreference = new SeekBarPreference(context);
mSeekBarPreference.setLayoutResource(R.layout.preference_widget_seekbar_settings);
mSeekBarPreference.setIconSpaceReserved(false);
mSeekBarPreference.setOnPreferenceChangeListener(this);
mSeekBarPreference.setOnSeekBarChangeListener(this);
mSeekBarPreference.setContinuousUpdates(true);
mSeekBarPreference.setMax(MAX_SEEKBAR_VALUE);
mSeekBarPreference.setMin(MIN_SEEKBAR_VALUE);
mSeekBarPreference.setKey(KEY_BATTERY_SAVER_SEEK_BAR);
mSeekBarPreference.setHapticFeedbackMode(SeekBarPreference.HAPTIC_FEEDBACK_MODE_ON_TICKS);
updateSeekBar();
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
mPercentage = ((Integer) newValue) * LEVEL_UNIT_SCALE;
final CharSequence stateDescription = formatStateDescription(mPercentage);
preference.setTitle(stateDescription);
mSeekBarPreference.overrideSeekBarStateDescription(stateDescription);
return true;
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (mPercentage > 0) {
Settings.Global.putInt(
mContext.getContentResolver(),
Global.LOW_POWER_MODE_TRIGGER_LEVEL,
mPercentage);
}
}
public void updateSeekBar() {
final ContentResolver resolver = mContext.getContentResolver();
final String mode = BatterySaverUtils.getBatterySaverScheduleKey(mContext);
if (KEY_PERCENTAGE.equals(mode)) {
final int threshold =
Settings.Global.getInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
final int currentSeekbarValue = Math.max(threshold / 5, MIN_SEEKBAR_VALUE);
mSeekBarPreference.setVisible(true);
mSeekBarPreference.setProgress(currentSeekbarValue);
final CharSequence stateDescription = formatStateDescription(currentSeekbarValue * 5);
mSeekBarPreference.setTitle(stateDescription);
mSeekBarPreference.overrideSeekBarStateDescription(stateDescription);
} else {
mSeekBarPreference.setVisible(false);
}
}
/**
* Adds the seekbar to the end of the provided preference screen
*
* @param screen The preference screen to add the seekbar to
*/
public void addToScreen(PreferenceScreen screen) {
// makes sure it gets added after the preferences if called due to first time battery
// saver message
mSeekBarPreference.setOrder(100);
screen.addPreference(mSeekBarPreference);
}
private CharSequence formatStateDescription(int percentage) {
return mContext.getString(
R.string.battery_saver_seekbar_title, Utils.formatPercentage(percentage));
}
}

View File

@@ -0,0 +1,238 @@
/*
* Copyright (C) 2018 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.batterysaver;
import static com.android.settingslib.fuelgauge.BatterySaverUtils.KEY_NO_SCHEDULE;
import static com.android.settingslib.fuelgauge.BatterySaverUtils.KEY_PERCENTAGE;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.database.ContentObserver;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.RadioButtonPickerFragment;
import com.android.settingslib.fuelgauge.BatterySaverUtils;
import com.android.settingslib.widget.CandidateInfo;
import com.android.settingslib.widget.SelectorWithWidgetPreference;
import com.google.common.collect.Lists;
import java.util.List;
/**
* Fragment that allows users to customize their automatic battery saver mode settings. <br>
* <br>
* Location: Settings > Battery > Battery Saver > Set a Schedule <br>
* See {@link BatterySaverSchedulePreferenceController} for the controller that manages navigation
* to this screen from "Settings > Battery > Battery Saver" and the summary. <br>
* See {@link BatterySaverScheduleRadioButtonsController} & {@link
* BatterySaverScheduleSeekBarController} for the controller that manages user interactions in this
* screen.
*/
public class BatterySaverScheduleSettings extends RadioButtonPickerFragment {
public BatterySaverScheduleRadioButtonsController mRadioButtonController;
@VisibleForTesting Context mContext;
private int mSaverPercentage;
private String mSaverScheduleKey;
private BatterySaverScheduleSeekBarController mSeekBarController;
@VisibleForTesting
final ContentObserver mSettingsObserver =
new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange, Uri uri) {
getPreferenceScreen().removeAll();
updateCandidates();
}
};
@Override
protected int getPreferenceScreenResId() {
return R.xml.battery_saver_schedule_settings;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
mSeekBarController = new BatterySaverScheduleSeekBarController(context);
mRadioButtonController =
new BatterySaverScheduleRadioButtonsController(context, mSeekBarController);
mContext = context;
}
@Override
public void onResume() {
super.onResume();
mContext.getContentResolver()
.registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.LOW_POWER_WARNING_ACKNOWLEDGED),
false,
mSettingsObserver);
mSaverScheduleKey = BatterySaverUtils.getBatterySaverScheduleKey(mContext);
mSaverPercentage = getSaverPercentage();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setDivider(new ColorDrawable(Color.TRANSPARENT));
setDividerHeight(0);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void onPause() {
mContext.getContentResolver().unregisterContentObserver(mSettingsObserver);
AsyncTask.execute(() -> logPowerSaver());
super.onPause();
}
@Override
protected List<? extends CandidateInfo> getCandidates() {
Context context = getContext();
List<CandidateInfo> candidates = Lists.newArrayList();
candidates.add(
new BatterySaverScheduleCandidateInfo(
context.getText(R.string.battery_saver_auto_no_schedule),
/* summary */ null,
KEY_NO_SCHEDULE,
/* enabled */ true));
BatterySaverUtils.revertScheduleToNoneIfNeeded(context);
candidates.add(
new BatterySaverScheduleCandidateInfo(
context.getText(R.string.battery_saver_auto_percentage),
/* summary */ null,
KEY_PERCENTAGE,
/* enabled */ true));
return candidates;
}
@Override
public void bindPreferenceExtra(
SelectorWithWidgetPreference pref,
String key,
CandidateInfo info,
String defaultKey,
String systemDefaultKey) {
final BatterySaverScheduleCandidateInfo candidateInfo =
(BatterySaverScheduleCandidateInfo) info;
final CharSequence summary = candidateInfo.getSummary();
if (summary != null) {
pref.setSummary(summary);
pref.setAppendixVisibility(View.GONE);
}
}
@Override
protected void addStaticPreferences(PreferenceScreen screen) {
mSeekBarController.updateSeekBar();
mSeekBarController.addToScreen(screen);
}
@Override
protected String getDefaultKey() {
return BatterySaverUtils.getBatterySaverScheduleKey(mContext);
}
@Override
protected boolean setDefaultKey(String key) {
return mRadioButtonController.setDefaultKey(key);
}
@Override
public int getMetricsCategory() {
return SettingsEnums.FUELGAUGE_BATTERY_SAVER_SCHEDULE;
}
private void logPowerSaver() {
final int currentSaverPercentage = getSaverPercentage();
final String currentSaverScheduleKey =
BatterySaverUtils.getBatterySaverScheduleKey(mContext);
if (mSaverScheduleKey.equals(currentSaverScheduleKey)
&& mSaverPercentage == currentSaverPercentage) {
return;
}
FeatureFactory.getFeatureFactory()
.getMetricsFeatureProvider()
.action(
SettingsEnums.FUELGAUGE_BATTERY_SAVER,
SettingsEnums.FIELD_BATTERY_SAVER_SCHEDULE_TYPE,
SettingsEnums.FIELD_BATTERY_SAVER_PERCENTAGE_VALUE,
currentSaverScheduleKey,
currentSaverPercentage);
}
private int getSaverPercentage() {
return Settings.Global.getInt(
mContext.getContentResolver(), Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, -1);
}
static class BatterySaverScheduleCandidateInfo extends CandidateInfo {
private final CharSequence mLabel;
private final CharSequence mSummary;
private final String mKey;
BatterySaverScheduleCandidateInfo(
CharSequence label, CharSequence summary, String key, boolean enabled) {
super(enabled);
mLabel = label;
mKey = key;
mSummary = summary;
}
@Override
public CharSequence loadLabel() {
return mLabel;
}
@Override
public Drawable loadIcon() {
return null;
}
@Override
public String getKey() {
return mKey;
}
public CharSequence getSummary() {
return mSummary;
}
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright (C) 2014 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.batterysaver;
import android.app.settings.SettingsEnums;
import android.text.TextUtils;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.search.BaseSearchIndexProvider;
import com.android.settingslib.HelpUtils;
import com.android.settingslib.search.SearchIndexable;
import com.android.settingslib.widget.FooterPreference;
/** Battery saver settings page */
@SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
public class BatterySaverSettings extends DashboardFragment {
private static final String TAG = "BatterySaverSettings";
private static final String KEY_FOOTER_PREFERENCE = "battery_saver_footer_preference";
private String mHelpUri;
@Override
public void onStart() {
super.onStart();
setupFooter();
}
@Override
public int getMetricsCategory() {
return SettingsEnums.OPEN_BATTERY_SAVER;
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.battery_saver_settings;
}
@Override
protected String getLogTag() {
return TAG;
}
@Override
public int getHelpResource() {
return R.string.help_url_battery_saver_settings;
}
/** For Search. */
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider(R.xml.battery_saver_settings);
// Updates the footer for this page.
@VisibleForTesting
void setupFooter() {
mHelpUri = getString(R.string.help_url_battery_saver_settings);
if (!TextUtils.isEmpty(mHelpUri)) {
addHelpLink();
}
}
// Changes the text to include a learn more link if possible.
@VisibleForTesting
void addHelpLink() {
FooterPreference pref = getPreferenceScreen().findPreference(KEY_FOOTER_PREFERENCE);
if (pref != null) {
pref.setLearnMoreAction(
v -> {
mMetricsFeatureProvider.action(
getContext(), SettingsEnums.ACTION_APP_BATTERY_LEARN_MORE);
startActivityForResult(
HelpUtils.getHelpIntent(
getContext(),
getString(R.string.help_url_battery_saver_settings),
/* backupContext= */ ""),
/* requestCode= */ 0);
});
pref.setLearnMoreText(getString(R.string.battery_saver_link_a11y));
}
}
}

View File

@@ -0,0 +1,82 @@
package com.android.settings.fuelgauge.batterysaver;
import android.content.Context;
import android.provider.Settings;
import android.provider.Settings.Global;
import androidx.preference.Preference;
import androidx.preference.TwoStatePreference;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.core.TogglePreferenceController;
public class BatterySaverStickyPreferenceController extends TogglePreferenceController
implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener {
private static final int DEFAULT_STICKY_SHUTOFF_LEVEL = 90;
private Context mContext;
public BatterySaverStickyPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
mContext = context;
}
@Override
public boolean isChecked() {
return Settings.Global.getInt(
mContext.getContentResolver(),
Global.LOW_POWER_MODE_STICKY_AUTO_DISABLE_ENABLED,
1)
== 1;
}
@Override
public boolean setChecked(boolean isChecked) {
Settings.Global.putInt(
mContext.getContentResolver(),
Global.LOW_POWER_MODE_STICKY_AUTO_DISABLE_ENABLED,
isChecked ? 1 : 0);
return true;
}
@Override
protected void refreshSummary(Preference preference) {
super.refreshSummary(preference);
final int stickyShutoffLevel =
Settings.Global.getInt(
mContext.getContentResolver(),
Global.LOW_POWER_MODE_STICKY_AUTO_DISABLE_LEVEL,
DEFAULT_STICKY_SHUTOFF_LEVEL);
final String formatPercentage = Utils.formatPercentage(stickyShutoffLevel);
preference.setTitle(
mContext.getString(
R.string.battery_saver_sticky_title_percentage, formatPercentage));
preference.setSummary(
mContext.getString(
R.string.battery_saver_sticky_description_new, formatPercentage));
}
@Override
public void updateState(Preference preference) {
int setting =
Settings.Global.getInt(
mContext.getContentResolver(),
Global.LOW_POWER_MODE_STICKY_AUTO_DISABLE_ENABLED,
1);
((TwoStatePreference) preference).setChecked(setting == 1);
refreshSummary(preference);
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@Override
public int getSliceHighlightMenuRes() {
return R.string.menu_key_battery;
}
}

View File

@@ -0,0 +1,207 @@
/*
* Copyright (C) 2018 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.batterytip;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** Database controls the anomaly logging(e.g. packageName, anomalyType and time) */
public class AnomalyDatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = "BatteryDatabaseHelper";
private static final String DATABASE_NAME = "battery_settings.db";
private static final int DATABASE_VERSION = 5;
@Retention(RetentionPolicy.SOURCE)
@IntDef({State.NEW, State.HANDLED, State.AUTO_HANDLED})
public @interface State {
int NEW = 0;
int HANDLED = 1;
int AUTO_HANDLED = 2;
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({ActionType.RESTRICTION})
public @interface ActionType {
int RESTRICTION = 0;
}
public interface Tables {
String TABLE_ANOMALY = "anomaly";
String TABLE_ACTION = "action";
}
public interface AnomalyColumns {
/** The package name of the anomaly app */
String PACKAGE_NAME = "package_name";
/** The uid of the anomaly app */
String UID = "uid";
/**
* The type of the anomaly app
*
* @see StatsManagerConfig.AnomalyType
*/
String ANOMALY_TYPE = "anomaly_type";
/**
* The state of the anomaly app
*
* @see State
*/
String ANOMALY_STATE = "anomaly_state";
/** The time when anomaly happens */
String TIME_STAMP_MS = "time_stamp_ms";
}
private static final String CREATE_ANOMALY_TABLE =
"CREATE TABLE "
+ Tables.TABLE_ANOMALY
+ "("
+ AnomalyColumns.UID
+ " INTEGER NOT NULL, "
+ AnomalyColumns.PACKAGE_NAME
+ " TEXT, "
+ AnomalyColumns.ANOMALY_TYPE
+ " INTEGER NOT NULL, "
+ AnomalyColumns.ANOMALY_STATE
+ " INTEGER NOT NULL, "
+ AnomalyColumns.TIME_STAMP_MS
+ " INTEGER NOT NULL, "
+ " PRIMARY KEY ("
+ AnomalyColumns.UID
+ ","
+ AnomalyColumns.ANOMALY_TYPE
+ ","
+ AnomalyColumns.ANOMALY_STATE
+ ","
+ AnomalyColumns.TIME_STAMP_MS
+ ")"
+ ")";
public interface ActionColumns {
/** The package name of an app been performed an action */
String PACKAGE_NAME = "package_name";
/** The uid of an app been performed an action */
String UID = "uid";
/**
* The type of user action
*
* @see ActionType
*/
String ACTION_TYPE = "action_type";
/** The time when action been performed */
String TIME_STAMP_MS = "time_stamp_ms";
}
private static final String CREATE_ACTION_TABLE =
"CREATE TABLE "
+ Tables.TABLE_ACTION
+ "("
+ ActionColumns.UID
+ " INTEGER NOT NULL, "
+ ActionColumns.PACKAGE_NAME
+ " TEXT, "
+ ActionColumns.ACTION_TYPE
+ " INTEGER NOT NULL, "
+ ActionColumns.TIME_STAMP_MS
+ " INTEGER NOT NULL, "
+ " PRIMARY KEY ("
+ ActionColumns.ACTION_TYPE
+ ","
+ ActionColumns.UID
+ ","
+ ActionColumns.PACKAGE_NAME
+ ")"
+ ")";
private static AnomalyDatabaseHelper sSingleton;
public static synchronized AnomalyDatabaseHelper getInstance(Context context) {
if (sSingleton == null) {
sSingleton = new AnomalyDatabaseHelper(context.getApplicationContext());
}
return sSingleton;
}
private AnomalyDatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
bootstrapDB(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < DATABASE_VERSION) {
Log.w(
TAG,
"Detected schema version '"
+ oldVersion
+ "'. "
+ "Index needs to be rebuilt for schema version '"
+ newVersion
+ "'.");
// We need to drop the tables and recreate them
reconstruct(db);
}
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(
TAG,
"Detected schema version '"
+ oldVersion
+ "'. "
+ "Index needs to be rebuilt for schema version '"
+ newVersion
+ "'.");
// We need to drop the tables and recreate them
reconstruct(db);
}
public void reconstruct(SQLiteDatabase db) {
dropTables(db);
bootstrapDB(db);
}
private void bootstrapDB(SQLiteDatabase db) {
db.execSQL(CREATE_ANOMALY_TABLE);
db.execSQL(CREATE_ACTION_TABLE);
Log.i(TAG, "Bootstrapped database");
}
private void dropTables(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_ANOMALY);
db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_ACTION);
}
}

View File

@@ -0,0 +1,142 @@
/*
* Copyright (C) 2018 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.batterytip;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.ArraySet;
import androidx.annotation.VisibleForTesting;
import java.util.Objects;
/** Model class stores app info(e.g. package name, type..) that used in battery tip */
public class AppInfo implements Comparable<AppInfo>, Parcelable {
public final String packageName;
/**
* Anomaly type of the app
*
* @see StatsManagerConfig.AnomalyType
*/
public final ArraySet<Integer> anomalyTypes;
public final long screenOnTimeMs;
public final int uid;
private AppInfo(AppInfo.Builder builder) {
packageName = builder.mPackageName;
anomalyTypes = builder.mAnomalyTypes;
screenOnTimeMs = builder.mScreenOnTimeMs;
uid = builder.mUid;
}
@VisibleForTesting
AppInfo(Parcel in) {
packageName = in.readString();
anomalyTypes = (ArraySet<Integer>) in.readArraySet(null /* loader */);
screenOnTimeMs = in.readLong();
uid = in.readInt();
}
@Override
public int compareTo(AppInfo o) {
return Long.compare(screenOnTimeMs, o.screenOnTimeMs);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(packageName);
dest.writeArraySet(anomalyTypes);
dest.writeLong(screenOnTimeMs);
dest.writeInt(uid);
}
@Override
public String toString() {
return "packageName="
+ packageName
+ ",anomalyTypes="
+ anomalyTypes
+ ",screenTime="
+ screenOnTimeMs;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof AppInfo)) {
return false;
}
AppInfo other = (AppInfo) obj;
return Objects.equals(anomalyTypes, other.anomalyTypes)
&& uid == other.uid
&& screenOnTimeMs == other.screenOnTimeMs
&& TextUtils.equals(packageName, other.packageName);
}
public static final Parcelable.Creator CREATOR =
new Parcelable.Creator() {
public AppInfo createFromParcel(Parcel in) {
return new AppInfo(in);
}
public AppInfo[] newArray(int size) {
return new AppInfo[size];
}
};
public static final class Builder {
private ArraySet<Integer> mAnomalyTypes = new ArraySet<>();
private String mPackageName;
private long mScreenOnTimeMs;
private int mUid;
public Builder addAnomalyType(int type) {
mAnomalyTypes.add(type);
return this;
}
public Builder setPackageName(String packageName) {
mPackageName = packageName;
return this;
}
public Builder setScreenOnTimeMs(long screenOnTimeMs) {
mScreenOnTimeMs = screenOnTimeMs;
return this;
}
public Builder setUid(int uid) {
mUid = uid;
return this;
}
public AppInfo build() {
return new AppInfo(this);
}
}
}

View File

@@ -0,0 +1,246 @@
/*
* Copyright (C) 2018 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.batterytip;
import static android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE;
import static android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE;
import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns.ANOMALY_STATE;
import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns.ANOMALY_TYPE;
import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns.PACKAGE_NAME;
import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns.TIME_STAMP_MS;
import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.AnomalyColumns.UID;
import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.Tables.TABLE_ACTION;
import static com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.Tables.TABLE_ANOMALY;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.SparseLongArray;
import androidx.annotation.VisibleForTesting;
import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper.ActionColumns;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Database manager for battery data. Now it only contains anomaly data stored in {@link AppInfo}.
*
* <p>This manager may be accessed by multi-threads. All the database related methods are
* synchronized so each operation won't be interfered by other threads.
*/
public class BatteryDatabaseManager {
private static BatteryDatabaseManager sSingleton;
private AnomalyDatabaseHelper mDatabaseHelper;
private BatteryDatabaseManager(Context context) {
mDatabaseHelper = AnomalyDatabaseHelper.getInstance(context);
}
public static synchronized BatteryDatabaseManager getInstance(Context context) {
if (sSingleton == null) {
sSingleton = new BatteryDatabaseManager(context);
}
return sSingleton;
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
public static void setUpForTest(BatteryDatabaseManager batteryDatabaseManager) {
sSingleton = batteryDatabaseManager;
}
/**
* Insert an anomaly log to database.
*
* @param uid the uid of the app
* @param packageName the package name of the app
* @param type the type of the anomaly
* @param anomalyState the state of the anomaly
* @param timestampMs the time when it is happened
* @return {@code true} if insert operation succeed
*/
public synchronized boolean insertAnomaly(
int uid, String packageName, int type, int anomalyState, long timestampMs) {
final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(UID, uid);
values.put(PACKAGE_NAME, packageName);
values.put(ANOMALY_TYPE, type);
values.put(ANOMALY_STATE, anomalyState);
values.put(TIME_STAMP_MS, timestampMs);
return db.insertWithOnConflict(TABLE_ANOMALY, null, values, CONFLICT_IGNORE) != -1;
}
/**
* Query all the anomalies that happened after {@code timestampMsAfter} and with {@code state}.
*/
public synchronized List<AppInfo> queryAllAnomalies(long timestampMsAfter, int state) {
final List<AppInfo> appInfos = new ArrayList<>();
final SQLiteDatabase db = mDatabaseHelper.getReadableDatabase();
final String[] projection = {PACKAGE_NAME, ANOMALY_TYPE, UID};
final String orderBy = AnomalyDatabaseHelper.AnomalyColumns.TIME_STAMP_MS + " DESC";
final Map<Integer, AppInfo.Builder> mAppInfoBuilders = new ArrayMap<>();
final String selection = TIME_STAMP_MS + " > ? AND " + ANOMALY_STATE + " = ? ";
final String[] selectionArgs =
new String[] {String.valueOf(timestampMsAfter), String.valueOf(state)};
try (Cursor cursor =
db.query(
TABLE_ANOMALY,
projection,
selection,
selectionArgs,
null /* groupBy */,
null /* having */,
orderBy)) {
while (cursor.moveToNext()) {
final int uid = cursor.getInt(cursor.getColumnIndex(UID));
if (!mAppInfoBuilders.containsKey(uid)) {
final AppInfo.Builder builder =
new AppInfo.Builder()
.setUid(uid)
.setPackageName(
cursor.getString(cursor.getColumnIndex(PACKAGE_NAME)));
mAppInfoBuilders.put(uid, builder);
}
mAppInfoBuilders
.get(uid)
.addAnomalyType(cursor.getInt(cursor.getColumnIndex(ANOMALY_TYPE)));
}
}
for (Integer uid : mAppInfoBuilders.keySet()) {
appInfos.add(mAppInfoBuilders.get(uid).build());
}
return appInfos;
}
public synchronized void deleteAllAnomaliesBeforeTimeStamp(long timestampMs) {
final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
db.delete(
TABLE_ANOMALY, TIME_STAMP_MS + " < ?", new String[] {String.valueOf(timestampMs)});
}
/**
* Update the type of anomalies to {@code state}
*
* @param appInfos represents the anomalies
* @param state which state to update to
*/
public synchronized void updateAnomalies(List<AppInfo> appInfos, int state) {
if (!appInfos.isEmpty()) {
final int size = appInfos.size();
final String[] whereArgs = new String[size];
for (int i = 0; i < size; i++) {
whereArgs[i] = appInfos.get(i).packageName;
}
final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
final ContentValues values = new ContentValues();
values.put(ANOMALY_STATE, state);
db.update(
TABLE_ANOMALY,
values,
PACKAGE_NAME
+ " IN ("
+ TextUtils.join(",", Collections.nCopies(appInfos.size(), "?"))
+ ")",
whereArgs);
}
}
/**
* Query latest timestamps when an app has been performed action {@code type}
*
* @param type of action been performed
* @return {@link SparseLongArray} where key is uid and value is timestamp
*/
public synchronized SparseLongArray queryActionTime(
@AnomalyDatabaseHelper.ActionType int type) {
final SparseLongArray timeStamps = new SparseLongArray();
final SQLiteDatabase db = mDatabaseHelper.getReadableDatabase();
final String[] projection = {ActionColumns.UID, ActionColumns.TIME_STAMP_MS};
final String selection = ActionColumns.ACTION_TYPE + " = ? ";
final String[] selectionArgs = new String[] {String.valueOf(type)};
try (Cursor cursor =
db.query(
TABLE_ACTION,
projection,
selection,
selectionArgs,
null /* groupBy */,
null /* having */,
null /* orderBy */)) {
final int uidIndex = cursor.getColumnIndex(ActionColumns.UID);
final int timestampIndex = cursor.getColumnIndex(ActionColumns.TIME_STAMP_MS);
while (cursor.moveToNext()) {
final int uid = cursor.getInt(uidIndex);
final long timeStamp = cursor.getLong(timestampIndex);
timeStamps.append(uid, timeStamp);
}
}
return timeStamps;
}
/** Insert an action, or update it if already existed */
public synchronized boolean insertAction(
@AnomalyDatabaseHelper.ActionType int type,
int uid,
String packageName,
long timestampMs) {
final SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
final ContentValues values = new ContentValues();
values.put(ActionColumns.UID, uid);
values.put(ActionColumns.PACKAGE_NAME, packageName);
values.put(ActionColumns.ACTION_TYPE, type);
values.put(ActionColumns.TIME_STAMP_MS, timestampMs);
return db.insertWithOnConflict(TABLE_ACTION, null, values, CONFLICT_REPLACE) != -1;
}
/** Remove an action */
public synchronized boolean deleteAction(
@AnomalyDatabaseHelper.ActionType int type, int uid, String packageName) {
SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
final String where =
ActionColumns.ACTION_TYPE
+ " = ? AND "
+ ActionColumns.UID
+ " = ? AND "
+ ActionColumns.PACKAGE_NAME
+ " = ? ";
final String[] whereArgs =
new String[] {
String.valueOf(type), String.valueOf(uid), String.valueOf(packageName)
};
return db.delete(TABLE_ACTION, where, whereArgs) != 0;
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2024 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.batterytip;
import android.app.AppOpsManager;
import android.content.Context;
import android.os.UserManager;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.utils.StringUtil;
/** Preference controller to control the battery manager */
public class BatteryManagerPreferenceController extends BasePreferenceController {
private static final String KEY_BATTERY_MANAGER = "smart_battery_manager";
private PowerUsageFeatureProvider mPowerUsageFeatureProvider;
private AppOpsManager mAppOpsManager;
private UserManager mUserManager;
private boolean mEnableAppBatteryUsagePage;
public BatteryManagerPreferenceController(Context context) {
super(context, KEY_BATTERY_MANAGER);
mPowerUsageFeatureProvider = FeatureFactory.getFeatureFactory()
.getPowerUsageFeatureProvider();
mAppOpsManager = context.getSystemService(AppOpsManager.class);
mUserManager = context.getSystemService(UserManager.class);
mEnableAppBatteryUsagePage =
mContext.getResources().getBoolean(R.bool.config_app_battery_usage_list_enabled);
}
@Override
public int getAvailabilityStatus() {
if (!mPowerUsageFeatureProvider.isBatteryManagerSupported()) {
return UNSUPPORTED_ON_DEVICE;
}
if (!mContext.getResources().getBoolean(R.bool.config_battery_manager_consider_ac)) {
return AVAILABLE_UNSEARCHABLE;
}
return mPowerUsageFeatureProvider.isAdaptiveChargingSupported()
? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE;
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
if (!mEnableAppBatteryUsagePage) {
final int num = BatteryTipUtils.getRestrictedAppsList(mAppOpsManager,
mUserManager).size();
updateSummary(preference, num);
}
}
@VisibleForTesting
void updateSummary(Preference preference, int num) {
if (num > 0) {
preference.setSummary(StringUtil.getIcuPluralsString(mContext, num,
R.string.battery_manager_app_restricted));
} else {
preference.setSummary(
mPowerUsageFeatureProvider.isAdaptiveChargingSupported()
? R.string.battery_manager_summary
: R.string.battery_manager_summary_unsupported);
}
}
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright (C) 2018 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.batterytip;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.Utils;
import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController.BatteryTipListener;
import com.android.settings.fuelgauge.batterytip.actions.BatteryTipAction;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import com.android.settings.fuelgauge.batterytip.tips.HighUsageTip;
import com.android.settings.fuelgauge.batterytip.tips.RestrictAppTip;
import com.android.settings.fuelgauge.batterytip.tips.UnrestrictAppTip;
import com.android.settingslib.utils.StringUtil;
import java.util.List;
/** Dialog Fragment to show action dialog for each anomaly */
public class BatteryTipDialogFragment extends InstrumentedDialogFragment
implements DialogInterface.OnClickListener {
private static final String ARG_BATTERY_TIP = "battery_tip";
private static final String ARG_METRICS_KEY = "metrics_key";
@VisibleForTesting BatteryTip mBatteryTip;
@VisibleForTesting int mMetricsKey;
public static BatteryTipDialogFragment newInstance(BatteryTip batteryTip, int metricsKey) {
BatteryTipDialogFragment dialogFragment = new BatteryTipDialogFragment();
Bundle args = new Bundle(1);
args.putParcelable(ARG_BATTERY_TIP, batteryTip);
args.putInt(ARG_METRICS_KEY, metricsKey);
dialogFragment.setArguments(args);
return dialogFragment;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Bundle bundle = getArguments();
final Context context = getContext();
mBatteryTip = bundle.getParcelable(ARG_BATTERY_TIP);
mMetricsKey = bundle.getInt(ARG_METRICS_KEY);
switch (mBatteryTip.getType()) {
case BatteryTip.TipType.SUMMARY:
return new AlertDialog.Builder(context)
.setMessage(R.string.battery_tip_dialog_summary_message)
.setPositiveButton(android.R.string.ok, null)
.create();
case BatteryTip.TipType.HIGH_DEVICE_USAGE:
final HighUsageTip highUsageTip = (HighUsageTip) mBatteryTip;
final RecyclerView view =
(RecyclerView)
LayoutInflater.from(context).inflate(R.layout.recycler_view, null);
view.setLayoutManager(new LinearLayoutManager(context));
view.setAdapter(new HighUsageAdapter(context, highUsageTip.getHighUsageAppList()));
return new AlertDialog.Builder(context)
.setMessage(
getString(
R.string.battery_tip_dialog_message,
highUsageTip.getHighUsageAppList().size()))
.setView(view)
.setPositiveButton(android.R.string.ok, null)
.create();
case BatteryTip.TipType.APP_RESTRICTION:
final RestrictAppTip restrictAppTip = (RestrictAppTip) mBatteryTip;
final List<AppInfo> restrictedAppList = restrictAppTip.getRestrictAppList();
final int num = restrictedAppList.size();
final CharSequence appLabel =
Utils.getApplicationLabel(context, restrictedAppList.get(0).packageName);
final AlertDialog.Builder builder =
new AlertDialog.Builder(context)
.setTitle(
StringUtil.getIcuPluralsString(
context,
num,
R.string.battery_tip_restrict_app_dialog_title))
.setPositiveButton(
R.string.battery_tip_restrict_app_dialog_ok, this)
.setNegativeButton(android.R.string.cancel, null);
if (num == 1) {
builder.setMessage(
getString(R.string.battery_tip_restrict_app_dialog_message, appLabel));
} else if (num <= 5) {
builder.setMessage(
getString(
R.string.battery_tip_restrict_apps_less_than_5_dialog_message));
final RecyclerView restrictionView =
(RecyclerView)
LayoutInflater.from(context)
.inflate(R.layout.recycler_view, null);
restrictionView.setLayoutManager(new LinearLayoutManager(context));
restrictionView.setAdapter(new HighUsageAdapter(context, restrictedAppList));
builder.setView(restrictionView);
} else {
builder.setMessage(
context.getString(
R.string.battery_tip_restrict_apps_more_than_5_dialog_message,
restrictAppTip.getRestrictAppsString(context)));
}
return builder.create();
case BatteryTip.TipType.REMOVE_APP_RESTRICTION:
final UnrestrictAppTip unrestrictAppTip = (UnrestrictAppTip) mBatteryTip;
final CharSequence name =
Utils.getApplicationLabel(context, unrestrictAppTip.getPackageName());
return new AlertDialog.Builder(context)
.setTitle(getString(R.string.battery_tip_unrestrict_app_dialog_title))
.setMessage(R.string.battery_tip_unrestrict_app_dialog_message)
.setPositiveButton(R.string.battery_tip_unrestrict_app_dialog_ok, this)
.setNegativeButton(R.string.battery_tip_unrestrict_app_dialog_cancel, null)
.create();
default:
throw new IllegalArgumentException("unknown type " + mBatteryTip.getType());
}
}
@Override
public int getMetricsCategory() {
return SettingsEnums.FUELGAUGE_BATTERY_TIP_DIALOG;
}
@Override
public void onClick(DialogInterface dialog, int which) {
final BatteryTipListener lsn = (BatteryTipListener) getTargetFragment();
if (lsn == null) {
return;
}
final BatteryTipAction action =
BatteryTipUtils.getActionForBatteryTip(
mBatteryTip,
(SettingsActivity) getActivity(),
(InstrumentedPreferenceFragment) getTargetFragment());
if (action != null) {
action.handlePositiveAction(mMetricsKey);
}
lsn.onBatteryTipHandled(mBatteryTip);
}
private boolean isPluggedIn() {
final Intent batteryIntent =
getContext()
.registerReceiver(
null /* receiver */,
new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
return batteryIntent != null
&& batteryIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0;
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.batterytip;
import android.content.Context;
import android.os.BatteryUsageStats;
import androidx.annotation.VisibleForTesting;
import com.android.settings.fuelgauge.BatteryInfo;
import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settings.fuelgauge.batterytip.detectors.BatteryDefenderDetector;
import com.android.settings.fuelgauge.batterytip.detectors.HighUsageDetector;
import com.android.settings.fuelgauge.batterytip.detectors.IncompatibleChargerDetector;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.utils.AsyncLoaderCompat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Loader to compute and return a battery tip list. It will always return a full length list even
* though some tips may have state {@code BaseBatteryTip.StateType.INVISIBLE}.
*/
public class BatteryTipLoader extends AsyncLoaderCompat<List<BatteryTip>> {
private static final String TAG = "BatteryTipLoader";
private BatteryUsageStats mBatteryUsageStats;
@VisibleForTesting BatteryUtils mBatteryUtils;
public BatteryTipLoader(Context context, BatteryUsageStats batteryUsageStats) {
super(context);
mBatteryUsageStats = batteryUsageStats;
mBatteryUtils = BatteryUtils.getInstance(context);
}
@Override
public List<BatteryTip> loadInBackground() {
final List<BatteryTip> tips = new ArrayList<>();
final BatteryTipPolicy batteryTipPolicy = new BatteryTipPolicy(getContext());
final BatteryInfo batteryInfo = mBatteryUtils.getBatteryInfo(TAG);
final Context context = getContext().getApplicationContext();
tips.add(
new HighUsageDetector(context, batteryTipPolicy, mBatteryUsageStats, batteryInfo)
.detect());
tips.add(new BatteryDefenderDetector(batteryInfo, context).detect());
tips.add(new IncompatibleChargerDetector(context).detect());
FeatureFactory.getFeatureFactory()
.getBatterySettingsFeatureProvider()
.addBatteryTipDetector(context, tips, batteryInfo, batteryTipPolicy);
Collections.sort(tips);
return tips;
}
@Override
protected void onDiscardResult(List<BatteryTip> result) {}
}

View File

@@ -0,0 +1,252 @@
/*
* 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.batterytip;
import android.content.Context;
import android.provider.Settings;
import android.util.KeyValueListParser;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.time.Duration;
/** Class to store the policy for battery tips, which comes from {@link Settings.Global} */
public class BatteryTipPolicy {
public static final String TAG = "BatteryTipPolicy";
private static final String KEY_BATTERY_TIP_ENABLED = "battery_tip_enabled";
private static final String KEY_SUMMARY_ENABLED = "summary_enabled";
private static final String KEY_BATTERY_SAVER_TIP_ENABLED = "battery_saver_tip_enabled";
private static final String KEY_HIGH_USAGE_ENABLED = "high_usage_enabled";
private static final String KEY_HIGH_USAGE_APP_COUNT = "high_usage_app_count";
private static final String KEY_HIGH_USAGE_PERIOD_MS = "high_usage_period_ms";
private static final String KEY_HIGH_USAGE_BATTERY_DRAINING = "high_usage_battery_draining";
private static final String KEY_APP_RESTRICTION_ENABLED = "app_restriction_enabled";
private static final String KEY_APP_RESTRICTION_ACTIVE_HOUR = "app_restriction_active_hour";
private static final String KEY_REDUCED_BATTERY_ENABLED = "reduced_battery_enabled";
private static final String KEY_REDUCED_BATTERY_PERCENT = "reduced_battery_percent";
private static final String KEY_LOW_BATTERY_ENABLED = "low_battery_enabled";
private static final String KEY_LOW_BATTERY_HOUR = "low_battery_hour";
private static final String KEY_DATA_HISTORY_RETAIN_DAY = "data_history_retain_day";
private static final String KEY_EXCESSIVE_BG_DRAIN_PERCENTAGE = "excessive_bg_drain_percentage";
private static final String KEY_TEST_BATTERY_SAVER_TIP = "test_battery_saver_tip";
private static final String KEY_TEST_HIGH_USAGE_TIP = "test_high_usage_tip";
private static final String KEY_TEST_SMART_BATTERY_TIP = "test_smart_battery_tip";
private static final String KEY_TEST_LOW_BATTERY_TIP = "test_low_battery_tip";
/**
* {@code true} if general battery tip is enabled
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_BATTERY_TIP_ENABLED
*/
public final boolean batteryTipEnabled;
/**
* {@code true} if summary tip is enabled
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_SUMMARY_ENABLED
*/
public final boolean summaryEnabled;
/**
* {@code true} if battery saver tip is enabled
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_BATTERY_SAVER_TIP_ENABLED
*/
public final boolean batterySaverTipEnabled;
/**
* {@code true} if high usage tip is enabled
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_HIGH_USAGE_ENABLED
*/
public final boolean highUsageEnabled;
/**
* The maximum number of apps shown in high usage
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_HIGH_USAGE_APP_COUNT
*/
public final int highUsageAppCount;
/**
* The size of the window(milliseconds) for checking if the device is being heavily used
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_HIGH_USAGE_PERIOD_MS
*/
public final long highUsagePeriodMs;
/**
* The battery draining threshold to detect whether device is heavily used. If battery drains
* more than {@link #highUsageBatteryDraining} in last {@link #highUsagePeriodMs}, treat device
* as heavily used.
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_HIGH_USAGE_BATTERY_DRAINING
*/
public final int highUsageBatteryDraining;
/**
* {@code true} if app restriction tip is enabled
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_APP_RESTRICTION_ENABLED
*/
public final boolean appRestrictionEnabled;
/**
* Period(hour) to show anomaly apps. If it is 24 hours, it means only show anomaly apps
* happened in last 24 hours.
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_APP_RESTRICTION_ACTIVE_HOUR
*/
public final int appRestrictionActiveHour;
/**
* {@code true} if reduced battery tip is enabled
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_REDUCED_BATTERY_ENABLED
*/
public final boolean reducedBatteryEnabled;
/**
* The percentage of reduced battery to trigger the tip(e.g. 50%)
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_REDUCED_BATTERY_PERCENT
*/
public final int reducedBatteryPercent;
/**
* {@code true} if low battery tip is enabled
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_LOW_BATTERY_ENABLED
*/
public final boolean lowBatteryEnabled;
/**
* Remaining battery hour to trigger the tip(e.g. 16 hours)
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_LOW_BATTERY_HOUR
*/
public final int lowBatteryHour;
/**
* TTL day for anomaly data stored in database
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_DATA_HISTORY_RETAIN_DAY
*/
public final int dataHistoryRetainDay;
/**
* Battery drain percentage threshold for excessive background anomaly(i.e. 10%)
*
* <p>This is an additional check for excessive background, to check whether battery drain for
* an app is larger than x%
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_EXCESSIVE_BG_DRAIN_PERCENTAGE
*/
public final int excessiveBgDrainPercentage;
/**
* {@code true} if we want to test battery saver tip.
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_TEST_BATTERY_SAVER_TIP
*/
public final boolean testBatterySaverTip;
/**
* {@code true} if we want to test high usage tip.
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_TEST_HIGH_USAGE_TIP
*/
public final boolean testHighUsageTip;
/**
* {@code true} if we want to test smart battery tip.
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_TEST_SMART_BATTERY_TIP
*/
public final boolean testSmartBatteryTip;
/**
* {@code true} if we want to test low battery tip.
*
* @see Settings.Global#BATTERY_TIP_CONSTANTS
* @see #KEY_TEST_LOW_BATTERY_TIP
*/
public final boolean testLowBatteryTip;
private final KeyValueListParser mParser;
public BatteryTipPolicy(Context context) {
this(context, new KeyValueListParser(','));
}
@VisibleForTesting
BatteryTipPolicy(Context context, KeyValueListParser parser) {
mParser = parser;
final String value =
Settings.Global.getString(
context.getContentResolver(), Settings.Global.BATTERY_TIP_CONSTANTS);
try {
mParser.setString(value);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Bad battery tip constants");
}
batteryTipEnabled = mParser.getBoolean(KEY_BATTERY_TIP_ENABLED, true);
summaryEnabled = mParser.getBoolean(KEY_SUMMARY_ENABLED, false);
batterySaverTipEnabled = mParser.getBoolean(KEY_BATTERY_SAVER_TIP_ENABLED, true);
highUsageEnabled = mParser.getBoolean(KEY_HIGH_USAGE_ENABLED, true);
highUsageAppCount = mParser.getInt(KEY_HIGH_USAGE_APP_COUNT, 3);
highUsagePeriodMs =
mParser.getLong(KEY_HIGH_USAGE_PERIOD_MS, Duration.ofHours(2).toMillis());
highUsageBatteryDraining = mParser.getInt(KEY_HIGH_USAGE_BATTERY_DRAINING, 25);
appRestrictionEnabled = mParser.getBoolean(KEY_APP_RESTRICTION_ENABLED, true);
appRestrictionActiveHour = mParser.getInt(KEY_APP_RESTRICTION_ACTIVE_HOUR, 24);
reducedBatteryEnabled = mParser.getBoolean(KEY_REDUCED_BATTERY_ENABLED, false);
reducedBatteryPercent = mParser.getInt(KEY_REDUCED_BATTERY_PERCENT, 50);
lowBatteryEnabled = mParser.getBoolean(KEY_LOW_BATTERY_ENABLED, true);
lowBatteryHour = mParser.getInt(KEY_LOW_BATTERY_HOUR, 3);
dataHistoryRetainDay = mParser.getInt(KEY_DATA_HISTORY_RETAIN_DAY, 30);
excessiveBgDrainPercentage = mParser.getInt(KEY_EXCESSIVE_BG_DRAIN_PERCENTAGE, 10);
testBatterySaverTip = mParser.getBoolean(KEY_TEST_BATTERY_SAVER_TIP, false);
testHighUsageTip = mParser.getBoolean(KEY_TEST_HIGH_USAGE_TIP, false);
testSmartBatteryTip = mParser.getBoolean(KEY_TEST_SMART_BATTERY_TIP, false);
testLowBatteryTip = mParser.getBoolean(KEY_TEST_LOW_BATTERY_TIP, false);
}
}

View File

@@ -0,0 +1,194 @@
/*
* 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.batterytip;
import android.content.Context;
import android.os.BadParcelableException;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.SettingsActivity;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settings.fuelgauge.batterytip.actions.BatteryTipAction;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.CardPreference;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/** Controller in charge of the battery tip group */
public class BatteryTipPreferenceController extends BasePreferenceController {
public static final String PREF_NAME = "battery_tip";
private static final String TAG = "BatteryTipPreferenceController";
private static final int REQUEST_ANOMALY_ACTION = 0;
private static final String KEY_BATTERY_TIPS = "key_battery_tips";
private BatteryTipListener mBatteryTipListener;
private List<BatteryTip> mBatteryTips;
private Map<String, BatteryTip> mBatteryTipMap;
private SettingsActivity mSettingsActivity;
private MetricsFeatureProvider mMetricsFeatureProvider;
private boolean mNeedUpdate;
@VisibleForTesting CardPreference mCardPreference;
@VisibleForTesting Context mPrefContext;
InstrumentedPreferenceFragment mFragment;
public BatteryTipPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
mBatteryTipMap = new ArrayMap<>();
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
mNeedUpdate = true;
}
public void setActivity(SettingsActivity activity) {
mSettingsActivity = activity;
}
public void setFragment(InstrumentedPreferenceFragment fragment) {
mFragment = fragment;
}
public void setBatteryTipListener(BatteryTipListener lsn) {
mBatteryTipListener = lsn;
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE_UNSEARCHABLE;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPrefContext = screen.getContext();
mCardPreference = screen.findPreference(getPreferenceKey());
// Set preference as invisible since there is no default tips.
mCardPreference.setVisible(false);
}
public void updateBatteryTips(List<BatteryTip> batteryTips) {
if (batteryTips == null) {
return;
}
mBatteryTips = batteryTips;
mCardPreference.setVisible(false);
for (int i = 0, size = batteryTips.size(); i < size; i++) {
final BatteryTip batteryTip = mBatteryTips.get(i);
batteryTip.validateCheck(mContext);
if (batteryTip.getState() != BatteryTip.StateType.INVISIBLE) {
mCardPreference.setVisible(true);
batteryTip.updatePreference(mCardPreference);
mBatteryTipMap.put(mCardPreference.getKey(), batteryTip);
batteryTip.log(mContext, mMetricsFeatureProvider);
mNeedUpdate = batteryTip.needUpdate();
break;
}
}
}
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
final BatteryTip batteryTip = mBatteryTipMap.get(preference.getKey());
if (batteryTip != null) {
if (batteryTip.shouldShowDialog()) {
BatteryTipDialogFragment dialogFragment =
BatteryTipDialogFragment.newInstance(
batteryTip, mFragment.getMetricsCategory());
dialogFragment.setTargetFragment(mFragment, REQUEST_ANOMALY_ACTION);
dialogFragment.show(mFragment.getFragmentManager(), TAG);
} else {
final BatteryTipAction action =
BatteryTipUtils.getActionForBatteryTip(
batteryTip, mSettingsActivity, mFragment);
if (action != null) {
action.handlePositiveAction(mFragment.getMetricsCategory());
}
if (mBatteryTipListener != null) {
mBatteryTipListener.onBatteryTipHandled(batteryTip);
}
}
return true;
}
return super.handlePreferenceTreeClick(preference);
}
public void restoreInstanceState(Bundle bundle) {
if (bundle == null) {
return;
}
try {
List<BatteryTip> batteryTips = bundle.getParcelableArrayList(KEY_BATTERY_TIPS);
updateBatteryTips(batteryTips);
} catch (BadParcelableException e) {
Log.e(TAG, "failed to invoke restoreInstanceState()", e);
}
}
public void saveInstanceState(Bundle bundle) {
if (bundle == null) {
return;
}
try {
bundle.putParcelableList(KEY_BATTERY_TIPS, mBatteryTips);
} catch (BadParcelableException e) {
Log.e(TAG, "failed to invoke saveInstanceState()", e);
}
}
public boolean needUpdate() {
return mNeedUpdate;
}
/**
* @return current battery tips, null if unavailable.
*/
@Nullable
public BatteryTip getCurrentBatteryTip() {
if (mBatteryTips == null) {
return null;
}
Optional<BatteryTip> visibleBatteryTip =
mBatteryTips.stream().filter(BatteryTip::isVisible).findFirst();
return visibleBatteryTip.orElse(null);
}
/** Listener to give the control back to target fragment */
public interface BatteryTipListener {
/**
* This method is invoked once battery tip is handled, then target fragment could do extra
* work.
*
* @param batteryTip that has been handled
*/
void onBatteryTipHandled(BatteryTip batteryTip);
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright (C) 2018 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.batterytip;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.Intent;
import android.os.UserHandle;
import android.os.UserManager;
import androidx.annotation.NonNull;
import com.android.internal.util.CollectionUtils;
import com.android.settings.SettingsActivity;
import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settings.fuelgauge.batterytip.actions.BatteryTipAction;
import com.android.settings.fuelgauge.batterytip.actions.OpenBatterySaverAction;
import com.android.settings.fuelgauge.batterytip.actions.OpenRestrictAppFragmentAction;
import com.android.settings.fuelgauge.batterytip.actions.RestrictAppAction;
import com.android.settings.fuelgauge.batterytip.actions.SmartBatteryAction;
import com.android.settings.fuelgauge.batterytip.actions.UnrestrictAppAction;
import com.android.settings.fuelgauge.batterytip.tips.AppLabelPredicate;
import com.android.settings.fuelgauge.batterytip.tips.AppRestrictionPredicate;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import com.android.settings.fuelgauge.batterytip.tips.RestrictAppTip;
import com.android.settings.fuelgauge.batterytip.tips.UnrestrictAppTip;
import java.util.ArrayList;
import java.util.List;
/** Utility class for {@link BatteryTip} */
public class BatteryTipUtils {
private static final int REQUEST_CODE = 0;
/** Get a list of restricted apps with {@link AppOpsManager#OP_RUN_ANY_IN_BACKGROUND} */
@NonNull
public static List<AppInfo> getRestrictedAppsList(
AppOpsManager appOpsManager, UserManager userManager) {
final List<UserHandle> userHandles = userManager.getUserProfiles();
final List<AppOpsManager.PackageOps> packageOpsList =
appOpsManager.getPackagesForOps(new int[] {AppOpsManager.OP_RUN_ANY_IN_BACKGROUND});
final List<AppInfo> appInfos = new ArrayList<>();
for (int i = 0, size = CollectionUtils.size(packageOpsList); i < size; i++) {
final AppOpsManager.PackageOps packageOps = packageOpsList.get(i);
final List<AppOpsManager.OpEntry> entries = packageOps.getOps();
for (int j = 0, entriesSize = entries.size(); j < entriesSize; j++) {
AppOpsManager.OpEntry entry = entries.get(j);
if (entry.getOp() != AppOpsManager.OP_RUN_ANY_IN_BACKGROUND) {
continue;
}
if (entry.getMode() != AppOpsManager.MODE_ALLOWED
&& userHandles.contains(
new UserHandle(UserHandle.getUserId(packageOps.getUid())))) {
appInfos.add(
new AppInfo.Builder()
.setPackageName(packageOps.getPackageName())
.setUid(packageOps.getUid())
.build());
}
}
}
return appInfos;
}
/**
* Get a corresponding action based on {@code batteryTip}
*
* @param batteryTip used to detect which action to choose
* @param settingsActivity used to populate {@link BatteryTipAction}
* @param fragment used to populate {@link BatteryTipAction}
* @return an action for {@code batteryTip}
*/
public static BatteryTipAction getActionForBatteryTip(
BatteryTip batteryTip,
SettingsActivity settingsActivity,
InstrumentedPreferenceFragment fragment) {
switch (batteryTip.getType()) {
case BatteryTip.TipType.SMART_BATTERY_MANAGER:
return new SmartBatteryAction(settingsActivity, fragment);
case BatteryTip.TipType.BATTERY_SAVER:
case BatteryTip.TipType.LOW_BATTERY:
return new OpenBatterySaverAction(settingsActivity);
case BatteryTip.TipType.APP_RESTRICTION:
if (batteryTip.getState() == BatteryTip.StateType.HANDLED) {
return new OpenRestrictAppFragmentAction(fragment, (RestrictAppTip) batteryTip);
} else {
return new RestrictAppAction(settingsActivity, (RestrictAppTip) batteryTip);
}
case BatteryTip.TipType.REMOVE_APP_RESTRICTION:
return new UnrestrictAppAction(settingsActivity, (UnrestrictAppTip) batteryTip);
default:
return null;
}
}
/** Detect and return anomaly apps after {@code timeAfterMs} */
public static List<AppInfo> detectAnomalies(Context context, long timeAfterMs) {
return new ArrayList<>();
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2018 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.batterytip;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.util.IconDrawableFactory;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.android.settings.R;
import com.android.settings.Utils;
import java.util.List;
/** Adapter for the high usage app list */
public class HighUsageAdapter extends RecyclerView.Adapter<HighUsageAdapter.ViewHolder> {
private final Context mContext;
private final IconDrawableFactory mIconDrawableFactory;
private final PackageManager mPackageManager;
private final List<AppInfo> mHighUsageAppList;
public static class ViewHolder extends RecyclerView.ViewHolder {
public View view;
public ImageView appIcon;
public TextView appName;
public TextView appTime;
public ViewHolder(View v) {
super(v);
view = v;
appIcon = v.findViewById(R.id.app_icon);
appName = v.findViewById(R.id.app_name);
appTime = v.findViewById(R.id.app_screen_time);
}
}
public HighUsageAdapter(Context context, List<AppInfo> highUsageAppList) {
mContext = context;
mHighUsageAppList = highUsageAppList;
mIconDrawableFactory = IconDrawableFactory.newInstance(context);
mPackageManager = context.getPackageManager();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final View view =
LayoutInflater.from(mContext).inflate(R.layout.app_high_usage_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
final AppInfo app = mHighUsageAppList.get(position);
holder.appIcon.setImageDrawable(
Utils.getBadgedIcon(
mIconDrawableFactory,
mPackageManager,
app.packageName,
UserHandle.getUserId(app.uid)));
CharSequence label = Utils.getApplicationLabel(mContext, app.packageName);
if (label == null) {
label = app.packageName;
}
holder.appName.setText(label);
}
@Override
public int getItemCount() {
return mHighUsageAppList.size();
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C) 2018 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.batterytip;
import android.os.BatteryStats;
import com.android.settings.fuelgauge.BatteryInfo;
/** DataParser used to go through battery data and detect whether battery is heavily used. */
public class HighUsageDataParser implements BatteryInfo.BatteryDataParser {
/** Time period to check the battery usage */
private final long mTimePeriodMs;
/**
* Treat device as heavily used if battery usage is more than {@code threshold}. 1 means 1%
* battery usage.
*/
private int mThreshold;
private long mEndTimeMs;
private byte mEndBatteryLevel;
private byte mLastPeriodBatteryLevel;
private int mBatteryDrain;
public HighUsageDataParser(long timePeriodMs, int threshold) {
mTimePeriodMs = timePeriodMs;
mThreshold = threshold;
}
@Override
public void onParsingStarted(long startTime, long endTime) {
mEndTimeMs = endTime;
}
@Override
public void onDataPoint(long time, BatteryStats.HistoryItem record) {
if (time == 0 || record.currentTime <= mEndTimeMs - mTimePeriodMs) {
// Since onDataPoint is invoked sorted by time, so we could use this way to get the
// closet battery level 'mTimePeriodMs' time ago.
mLastPeriodBatteryLevel = record.batteryLevel;
}
mEndBatteryLevel = record.batteryLevel;
}
@Override
public void onDataGap() {
// do nothing
}
@Override
public void onParsingDone() {
mBatteryDrain = mLastPeriodBatteryLevel - mEndBatteryLevel;
}
/** Return {@code true} if the battery drain in {@link #mTimePeriodMs} is too much */
public boolean isDeviceHeavilyUsed() {
return mBatteryDrain > mThreshold;
}
}

View File

@@ -0,0 +1,217 @@
/*
* Copyright (C) 2018 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.batterytip;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** This class provides all the configs needed if we want to use {@link android.app.StatsManager} */
public class StatsManagerConfig {
/**
* The key that represents the anomaly config. This value is used in {@link
* android.app.StatsManager#addConfig(long, byte[])}
*/
public static final long ANOMALY_CONFIG_KEY = 1;
/** The key that represents subscriber, which is settings app. */
public static final long SUBSCRIBER_ID = 1;
@Retention(RetentionPolicy.SOURCE)
@IntDef({
AnomalyType.NULL,
AnomalyType.UNKNOWN_REASON,
AnomalyType.EXCESSIVE_WAKELOCK_ALL_SCREEN_OFF,
AnomalyType.EXCESSIVE_WAKEUPS_IN_BACKGROUND,
AnomalyType.EXCESSIVE_UNOPTIMIZED_BLE_SCAN,
AnomalyType.EXCESSIVE_BACKGROUND_SERVICE,
AnomalyType.EXCESSIVE_WIFI_SCAN,
AnomalyType.EXCESSIVE_FLASH_WRITES,
AnomalyType.EXCESSIVE_MEMORY_IN_BACKGROUND,
AnomalyType.EXCESSIVE_DAVEY_RATE,
AnomalyType.EXCESSIVE_JANKY_FRAMES,
AnomalyType.SLOW_COLD_START_TIME,
AnomalyType.SLOW_HOT_START_TIME,
AnomalyType.SLOW_WARM_START_TIME,
AnomalyType.EXCESSIVE_BACKGROUND_SYNCS,
AnomalyType.EXCESSIVE_GPS_SCANS_IN_BACKGROUND,
AnomalyType.EXCESSIVE_JOB_SCHEDULING,
AnomalyType.EXCESSIVE_MOBILE_NETWORK_IN_BACKGROUND,
AnomalyType.EXCESSIVE_WIFI_LOCK_TIME,
AnomalyType.JOB_TIMED_OUT,
AnomalyType.LONG_UNOPTIMIZED_BLE_SCAN,
AnomalyType.BACKGROUND_ANR,
AnomalyType.BACKGROUND_CRASH_RATE,
AnomalyType.EXCESSIVE_ANR_LOOPING,
AnomalyType.EXCESSIVE_ANRS,
AnomalyType.EXCESSIVE_CRASH_RATE,
AnomalyType.EXCESSIVE_CRASH_LOOPING,
AnomalyType.NUMBER_OF_OPEN_FILES,
AnomalyType.EXCESSIVE_CAMERA_USAGE_IN_BACKGROUND,
AnomalyType.EXCESSIVE_CONTACT_ACCESS,
AnomalyType.EXCESSIVE_AUDIO_IN_BACKGROUND,
AnomalyType.EXCESSIVE_CRASH_ANR_IN_BACKGROUND,
AnomalyType.BATTERY_DRAIN_FROM_UNUSED_APP,
})
public @interface AnomalyType {
/** This represents an error condition in the anomaly detection. */
int NULL = -1;
/** The anomaly type does not match any other defined type. */
int UNKNOWN_REASON = 0;
/**
* The application held a partial (screen off) wake lock for a period of time that exceeded
* the threshold with the screen off when not charging.
*/
int EXCESSIVE_WAKELOCK_ALL_SCREEN_OFF = 1;
/**
* The application exceeded the maximum number of wakeups while in the background when not
* charging.
*/
int EXCESSIVE_WAKEUPS_IN_BACKGROUND = 2;
/** The application did unoptimized Bluetooth scans too frequently when not charging. */
int EXCESSIVE_UNOPTIMIZED_BLE_SCAN = 3;
/**
* The application ran in the background for a period of time that exceeded the threshold.
*/
int EXCESSIVE_BACKGROUND_SERVICE = 4;
/** The application exceeded the maximum number of wifi scans when not charging. */
int EXCESSIVE_WIFI_SCAN = 5;
/** The application exceed the maximum number of flash writes */
int EXCESSIVE_FLASH_WRITES = 6;
/**
* The application used more than the maximum memory, while not spending any time in the
* foreground.
*/
int EXCESSIVE_MEMORY_IN_BACKGROUND = 7;
/**
* The application exceeded the maximum percentage of frames with a render rate of greater
* than 700ms.
*/
int EXCESSIVE_DAVEY_RATE = 8;
/**
* The application exceeded the maximum percentage of frames with a render rate greater than
* 16ms.
*/
int EXCESSIVE_JANKY_FRAMES = 9;
/**
* The application exceeded the maximum cold start time - the app has not been launched
* since last system start, died or was killed.
*/
int SLOW_COLD_START_TIME = 10;
/**
* The application exceeded the maximum hot start time - the app and activity are already in
* memory.
*/
int SLOW_HOT_START_TIME = 11;
/**
* The application exceeded the maximum warm start time - the app was already in memory but
* the activity wasnt created yet or was removed from memory.
*/
int SLOW_WARM_START_TIME = 12;
/** The application exceeded the maximum number of syncs while in the background. */
int EXCESSIVE_BACKGROUND_SYNCS = 13;
/** The application exceeded the maximum number of gps scans while in the background. */
int EXCESSIVE_GPS_SCANS_IN_BACKGROUND = 14;
/** The application scheduled more than the maximum number of jobs while not charging. */
int EXCESSIVE_JOB_SCHEDULING = 15;
/**
* The application exceeded the maximum amount of mobile network traffic while in the
* background.
*/
int EXCESSIVE_MOBILE_NETWORK_IN_BACKGROUND = 16;
/**
* The application held the WiFi lock for more than the maximum amount of time while not
* charging.
*/
int EXCESSIVE_WIFI_LOCK_TIME = 17;
/** The application scheduled a job that ran longer than the maximum amount of time. */
int JOB_TIMED_OUT = 18;
/**
* The application did an unoptimized Bluetooth scan that exceeded the maximum time while in
* the background.
*/
int LONG_UNOPTIMIZED_BLE_SCAN = 19;
/** The application exceeded the maximum ANR rate while in the background. */
int BACKGROUND_ANR = 20;
/** The application exceeded the maximum crash rate while in the background. */
int BACKGROUND_CRASH_RATE = 21;
/** The application exceeded the maximum ANR-looping rate. */
int EXCESSIVE_ANR_LOOPING = 22;
/** The application exceeded the maximum ANR rate. */
int EXCESSIVE_ANRS = 23;
/** The application exceeded the maximum crash rate. */
int EXCESSIVE_CRASH_RATE = 24;
/** The application exceeded the maximum crash-looping rate. */
int EXCESSIVE_CRASH_LOOPING = 25;
/** The application crashed because no more file descriptors were available. */
int NUMBER_OF_OPEN_FILES = 26;
/** The application used an excessive amount of CPU while in a background process state. */
int EXCESSIVE_CPU_USAGE_IN_BACKGROUND = 27;
/**
* The application kept the camera open for an excessive amount of time while in a bckground
* process state.
*/
int EXCESSIVE_CAMERA_USAGE_IN_BACKGROUND = 28;
/** The application has accessed the contacts content provider an excessive amount. */
int EXCESSIVE_CONTACT_ACCESS = 29;
/** The application has played too much audio while in a background process state. */
int EXCESSIVE_AUDIO_IN_BACKGROUND = 30;
/**
* The application has crashed or ANRed too many times while in a background process state.
*/
int EXCESSIVE_CRASH_ANR_IN_BACKGROUND = 31;
/**
* An application which has not been used by the user recently was detected to cause an
* excessive amount of battery drain.
*/
int BATTERY_DRAIN_FROM_UNUSED_APP = 32;
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2018 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.batterytip.actions;
import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_UNKNOWN;
import android.app.settings.SettingsEnums;
import android.content.Context;
import com.android.settingslib.fuelgauge.BatterySaverUtils;
public class BatterySaverAction extends BatteryTipAction {
public BatterySaverAction(Context context) {
super(context);
}
/** Handle the action when user clicks positive button */
@Override
public void handlePositiveAction(int metricsKey) {
BatterySaverUtils.setPowerSaveMode(
mContext, true, /*needFirstTimeWarning*/ true, SAVER_ENABLED_UNKNOWN);
mMetricsFeatureProvider.action(
mContext, SettingsEnums.ACTION_TIP_TURN_ON_BATTERY_SAVER, metricsKey);
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2018 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.batterytip.actions;
import android.content.Context;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
/**
* Abstract class for battery tip action, which is triggered if we need to handle the battery tip
*/
public abstract class BatteryTipAction {
protected Context mContext;
protected MetricsFeatureProvider mMetricsFeatureProvider;
public BatteryTipAction(Context context) {
mContext = context;
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
}
/** Handle the action when user clicks positive button */
public abstract void handlePositiveAction(int metricsKey);
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2018 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.batterytip.actions;
import android.app.settings.SettingsEnums;
import android.content.Context;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.fuelgauge.batterysaver.BatterySaverSettings;
/** Action to open the {@link com.android.settings.fuelgauge.batterysaver.BatterySaverSettings} */
public class OpenBatterySaverAction extends BatteryTipAction {
public OpenBatterySaverAction(Context context) {
super(context);
}
/** Handle the action when user clicks positive button */
@Override
public void handlePositiveAction(int metricsKey) {
mMetricsFeatureProvider.action(
mContext, SettingsEnums.ACTION_TIP_OPEN_BATTERY_SAVER_PAGE, metricsKey);
new SubSettingLauncher(mContext)
.setDestination(BatterySaverSettings.class.getName())
.setSourceMetricsCategory(metricsKey)
.launch();
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2018 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.batterytip.actions;
import android.app.settings.SettingsEnums;
import androidx.annotation.VisibleForTesting;
import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settings.fuelgauge.RestrictedAppDetails;
import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper;
import com.android.settings.fuelgauge.batterytip.AppInfo;
import com.android.settings.fuelgauge.batterytip.BatteryDatabaseManager;
import com.android.settings.fuelgauge.batterytip.tips.RestrictAppTip;
import com.android.settingslib.utils.ThreadUtils;
import java.util.List;
/** Action to open the {@link com.android.settings.fuelgauge.RestrictedAppDetails} */
public class OpenRestrictAppFragmentAction extends BatteryTipAction {
private final RestrictAppTip mRestrictAppTip;
private final InstrumentedPreferenceFragment mFragment;
@VisibleForTesting BatteryDatabaseManager mBatteryDatabaseManager;
public OpenRestrictAppFragmentAction(
InstrumentedPreferenceFragment fragment, RestrictAppTip tip) {
super(fragment.getContext());
mFragment = fragment;
mRestrictAppTip = tip;
mBatteryDatabaseManager = BatteryDatabaseManager.getInstance(mContext);
}
/** Handle the action when user clicks positive button */
@Override
public void handlePositiveAction(int metricsKey) {
mMetricsFeatureProvider.action(
mContext, SettingsEnums.ACTION_TIP_OPEN_APP_RESTRICTION_PAGE, metricsKey);
final List<AppInfo> mAppInfos = mRestrictAppTip.getRestrictAppList();
RestrictedAppDetails.startRestrictedAppDetails(mFragment, mAppInfos);
// Mark all the anomalies as handled, so it won't show up again.
ThreadUtils.postOnBackgroundThread(
() ->
mBatteryDatabaseManager.updateAnomalies(
mAppInfos, AnomalyDatabaseHelper.State.HANDLED));
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright (C) 2018 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.batterytip.actions;
import android.app.AppOpsManager;
import android.app.settings.SettingsEnums;
import android.content.Context;
import androidx.annotation.VisibleForTesting;
import com.android.internal.util.CollectionUtils;
import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settings.fuelgauge.batterytip.AnomalyDatabaseHelper;
import com.android.settings.fuelgauge.batterytip.AppInfo;
import com.android.settings.fuelgauge.batterytip.BatteryDatabaseManager;
import com.android.settings.fuelgauge.batterytip.tips.RestrictAppTip;
import java.util.List;
/** Action to restrict the apps, then app is not allowed to run in the background. */
public class RestrictAppAction extends BatteryTipAction {
private RestrictAppTip mRestrictAppTip;
@VisibleForTesting BatteryDatabaseManager mBatteryDatabaseManager;
@VisibleForTesting BatteryUtils mBatteryUtils;
public RestrictAppAction(Context context, RestrictAppTip tip) {
super(context);
mRestrictAppTip = tip;
mBatteryUtils = BatteryUtils.getInstance(context);
mBatteryDatabaseManager = BatteryDatabaseManager.getInstance(context);
}
/** Handle the action when user clicks positive button */
@Override
public void handlePositiveAction(int metricsKey) {
final List<AppInfo> appInfos = mRestrictAppTip.getRestrictAppList();
for (int i = 0, size = appInfos.size(); i < size; i++) {
final AppInfo appInfo = appInfos.get(i);
final String packageName = appInfo.packageName;
// Force app standby, then app can't run in the background
mBatteryUtils.setForceAppStandby(appInfo.uid, packageName, AppOpsManager.MODE_IGNORED);
if (CollectionUtils.isEmpty(appInfo.anomalyTypes)) {
// Only log context if there is no anomaly type
mMetricsFeatureProvider.action(
SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_TIP_RESTRICT_APP,
metricsKey,
packageName,
0);
} else {
for (int type : appInfo.anomalyTypes) {
mMetricsFeatureProvider.action(
SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_TIP_RESTRICT_APP,
metricsKey,
packageName,
type);
}
}
}
mBatteryDatabaseManager.updateAnomalies(appInfos, AnomalyDatabaseHelper.State.HANDLED);
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright (C) 2018 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.batterytip.actions;
import android.app.settings.SettingsEnums;
import androidx.fragment.app.Fragment;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.fuelgauge.SmartBatterySettings;
import com.android.settingslib.core.instrumentation.Instrumentable;
public class SmartBatteryAction extends BatteryTipAction {
private SettingsActivity mSettingsActivity;
private Fragment mFragment;
public SmartBatteryAction(SettingsActivity settingsActivity, Fragment fragment) {
super(settingsActivity.getApplicationContext());
mSettingsActivity = settingsActivity;
mFragment = fragment;
}
/** Handle the action when user clicks positive button */
@Override
public void handlePositiveAction(int metricsKey) {
mMetricsFeatureProvider.action(
mContext, SettingsEnums.ACTION_TIP_OPEN_SMART_BATTERY, metricsKey);
new SubSettingLauncher(mSettingsActivity)
.setSourceMetricsCategory(
mFragment instanceof Instrumentable
? ((Instrumentable) mFragment).getMetricsCategory()
: Instrumentable.METRICS_CATEGORY_UNKNOWN)
.setDestination(SmartBatterySettings.class.getName())
.setTitleRes(R.string.smart_battery_manager_title)
.launch();
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2018 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.batterytip.actions;
import android.app.AppOpsManager;
import android.app.settings.SettingsEnums;
import android.content.Context;
import androidx.annotation.VisibleForTesting;
import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settings.fuelgauge.batterytip.AppInfo;
import com.android.settings.fuelgauge.batterytip.tips.UnrestrictAppTip;
/** Action to clear the restriction to the app */
public class UnrestrictAppAction extends BatteryTipAction {
private UnrestrictAppTip mUnRestrictAppTip;
@VisibleForTesting BatteryUtils mBatteryUtils;
public UnrestrictAppAction(Context context, UnrestrictAppTip tip) {
super(context);
mUnRestrictAppTip = tip;
mBatteryUtils = BatteryUtils.getInstance(context);
}
/** Handle the action when user clicks positive button */
@Override
public void handlePositiveAction(int metricsKey) {
final AppInfo appInfo = mUnRestrictAppTip.getUnrestrictAppInfo();
// Clear force app standby, then app can run in the background
mBatteryUtils.setForceAppStandby(
appInfo.uid, appInfo.packageName, AppOpsManager.MODE_ALLOWED);
mMetricsFeatureProvider.action(
SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_TIP_UNRESTRICT_APP,
metricsKey,
appInfo.packageName,
0);
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2020 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.batterytip.detectors;
import android.content.Context;
import com.android.settings.fuelgauge.BatteryInfo;
import com.android.settings.fuelgauge.batterytip.tips.BatteryDefenderTip;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import com.android.settings.overlay.FeatureFactory;
/** Detect whether the battery is overheated */
public class BatteryDefenderDetector implements BatteryTipDetector {
private final BatteryInfo mBatteryInfo;
private final Context mContext;
public BatteryDefenderDetector(BatteryInfo batteryInfo, Context context) {
mBatteryInfo = batteryInfo;
mContext = context;
}
@Override
public BatteryTip detect() {
final boolean isBasicBatteryDefend =
mBatteryInfo.isBatteryDefender
&& !FeatureFactory.getFeatureFactory()
.getPowerUsageFeatureProvider()
.isExtraDefend();
final int state =
isBasicBatteryDefend ? BatteryTip.StateType.NEW : BatteryTip.StateType.INVISIBLE;
final boolean isPluggedIn = mBatteryInfo.pluggedStatus != 0;
return new BatteryDefenderTip(state, isPluggedIn);
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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.batterytip.detectors;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
public interface BatteryTipDetector {
/**
* Detect and update the status of {@link BatteryTip}
*
* @return a not null {@link BatteryTip}
*/
BatteryTip detect();
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright (C) 2018 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.batterytip.detectors;
import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME;
import android.content.Context;
import android.os.BatteryUsageStats;
import android.os.UidBatteryConsumer;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.settings.fuelgauge.BatteryInfo;
import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settings.fuelgauge.batterytip.AppInfo;
import com.android.settings.fuelgauge.batterytip.BatteryTipPolicy;
import com.android.settings.fuelgauge.batterytip.HighUsageDataParser;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import com.android.settings.fuelgauge.batterytip.tips.HighUsageTip;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Detector whether to show summary tip. This detector should be executed as the last {@link
* BatteryTipDetector} since it need the most up-to-date {@code visibleTips}
*/
public class HighUsageDetector implements BatteryTipDetector {
private static final String TAG = "HighUsageDetector";
private BatteryTipPolicy mPolicy;
private BatteryUsageStats mBatteryUsageStats;
private final BatteryInfo mBatteryInfo;
private List<AppInfo> mHighUsageAppList;
@VisibleForTesting HighUsageDataParser mDataParser;
@VisibleForTesting BatteryUtils mBatteryUtils;
@VisibleForTesting boolean mDischarging;
public HighUsageDetector(
Context context,
BatteryTipPolicy policy,
BatteryUsageStats batteryUsageStats,
BatteryInfo batteryInfo) {
mPolicy = policy;
mBatteryUsageStats = batteryUsageStats;
mBatteryInfo = batteryInfo;
mHighUsageAppList = new ArrayList<>();
mBatteryUtils = BatteryUtils.getInstance(context);
mDataParser =
new HighUsageDataParser(
mPolicy.highUsagePeriodMs, mPolicy.highUsageBatteryDraining);
mDischarging = batteryInfo.discharging;
}
@Override
public BatteryTip detect() {
final long lastFullChargeTimeMs =
mBatteryUtils.calculateLastFullChargeTime(
mBatteryUsageStats, System.currentTimeMillis());
if (mPolicy.highUsageEnabled && mDischarging) {
parseBatteryData();
if (mDataParser.isDeviceHeavilyUsed() || mPolicy.testHighUsageTip) {
final double totalPower = mBatteryUsageStats.getConsumedPower();
final int dischargeAmount = mBatteryUsageStats.getDischargePercentage();
final List<UidBatteryConsumer> uidBatteryConsumers =
mBatteryUsageStats.getUidBatteryConsumers();
// Sort by descending power
uidBatteryConsumers.sort(
(consumer1, consumer2) ->
Double.compare(
consumer2.getConsumedPower(),
consumer1.getConsumedPower()));
for (UidBatteryConsumer consumer : uidBatteryConsumers) {
final double percent =
mBatteryUtils.calculateBatteryPercent(
consumer.getConsumedPower(), totalPower, dischargeAmount);
if ((percent + 0.5f < 1f)
|| mBatteryUtils.shouldHideUidBatteryConsumer(consumer)) {
// Don't show it if we should hide or usage percentage is lower than 1%
continue;
}
mHighUsageAppList.add(
new AppInfo.Builder()
.setUid(consumer.getUid())
.setPackageName(mBatteryUtils.getPackageName(consumer.getUid()))
.build());
if (mHighUsageAppList.size() >= mPolicy.highUsageAppCount) {
break;
}
}
// When in test mode, add an app if necessary
if (mPolicy.testHighUsageTip && mHighUsageAppList.isEmpty()) {
mHighUsageAppList.add(
new AppInfo.Builder()
.setPackageName(SETTINGS_PACKAGE_NAME)
.setScreenOnTimeMs(TimeUnit.HOURS.toMillis(3))
.build());
}
}
}
return new HighUsageTip(lastFullChargeTimeMs, mHighUsageAppList);
}
@VisibleForTesting
void parseBatteryData() {
try {
mBatteryInfo.parseBatteryHistory(mDataParser);
} catch (IllegalStateException e) {
Log.e(TAG, "parseBatteryData() failed", e);
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (C) 2023 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.batterytip.detectors;
import android.content.Context;
import android.util.Log;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import com.android.settings.fuelgauge.batterytip.tips.IncompatibleChargerTip;
import com.android.settingslib.Utils;
/** Detect whether it is in the incompatible charging state */
public final class IncompatibleChargerDetector implements BatteryTipDetector {
private static final String TAG = "IncompatibleChargerDetector";
private final Context mContext;
public IncompatibleChargerDetector(Context context) {
mContext = context;
}
@Override
public BatteryTip detect() {
final boolean isIncompatibleCharging = Utils.containsIncompatibleChargers(mContext, TAG);
final int state =
isIncompatibleCharging ? BatteryTip.StateType.NEW : BatteryTip.StateType.INVISIBLE;
Log.d(
TAG,
"detect() state= " + state + " isIncompatibleCharging: " + isIncompatibleCharging);
return new IncompatibleChargerTip(state);
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.batterytip.detectors;
import android.content.Context;
import android.os.PowerManager;
import com.android.settings.fuelgauge.BatteryInfo;
import com.android.settings.fuelgauge.batterytip.BatteryTipPolicy;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import com.android.settings.fuelgauge.batterytip.tips.LowBatteryTip;
/** Detect whether the battery is too low */
public class LowBatteryDetector implements BatteryTipDetector {
private final BatteryInfo mBatteryInfo;
private final BatteryTipPolicy mBatteryTipPolicy;
private final boolean mIsPowerSaveMode;
private final int mWarningLevel;
public LowBatteryDetector(
Context context, BatteryTipPolicy batteryTipPolicy, BatteryInfo batteryInfo) {
mBatteryTipPolicy = batteryTipPolicy;
mBatteryInfo = batteryInfo;
mWarningLevel =
context.getResources()
.getInteger(com.android.internal.R.integer.config_lowBatteryWarningLevel);
mIsPowerSaveMode = context.getSystemService(PowerManager.class).isPowerSaveMode();
}
@Override
public BatteryTip detect() {
final boolean lowBattery = mBatteryInfo.batteryLevel <= mWarningLevel;
final boolean lowBatteryEnabled = mBatteryTipPolicy.lowBatteryEnabled && !mIsPowerSaveMode;
final boolean dischargingLowBatteryState =
mBatteryTipPolicy.testLowBatteryTip || (mBatteryInfo.discharging && lowBattery);
// Show it as new if in test or in discharging low battery state,
// dismiss it if battery saver is on or disabled by config.
final int state =
lowBatteryEnabled && dischargingLowBatteryState
? BatteryTip.StateType.NEW
: BatteryTip.StateType.INVISIBLE;
return new LowBatteryTip(state, mIsPowerSaveMode);
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright (C) 2018 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.batterytip.detectors;
import android.content.ContentResolver;
import android.content.Context;
import android.provider.Settings;
import com.android.settings.fuelgauge.BatteryInfo;
import com.android.settings.fuelgauge.batterytip.BatteryTipPolicy;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import com.android.settings.fuelgauge.batterytip.tips.SmartBatteryTip;
/** Detect whether to show smart battery tip. */
public class SmartBatteryDetector implements BatteryTipDetector {
private static final int EXPECTED_BATTERY_LEVEL = 30;
private final BatteryInfo mBatteryInfo;
private final BatteryTipPolicy mPolicy;
private final ContentResolver mContentResolver;
private final boolean mIsPowerSaveMode;
public SmartBatteryDetector(
Context context,
BatteryTipPolicy policy,
BatteryInfo batteryInfo,
ContentResolver contentResolver,
boolean isPowerSaveMode) {
mPolicy = policy;
mBatteryInfo = batteryInfo;
mContentResolver = contentResolver;
mIsPowerSaveMode = isPowerSaveMode;
}
@Override
public BatteryTip detect() {
final boolean smartBatteryOff =
Settings.Global.getInt(
mContentResolver,
Settings.Global.ADAPTIVE_BATTERY_MANAGEMENT_ENABLED,
1)
== 0;
final boolean isUnderExpectedBatteryLevel =
mBatteryInfo.batteryLevel <= EXPECTED_BATTERY_LEVEL;
// Show it if in test or smart battery is off.
final boolean enableSmartBatteryTip =
smartBatteryOff && !mIsPowerSaveMode && isUnderExpectedBatteryLevel
|| mPolicy.testSmartBatteryTip;
final int state =
enableSmartBatteryTip ? BatteryTip.StateType.NEW : BatteryTip.StateType.INVISIBLE;
return new SmartBatteryTip(state);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2018 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.batterytip.tips;
import android.content.Context;
import com.android.settings.Utils;
import com.android.settings.fuelgauge.batterytip.AppInfo;
import java.util.function.Predicate;
/** {@link Predicate} for {@link AppInfo} to check whether it has label */
public class AppLabelPredicate implements Predicate<AppInfo> {
private static AppLabelPredicate sInstance;
private Context mContext;
public static AppLabelPredicate getInstance(Context context) {
if (sInstance == null) {
sInstance = new AppLabelPredicate(context.getApplicationContext());
}
return sInstance;
}
private AppLabelPredicate(Context context) {
mContext = context;
}
@Override
public boolean test(AppInfo appInfo) {
// Return true if app doesn't have label
return Utils.getApplicationLabel(mContext, appInfo.packageName) == null;
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2018 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.batterytip.tips;
import android.app.AppOpsManager;
import android.content.Context;
import com.android.settings.fuelgauge.batterytip.AppInfo;
import java.util.function.Predicate;
/** {@link Predicate} for {@link AppInfo} to check whether it is restricted. */
public class AppRestrictionPredicate implements Predicate<AppInfo> {
private static AppRestrictionPredicate sInstance;
private AppOpsManager mAppOpsManager;
public static AppRestrictionPredicate getInstance(Context context) {
if (sInstance == null) {
sInstance = new AppRestrictionPredicate(context.getApplicationContext());
}
return sInstance;
}
private AppRestrictionPredicate(Context context) {
mAppOpsManager = context.getSystemService(AppOpsManager.class);
}
@Override
public boolean test(AppInfo appInfo) {
// Return true if app already been restricted
return mAppOpsManager.checkOpNoThrow(
AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, appInfo.uid, appInfo.packageName)
== AppOpsManager.MODE_IGNORED;
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright (C) 2020 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.batterytip.tips;
import android.app.Activity;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.os.Parcel;
import android.util.Log;
import androidx.core.app.ActivityCompat;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.CardPreference;
import com.android.settingslib.HelpUtils;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import kotlin.Unit;
/** Tip to show current battery is overheated */
public class BatteryDefenderTip extends BatteryTip {
private static final String TAG = "BatteryDefenderTip";
private boolean mIsPluggedIn;
public BatteryDefenderTip(@StateType int state, boolean isPluggedIn) {
super(TipType.BATTERY_DEFENDER, state, false /* showDialog */);
mIsPluggedIn = isPluggedIn;
}
private BatteryDefenderTip(Parcel in) {
super(in);
}
@Override
public CharSequence getTitle(Context context) {
return context.getString(R.string.battery_tip_limited_temporarily_title);
}
@Override
public CharSequence getSummary(Context context) {
return context.getString(R.string.battery_tip_limited_temporarily_summary);
}
@Override
public int getIconId() {
return R.drawable.ic_battery_defender_tip_shield;
}
@Override
public void updateState(BatteryTip tip) {
mState = tip.mState;
}
@Override
public void log(Context context, MetricsFeatureProvider metricsFeatureProvider) {
metricsFeatureProvider.action(context, SettingsEnums.ACTION_BATTERY_DEFENDER_TIP, mState);
}
@Override
public void updatePreference(Preference preference) {
super.updatePreference(preference);
final Context context = preference.getContext();
CardPreference cardPreference = castToCardPreferenceSafely(preference);
if (cardPreference == null) {
Log.e(TAG, "cast Preference to CardPreference failed");
return;
}
cardPreference.setSelectable(false);
cardPreference.setIconResId(getIconId());
cardPreference.setPrimaryButtonText(context.getString(R.string.learn_more));
cardPreference.setPrimaryButtonAction(
() -> {
var helpIntent =
HelpUtils.getHelpIntent(
context,
context.getString(R.string.help_url_battery_defender),
/* backupContext= */ "");
ActivityCompat.startActivityForResult(
(Activity) preference.getContext(),
helpIntent,
/* requestCode= */ 0,
/* options= */ null);
return Unit.INSTANCE;
});
cardPreference.setPrimaryButtonVisibility(true);
cardPreference.setPrimaryButtonContentDescription(
context.getString(
R.string.battery_tip_limited_temporarily_sec_button_content_description));
cardPreference.setSecondaryButtonText(
context.getString(R.string.battery_tip_charge_to_full_button));
cardPreference.setSecondaryButtonAction(
() -> {
resumeCharging(context);
preference.setVisible(false);
return Unit.INSTANCE;
});
cardPreference.setSecondaryButtonVisibility(mIsPluggedIn);
cardPreference.buildContent();
}
private void resumeCharging(Context context) {
final Intent intent =
FeatureFactory.getFeatureFactory()
.getPowerUsageFeatureProvider()
.getResumeChargeIntent(false);
if (intent != null) {
context.sendBroadcast(intent);
}
Log.i(TAG, "send resume charging broadcast intent=" + intent);
}
public static final Creator CREATOR =
new Creator() {
public BatteryTip createFromParcel(Parcel in) {
return new BatteryDefenderTip(in);
}
public BatteryTip[] newArray(int size) {
return new BatteryDefenderTip[size];
}
};
}

View File

@@ -0,0 +1,215 @@
/*
* 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.batterytip.tips;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.SparseIntArray;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.settings.widget.CardPreference;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Base model for a battery tip(e.g. suggest user to turn on battery saver)
*
* <p>Each {@link BatteryTip} contains basic data(e.g. title, summary, icon) as well as the
* pre-defined action(e.g. turn on battery saver)
*/
public abstract class BatteryTip implements Comparable<BatteryTip>, Parcelable {
@Retention(RetentionPolicy.SOURCE)
@IntDef({StateType.NEW, StateType.HANDLED, StateType.INVISIBLE})
public @interface StateType {
int NEW = 0;
int HANDLED = 1;
int INVISIBLE = 2;
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({
TipType.SUMMARY,
TipType.BATTERY_SAVER,
TipType.HIGH_DEVICE_USAGE,
TipType.SMART_BATTERY_MANAGER,
TipType.APP_RESTRICTION,
TipType.REDUCED_BATTERY,
TipType.LOW_BATTERY,
TipType.REMOVE_APP_RESTRICTION,
TipType.BATTERY_DEFENDER,
TipType.DOCK_DEFENDER,
TipType.INCOMPATIBLE_CHARGER,
TipType.BATTERY_WARNING,
TipType.WIRELESS_CHARGING_WARNING
})
public @interface TipType {
int SMART_BATTERY_MANAGER = 0;
int APP_RESTRICTION = 1;
int HIGH_DEVICE_USAGE = 2;
int BATTERY_SAVER = 3;
int REDUCED_BATTERY = 4;
int LOW_BATTERY = 5;
int SUMMARY = 6;
int REMOVE_APP_RESTRICTION = 7;
int BATTERY_DEFENDER = 8;
int DOCK_DEFENDER = 9;
int INCOMPATIBLE_CHARGER = 10;
int BATTERY_WARNING = 11;
int WIRELESS_CHARGING_WARNING = 12;
}
@VisibleForTesting static final SparseIntArray TIP_ORDER;
static {
TIP_ORDER = new SparseIntArray();
TIP_ORDER.append(TipType.BATTERY_SAVER, 0);
TIP_ORDER.append(TipType.LOW_BATTERY, 1);
TIP_ORDER.append(TipType.BATTERY_DEFENDER, 2);
TIP_ORDER.append(TipType.DOCK_DEFENDER, 3);
TIP_ORDER.append(TipType.INCOMPATIBLE_CHARGER, 4);
TIP_ORDER.append(TipType.APP_RESTRICTION, 5);
TIP_ORDER.append(TipType.HIGH_DEVICE_USAGE, 6);
TIP_ORDER.append(TipType.SUMMARY, 7);
TIP_ORDER.append(TipType.SMART_BATTERY_MANAGER, 8);
TIP_ORDER.append(TipType.REDUCED_BATTERY, 9);
TIP_ORDER.append(TipType.REMOVE_APP_RESTRICTION, 10);
TIP_ORDER.append(TipType.BATTERY_WARNING, 11);
TIP_ORDER.append(TipType.WIRELESS_CHARGING_WARNING, 12);
}
private static final String KEY_PREFIX = "key_battery_tip";
protected int mState;
protected int mType;
protected boolean mShowDialog;
/** Whether we need to update battery tip when configuration change */
protected boolean mNeedUpdate;
public BatteryTip(Parcel in) {
mType = in.readInt();
mState = in.readInt();
mShowDialog = in.readBoolean();
mNeedUpdate = in.readBoolean();
}
public BatteryTip(int type, int state, boolean showDialog) {
mType = type;
mState = state;
mShowDialog = showDialog;
mNeedUpdate = true;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mType);
dest.writeInt(mState);
dest.writeBoolean(mShowDialog);
dest.writeBoolean(mNeedUpdate);
}
public abstract CharSequence getTitle(Context context);
public abstract CharSequence getSummary(Context context);
/** Gets the drawable resource id for the icon. */
@DrawableRes
public abstract int getIconId();
/**
* Update the current {@link #mState} using the new {@code tip}.
*
* @param tip used to update
*/
public abstract void updateState(BatteryTip tip);
/**
* Check whether data is still make sense. If not, try recover.
*
* @param context used to do validate check
*/
public void validateCheck(Context context) {
// do nothing
}
/** Log the battery tip */
public abstract void log(Context context, MetricsFeatureProvider metricsFeatureProvider);
public void updatePreference(Preference preference) {
final Context context = preference.getContext();
preference.setTitle(getTitle(context));
preference.setSummary(getSummary(context));
preference.setIcon(getIconId());
final CardPreference cardPreference = castToCardPreferenceSafely(preference);
if (cardPreference != null) {
cardPreference.resetLayoutState();
}
}
public boolean shouldShowDialog() {
return mShowDialog;
}
public boolean needUpdate() {
return mNeedUpdate;
}
public String getKey() {
return KEY_PREFIX + mType;
}
public int getType() {
return mType;
}
@StateType
public int getState() {
return mState;
}
public boolean isVisible() {
return mState != StateType.INVISIBLE;
}
@Override
public int compareTo(BatteryTip o) {
return TIP_ORDER.get(mType) - TIP_ORDER.get(o.mType);
}
@Override
public String toString() {
return "type=" + mType + " state=" + mState;
}
public CardPreference castToCardPreferenceSafely(Preference preference) {
return preference instanceof CardPreference ? (CardPreference) preference : null;
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright (C) 2018 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.batterytip.tips;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.fuelgauge.batterytip.AppInfo;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import java.util.List;
/** Tip to show general summary about battery life */
public class HighUsageTip extends BatteryTip {
private final long mLastFullChargeTimeMs;
@VisibleForTesting final List<AppInfo> mHighUsageAppList;
public HighUsageTip(long lastFullChargeTimeMs, List<AppInfo> appList) {
super(
TipType.HIGH_DEVICE_USAGE,
appList.isEmpty() ? StateType.INVISIBLE : StateType.NEW,
true /* showDialog */);
mLastFullChargeTimeMs = lastFullChargeTimeMs;
mHighUsageAppList = appList;
}
@VisibleForTesting
HighUsageTip(Parcel in) {
super(in);
mLastFullChargeTimeMs = in.readLong();
mHighUsageAppList = in.createTypedArrayList(AppInfo.CREATOR);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeLong(mLastFullChargeTimeMs);
dest.writeTypedList(mHighUsageAppList);
}
@Override
public CharSequence getTitle(Context context) {
return context.getString(R.string.battery_tip_high_usage_title);
}
@Override
public CharSequence getSummary(Context context) {
return context.getString(R.string.battery_tip_high_usage_summary);
}
@Override
public int getIconId() {
return R.drawable.ic_perm_device_information_theme;
}
@Override
public void updateState(BatteryTip tip) {
mState = tip.mState;
}
@Override
public void log(Context context, MetricsFeatureProvider metricsFeatureProvider) {
metricsFeatureProvider.action(context, SettingsEnums.ACTION_HIGH_USAGE_TIP, mState);
for (int i = 0, size = mHighUsageAppList.size(); i < size; i++) {
final AppInfo appInfo = mHighUsageAppList.get(i);
metricsFeatureProvider.action(
context, SettingsEnums.ACTION_HIGH_USAGE_TIP_LIST, appInfo.packageName);
}
}
public long getLastFullChargeTimeMs() {
return mLastFullChargeTimeMs;
}
public List<AppInfo> getHighUsageAppList() {
return mHighUsageAppList;
}
@Override
public String toString() {
final StringBuilder stringBuilder = new StringBuilder(super.toString());
stringBuilder.append(" {");
for (int i = 0, size = mHighUsageAppList.size(); i < size; i++) {
final AppInfo appInfo = mHighUsageAppList.get(i);
stringBuilder.append(" " + appInfo.toString() + " ");
}
stringBuilder.append('}');
return stringBuilder.toString();
}
public static final Parcelable.Creator CREATOR =
new Parcelable.Creator() {
public BatteryTip createFromParcel(Parcel in) {
return new HighUsageTip(in);
}
public BatteryTip[] newArray(int size) {
return new HighUsageTip[size];
}
};
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright (C) 2023 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.batterytip.tips;
import android.app.Activity;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Parcel;
import android.util.Log;
import androidx.core.app.ActivityCompat;
import androidx.preference.Preference;
import com.android.settings.R;
import com.android.settings.widget.CardPreference;
import com.android.settingslib.HelpUtils;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import kotlin.Unit;
/** Tip to show incompatible charger state */
public final class IncompatibleChargerTip extends BatteryTip {
private static final String TAG = "IncompatibleChargerTip";
public IncompatibleChargerTip(@StateType int state) {
super(TipType.INCOMPATIBLE_CHARGER, state, /* showDialog */ false);
}
private IncompatibleChargerTip(Parcel in) {
super(in);
}
@Override
public CharSequence getTitle(Context context) {
return context.getString(R.string.battery_tip_incompatible_charging_title);
}
@Override
public CharSequence getSummary(Context context) {
return context.getString(R.string.battery_tip_incompatible_charging_message);
}
@Override
public int getIconId() {
return R.drawable.ic_battery_incompatible_charger;
}
@Override
public void updateState(BatteryTip tip) {
mState = tip.mState;
}
@Override
public void log(Context context, MetricsFeatureProvider metricsFeatureProvider) {
metricsFeatureProvider.action(
context, SettingsEnums.ACTION_INCOMPATIBLE_CHARGING_TIP, mState);
}
@Override
public void updatePreference(Preference preference) {
super.updatePreference(preference);
final Context context = preference.getContext();
final CardPreference cardPreference = castToCardPreferenceSafely(preference);
if (cardPreference == null) {
Log.e(TAG, "cast Preference to CardPreference failed");
return;
}
cardPreference.setSelectable(false);
cardPreference.enableDismiss(false);
cardPreference.setIconResId(getIconId());
cardPreference.setPrimaryButtonText(context.getString(R.string.learn_more));
cardPreference.setPrimaryButtonAction(
() -> {
var helpIntent =
HelpUtils.getHelpIntent(
context,
context.getString(R.string.help_url_incompatible_charging),
/* backupContext */ "");
ActivityCompat.startActivityForResult(
(Activity) context,
helpIntent,
/* requestCode= */ 0,
/* options= */ null);
return Unit.INSTANCE;
});
cardPreference.setPrimaryButtonVisibility(true);
cardPreference.setPrimaryButtonContentDescription(
context.getString(R.string.battery_tip_incompatible_charging_content_description));
cardPreference.buildContent();
}
public static final Creator CREATOR =
new Creator() {
public BatteryTip createFromParcel(Parcel in) {
return new IncompatibleChargerTip(in);
}
public BatteryTip[] newArray(int size) {
return new IncompatibleChargerTip[size];
}
};
}

View File

@@ -0,0 +1,88 @@
/*
* 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.batterytip.tips;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.settings.R;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
/** Tip to show current battery level is low */
public class LowBatteryTip extends BatteryTip {
private boolean mPowerSaveModeOn;
public LowBatteryTip(@StateType int state, boolean powerSaveModeOn) {
super(TipType.LOW_BATTERY, state, false /* showDialog */);
mPowerSaveModeOn = powerSaveModeOn;
}
public LowBatteryTip(Parcel in) {
super(in);
mPowerSaveModeOn = in.readBoolean();
}
@Override
public CharSequence getTitle(Context context) {
return context.getString(R.string.battery_tip_low_battery_title);
}
@Override
public CharSequence getSummary(Context context) {
return context.getString(R.string.battery_tip_low_battery_summary);
}
@Override
public int getIconId() {
return mState = R.drawable.ic_battery_saver_accent_24dp;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeBoolean(mPowerSaveModeOn);
}
@Override
public void log(Context context, MetricsFeatureProvider metricsFeatureProvider) {
metricsFeatureProvider.action(context, SettingsEnums.ACTION_LOW_BATTERY_TIP, mState);
}
@Override
public void updateState(BatteryTip tip) {
final LowBatteryTip lowBatteryTip = (LowBatteryTip) tip;
mState = lowBatteryTip.mPowerSaveModeOn ? StateType.INVISIBLE : lowBatteryTip.getState();
}
boolean isPowerSaveModeOn() {
return mPowerSaveModeOn;
}
public static final Parcelable.Creator CREATOR =
new Parcelable.Creator() {
public BatteryTip createFromParcel(Parcel in) {
return new LowBatteryTip(in);
}
public BatteryTip[] newArray(int size) {
return new LowBatteryTip[size];
}
};
}

View File

@@ -0,0 +1,192 @@
/*
* Copyright (C) 2018 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.batterytip.tips;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.icu.text.ListFormatter;
import android.os.Parcel;
import android.util.ArrayMap;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.fuelgauge.batterytip.AppInfo;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.utils.StringUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/** Tip to suggest user to restrict some bad apps */
public class RestrictAppTip extends BatteryTip {
private List<AppInfo> mRestrictAppList;
public RestrictAppTip(@StateType int state, List<AppInfo> restrictApps) {
super(TipType.APP_RESTRICTION, state, state == StateType.NEW /* showDialog */);
mRestrictAppList = restrictApps;
mNeedUpdate = false;
}
public RestrictAppTip(@StateType int state, AppInfo appInfo) {
super(TipType.APP_RESTRICTION, state, state == StateType.NEW /* showDialog */);
mRestrictAppList = new ArrayList<>();
mRestrictAppList.add(appInfo);
mNeedUpdate = false;
}
@VisibleForTesting
RestrictAppTip(Parcel in) {
super(in);
mRestrictAppList = in.createTypedArrayList(AppInfo.CREATOR);
}
@Override
public CharSequence getTitle(Context context) {
final int num = mRestrictAppList.size();
final CharSequence appLabel =
num > 0
? Utils.getApplicationLabel(context, mRestrictAppList.get(0).packageName)
: "";
Map<String, Object> arguments = new ArrayMap<>();
arguments.put("count", num);
arguments.put("label", appLabel);
return mState == StateType.HANDLED
? StringUtil.getIcuPluralsString(
context, arguments, R.string.battery_tip_restrict_handled_title)
: StringUtil.getIcuPluralsString(
context, arguments, R.string.battery_tip_restrict_title);
}
@Override
public CharSequence getSummary(Context context) {
final int num = mRestrictAppList.size();
final CharSequence appLabel =
num > 0
? Utils.getApplicationLabel(context, mRestrictAppList.get(0).packageName)
: "";
final int resId =
mState == StateType.HANDLED
? R.string.battery_tip_restrict_handled_summary
: R.string.battery_tip_restrict_summary;
Map<String, Object> arguments = new ArrayMap<>();
arguments.put("count", num);
arguments.put("label", appLabel);
return StringUtil.getIcuPluralsString(context, arguments, resId);
}
@Override
public int getIconId() {
return mState == StateType.HANDLED
? R.drawable.ic_perm_device_information_theme
: R.drawable.ic_battery_alert_theme;
}
@Override
public void updateState(BatteryTip tip) {
if (tip.mState == StateType.NEW) {
// Display it if new anomaly comes
mState = StateType.NEW;
mRestrictAppList = ((RestrictAppTip) tip).mRestrictAppList;
mShowDialog = true;
} else if (mState == StateType.NEW && tip.mState == StateType.INVISIBLE) {
// If anomaly becomes invisible, show it as handled
mState = StateType.HANDLED;
mShowDialog = false;
} else {
mState = tip.getState();
mShowDialog = tip.shouldShowDialog();
mRestrictAppList = ((RestrictAppTip) tip).mRestrictAppList;
}
}
@Override
public void validateCheck(Context context) {
super.validateCheck(context);
// Set it invisible if there is no valid app
mRestrictAppList.removeIf(AppLabelPredicate.getInstance(context));
if (mRestrictAppList.isEmpty()) {
mState = StateType.INVISIBLE;
}
}
@Override
public void log(Context context, MetricsFeatureProvider metricsFeatureProvider) {
metricsFeatureProvider.action(context, SettingsEnums.ACTION_APP_RESTRICTION_TIP, mState);
if (mState == StateType.NEW) {
for (int i = 0, size = mRestrictAppList.size(); i < size; i++) {
final AppInfo appInfo = mRestrictAppList.get(i);
for (Integer anomalyType : appInfo.anomalyTypes) {
metricsFeatureProvider.action(
SettingsEnums.PAGE_UNKNOWN,
SettingsEnums.ACTION_APP_RESTRICTION_TIP_LIST,
SettingsEnums.PAGE_UNKNOWN,
appInfo.packageName,
anomalyType);
}
}
}
}
public List<AppInfo> getRestrictAppList() {
return mRestrictAppList;
}
/** Construct the app list string(e.g. app1, app2, and app3) */
public CharSequence getRestrictAppsString(Context context) {
final List<CharSequence> appLabels = new ArrayList<>();
for (int i = 0, size = mRestrictAppList.size(); i < size; i++) {
appLabels.add(Utils.getApplicationLabel(context, mRestrictAppList.get(i).packageName));
}
return ListFormatter.getInstance().format(appLabels);
}
@Override
public String toString() {
final StringBuilder stringBuilder = new StringBuilder(super.toString());
stringBuilder.append(" {");
for (int i = 0, size = mRestrictAppList.size(); i < size; i++) {
final AppInfo appInfo = mRestrictAppList.get(i);
stringBuilder.append(" " + appInfo.toString() + " ");
}
stringBuilder.append('}');
return stringBuilder.toString();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeTypedList(mRestrictAppList);
}
public static final Creator CREATOR =
new Creator() {
public BatteryTip createFromParcel(Parcel in) {
return new RestrictAppTip(in);
}
public BatteryTip[] newArray(int size) {
return new RestrictAppTip[size];
}
};
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2018 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.batterytip.tips;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Parcel;
import com.android.settings.R;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
/** Tip to suggest turn on smart battery if it is not on */
public class SmartBatteryTip extends BatteryTip {
public SmartBatteryTip(@StateType int state) {
super(TipType.SMART_BATTERY_MANAGER, state, false /* showDialog */);
}
private SmartBatteryTip(Parcel in) {
super(in);
}
@Override
public CharSequence getTitle(Context context) {
return context.getString(R.string.battery_tip_smart_battery_title);
}
@Override
public CharSequence getSummary(Context context) {
return context.getString(R.string.battery_tip_smart_battery_summary);
}
@Override
public int getIconId() {
return R.drawable.ic_perm_device_information_theme;
}
@Override
public void updateState(BatteryTip tip) {
mState = tip.mState;
}
@Override
public void log(Context context, MetricsFeatureProvider metricsFeatureProvider) {
metricsFeatureProvider.action(context, SettingsEnums.ACTION_SMART_BATTERY_TIP, mState);
}
public static final Creator CREATOR =
new Creator() {
public BatteryTip createFromParcel(Parcel in) {
return new SmartBatteryTip(in);
}
public BatteryTip[] newArray(int size) {
return new SmartBatteryTip[size];
}
};
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright (C) 2018 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.batterytip.tips;
import android.content.Context;
import android.os.Parcel;
import androidx.annotation.VisibleForTesting;
import com.android.settings.fuelgauge.AdvancedPowerUsageDetail;
import com.android.settings.fuelgauge.batterytip.AppInfo;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
/**
* Tip to suggest user to remove app restriction. This is the empty tip and it is only used in
* {@link AdvancedPowerUsageDetail} to create dialog.
*/
public class UnrestrictAppTip extends BatteryTip {
private AppInfo mAppInfo;
public UnrestrictAppTip(@StateType int state, AppInfo appInfo) {
super(TipType.REMOVE_APP_RESTRICTION, state, true /* showDialog */);
mAppInfo = appInfo;
}
@VisibleForTesting
UnrestrictAppTip(Parcel in) {
super(in);
mAppInfo = in.readParcelable(getClass().getClassLoader());
}
@Override
public CharSequence getTitle(Context context) {
// Don't need title since this is an empty tip
return null;
}
@Override
public CharSequence getSummary(Context context) {
// Don't need summary since this is an empty tip
return null;
}
@Override
public int getIconId() {
return 0;
}
public String getPackageName() {
return mAppInfo.packageName;
}
@Override
public void updateState(BatteryTip tip) {
mState = tip.mState;
}
@Override
public void log(Context context, MetricsFeatureProvider metricsFeatureProvider) {
// Do nothing
}
public AppInfo getUnrestrictAppInfo() {
return mAppInfo;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeParcelable(mAppInfo, flags);
}
public static final Creator CREATOR =
new Creator() {
public BatteryTip createFromParcel(Parcel in) {
return new UnrestrictAppTip(in);
}
public BatteryTip[] newArray(int size) {
return new UnrestrictAppTip[size];
}
};
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2023 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.batteryusage;
import android.content.Context;
import android.text.TextUtils;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
class AnomalyAppItemPreference extends PowerGaugePreference {
private static final String TAG = "AnomalyAppItemPreference";
private CharSequence mAnomalyHintText;
AnomalyAppItemPreference(Context context) {
super(context, /* attrs */ null);
setLayoutResource(R.layout.anomaly_app_item_preference);
}
void setAnomalyHint(CharSequence anomalyHintText) {
if (!TextUtils.equals(mAnomalyHintText, anomalyHintText)) {
mAnomalyHintText = anomalyHintText;
notifyChanged();
}
}
@Override
public void onBindViewHolder(PreferenceViewHolder viewHolder) {
super.onBindViewHolder(viewHolder);
final LinearLayout warningChipView =
(LinearLayout) viewHolder.findViewById(R.id.warning_chip);
if (!TextUtils.isEmpty(mAnomalyHintText)) {
((TextView) warningChipView.findViewById(R.id.warning_info)).setText(mAnomalyHintText);
warningChipView.setVisibility(View.VISIBLE);
} else {
warningChipView.setVisibility(View.GONE);
}
}
}

View File

@@ -0,0 +1,255 @@
/*
* Copyright (C) 2023 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.batteryusage;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Pair;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.core.SubSettingLauncher;
import java.util.function.Function;
final class AnomalyEventWrapper {
private static final String TAG = "AnomalyEventWrapper";
private final Context mContext;
private final PowerAnomalyEvent mPowerAnomalyEvent;
private final int mCardStyleId;
private final int mResourceIndex;
private SubSettingLauncher mSubSettingLauncher = null;
private Pair<Integer, Integer> mHighlightSlotPair = null;
private BatteryDiffEntry mRelatedBatteryDiffEntry = null;
AnomalyEventWrapper(Context context, PowerAnomalyEvent powerAnomalyEvent) {
mContext = context;
mPowerAnomalyEvent = powerAnomalyEvent;
// Set basic battery tips card info
mCardStyleId = mPowerAnomalyEvent.getType().getNumber();
mResourceIndex = mPowerAnomalyEvent.getKey().getNumber();
}
private <T> T getInfo(
Function<WarningBannerInfo, T> warningBannerInfoSupplier,
Function<WarningItemInfo, T> warningItemInfoSupplier) {
if (warningBannerInfoSupplier != null && mPowerAnomalyEvent.hasWarningBannerInfo()) {
return warningBannerInfoSupplier.apply(mPowerAnomalyEvent.getWarningBannerInfo());
} else if (warningItemInfoSupplier != null && mPowerAnomalyEvent.hasWarningItemInfo()) {
return warningItemInfoSupplier.apply(mPowerAnomalyEvent.getWarningItemInfo());
}
return null;
}
private int getResourceId(int resourceId, int resourceIndex, String defType) {
final String key = getStringFromArrayResource(resourceId, resourceIndex);
return TextUtils.isEmpty(key)
? 0
: mContext.getResources().getIdentifier(key, defType, mContext.getPackageName());
}
private String getString(
Function<WarningBannerInfo, String> warningBannerInfoSupplier,
Function<WarningItemInfo, String> warningItemInfoSupplier,
int resourceId,
int resourceIndex) {
final String string = getInfo(warningBannerInfoSupplier, warningItemInfoSupplier);
return (!TextUtils.isEmpty(string) || resourceId <= 0)
? string
: getStringFromArrayResource(resourceId, resourceIndex);
}
private String getStringFromArrayResource(int resourceId, int resourceIndex) {
if (resourceId <= 0 || resourceIndex < 0) {
return null;
}
final String[] stringArray = mContext.getResources().getStringArray(resourceId);
return (resourceIndex >= 0 && resourceIndex < stringArray.length)
? stringArray[resourceIndex]
: null;
}
void setRelatedBatteryDiffEntry(BatteryDiffEntry batteryDiffEntry) {
mRelatedBatteryDiffEntry = batteryDiffEntry;
}
int getAnomalyKeyNumber() {
return mPowerAnomalyEvent.getKey().getNumber();
}
String getEventId() {
return mPowerAnomalyEvent.hasEventId() ? mPowerAnomalyEvent.getEventId() : null;
}
int getIconResId() {
return getResourceId(R.array.battery_tips_card_icons, mCardStyleId, "drawable");
}
int getColorResId() {
return getResourceId(R.array.battery_tips_card_colors, mCardStyleId, "color");
}
String getTitleString() {
final String titleStringFromProto =
getInfo(WarningBannerInfo::getTitleString, WarningItemInfo::getTitleString);
if (!TextUtils.isEmpty(titleStringFromProto)) {
return titleStringFromProto;
}
final int titleFormatResId =
getResourceId(R.array.power_anomaly_title_ids, mResourceIndex, "string");
if (mPowerAnomalyEvent.hasWarningBannerInfo()) {
return mContext.getString(titleFormatResId);
} else if (mPowerAnomalyEvent.hasWarningItemInfo() && mRelatedBatteryDiffEntry != null) {
final String appLabel = mRelatedBatteryDiffEntry.getAppLabel();
return mContext.getString(titleFormatResId, appLabel);
}
return null;
}
String getMainBtnString() {
return getString(
WarningBannerInfo::getMainButtonString,
WarningItemInfo::getMainButtonString,
R.array.power_anomaly_main_btn_strings,
mResourceIndex);
}
String getDismissBtnString() {
return getString(
WarningBannerInfo::getCancelButtonString,
WarningItemInfo::getCancelButtonString,
R.array.power_anomaly_dismiss_btn_strings,
mResourceIndex);
}
String getAnomalyHintString() {
final String anomalyHintStringFromProto =
getInfo(null, WarningItemInfo::getWarningInfoString);
return TextUtils.isEmpty(anomalyHintStringFromProto)
? getStringFromArrayResource(R.array.power_anomaly_hint_messages, mResourceIndex)
: anomalyHintStringFromProto;
}
String getAnomalyHintPrefKey() {
return getInfo(null, WarningItemInfo::getAnomalyHintPrefKey);
}
String getDismissRecordKey() {
return mPowerAnomalyEvent.getDismissRecordKey();
}
boolean hasAnomalyEntryKey() {
return getAnomalyEntryKey() != null;
}
String getAnomalyEntryKey() {
return mPowerAnomalyEvent.hasWarningItemInfo()
&& mPowerAnomalyEvent.getWarningItemInfo().hasItemKey()
? mPowerAnomalyEvent.getWarningItemInfo().getItemKey()
: null;
}
boolean hasSubSettingLauncher() {
if (mSubSettingLauncher == null) {
mSubSettingLauncher = getSubSettingLauncher();
}
return mSubSettingLauncher != null;
}
SubSettingLauncher getSubSettingLauncher() {
if (mSubSettingLauncher != null) {
return mSubSettingLauncher;
}
final String destinationClassName =
getInfo(WarningBannerInfo::getMainButtonDestination, null);
if (!TextUtils.isEmpty(destinationClassName)) {
final Integer sourceMetricsCategory =
getInfo(WarningBannerInfo::getMainButtonSourceMetricsCategory, null);
final String preferenceHighlightKey =
getInfo(WarningBannerInfo::getMainButtonSourceHighlightKey, null);
Bundle arguments = Bundle.EMPTY;
if (!TextUtils.isEmpty(preferenceHighlightKey)) {
arguments = new Bundle(1);
arguments.putString(
SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, preferenceHighlightKey);
}
mSubSettingLauncher =
new SubSettingLauncher(mContext)
.setDestination(destinationClassName)
.setSourceMetricsCategory(sourceMetricsCategory)
.setArguments(arguments);
}
return mSubSettingLauncher;
}
boolean hasHighlightSlotPair(BatteryLevelData batteryLevelData) {
if (mHighlightSlotPair == null) {
mHighlightSlotPair = getHighlightSlotPair(batteryLevelData);
}
return mHighlightSlotPair != null;
}
Pair<Integer, Integer> getHighlightSlotPair(BatteryLevelData batteryLevelData) {
if (mHighlightSlotPair != null) {
return mHighlightSlotPair;
}
if (!mPowerAnomalyEvent.hasWarningItemInfo()) {
return null;
}
final WarningItemInfo warningItemInfo = mPowerAnomalyEvent.getWarningItemInfo();
final Long startTimestamp =
warningItemInfo.hasStartTimestamp() ? warningItemInfo.getStartTimestamp() : null;
final Long endTimestamp =
warningItemInfo.hasEndTimestamp() ? warningItemInfo.getEndTimestamp() : null;
if (startTimestamp != null && endTimestamp != null) {
mHighlightSlotPair =
batteryLevelData.getIndexByTimestamps(startTimestamp, endTimestamp);
if (mHighlightSlotPair.first == BatteryChartViewModel.SELECTED_INDEX_INVALID
|| mHighlightSlotPair.second == BatteryChartViewModel.SELECTED_INDEX_INVALID) {
// Drop invalid mHighlightSlotPair index
mHighlightSlotPair = null;
}
}
return mHighlightSlotPair;
}
boolean updateTipsCardPreference(BatteryTipsCardPreference preference) {
final String titleString = getTitleString();
if (TextUtils.isEmpty(titleString)) {
return false;
}
preference.setTitle(titleString);
preference.setIconResourceId(getIconResId());
preference.setButtonColorResourceId(getColorResId());
preference.setMainButtonLabel(getMainBtnString());
preference.setDismissButtonLabel(getDismissBtnString());
return true;
}
boolean launchSubSetting() {
if (!hasSubSettingLauncher()) {
return false;
}
// Navigate to sub setting page
mSubSettingLauncher.launch();
return true;
}
}

View File

@@ -0,0 +1,688 @@
/*
* Copyright (C) 2022 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.batteryusage;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceScreen;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.Utils;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnCreate;
import com.android.settingslib.core.lifecycle.events.OnDestroy;
import com.android.settingslib.core.lifecycle.events.OnResume;
import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState;
import com.google.common.base.Objects;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Map;
/** Controls the update for chart graph and the list items. */
public class BatteryChartPreferenceController extends AbstractPreferenceController
implements PreferenceControllerMixin,
LifecycleObserver,
OnCreate,
OnDestroy,
OnSaveInstanceState,
OnResume {
private static final String TAG = "BatteryChartPreferenceController";
private static final String PREFERENCE_KEY = "battery_chart";
private static final long FADE_IN_ANIMATION_DURATION = 400L;
private static final long FADE_OUT_ANIMATION_DURATION = 200L;
// Keys for bundle instance to restore configurations.
private static final String KEY_DAILY_CHART_INDEX = "daily_chart_index";
private static final String KEY_HOURLY_CHART_INDEX = "hourly_chart_index";
/** A callback listener for the selected index is updated. */
interface OnSelectedIndexUpdatedListener {
/** The callback function for the selected index is updated. */
void onSelectedIndexUpdated();
}
@VisibleForTesting Context mPrefContext;
@VisibleForTesting TextView mChartSummaryTextView;
@VisibleForTesting BatteryChartView mDailyChartView;
@VisibleForTesting BatteryChartView mHourlyChartView;
@VisibleForTesting int mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
@VisibleForTesting int mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
@VisibleForTesting int mDailyHighlightSlotIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
@VisibleForTesting int mHourlyHighlightSlotIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
private boolean mIs24HourFormat;
private View mBatteryChartViewGroup;
private BatteryChartViewModel mDailyViewModel;
private List<BatteryChartViewModel> mHourlyViewModels;
private OnSelectedIndexUpdatedListener mOnSelectedIndexUpdatedListener;
private final SettingsActivity mActivity;
private final MetricsFeatureProvider mMetricsFeatureProvider;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final AnimatorListenerAdapter mHourlyChartFadeInAdapter =
createHourlyChartAnimatorListenerAdapter(/* visible= */ true);
private final AnimatorListenerAdapter mHourlyChartFadeOutAdapter =
createHourlyChartAnimatorListenerAdapter(/* visible= */ false);
@VisibleForTesting
final DailyChartLabelTextGenerator mDailyChartLabelTextGenerator =
new DailyChartLabelTextGenerator();
@VisibleForTesting
final HourlyChartLabelTextGenerator mHourlyChartLabelTextGenerator =
new HourlyChartLabelTextGenerator();
public BatteryChartPreferenceController(
Context context, Lifecycle lifecycle, SettingsActivity activity) {
super(context);
mActivity = activity;
mIs24HourFormat = DateFormat.is24HourFormat(context);
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
if (lifecycle != null) {
lifecycle.addObserver(this);
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
if (savedInstanceState == null) {
return;
}
mDailyChartIndex = savedInstanceState.getInt(KEY_DAILY_CHART_INDEX, mDailyChartIndex);
mHourlyChartIndex = savedInstanceState.getInt(KEY_HOURLY_CHART_INDEX, mHourlyChartIndex);
Log.d(
TAG,
String.format(
"onCreate() dailyIndex=%d hourlyIndex=%d",
mDailyChartIndex, mHourlyChartIndex));
}
@Override
public void onResume() {
mIs24HourFormat = DateFormat.is24HourFormat(mContext);
mMetricsFeatureProvider.action(mPrefContext, SettingsEnums.OPEN_BATTERY_USAGE);
}
@Override
public void onSaveInstanceState(Bundle savedInstance) {
if (savedInstance == null) {
return;
}
savedInstance.putInt(KEY_DAILY_CHART_INDEX, mDailyChartIndex);
savedInstance.putInt(KEY_HOURLY_CHART_INDEX, mHourlyChartIndex);
Log.d(
TAG,
String.format(
"onSaveInstanceState() dailyIndex=%d hourlyIndex=%d",
mDailyChartIndex, mHourlyChartIndex));
}
@Override
public void onDestroy() {
if (mActivity == null || mActivity.isChangingConfigurations()) {
BatteryDiffEntry.clearCache();
}
mHandler.removeCallbacksAndMessages(/* token= */ null);
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPrefContext = screen.getContext();
}
@Override
public boolean isAvailable() {
return true;
}
@Override
public String getPreferenceKey() {
return PREFERENCE_KEY;
}
int getDailyChartIndex() {
return mDailyChartIndex;
}
int getHourlyChartIndex() {
return mHourlyChartIndex;
}
void setOnSelectedIndexUpdatedListener(OnSelectedIndexUpdatedListener listener) {
mOnSelectedIndexUpdatedListener = listener;
}
void onBatteryLevelDataUpdate(final BatteryLevelData batteryLevelData) {
Log.d(TAG, "onBatteryLevelDataUpdate: " + batteryLevelData);
mMetricsFeatureProvider.action(
mPrefContext,
SettingsEnums.ACTION_BATTERY_HISTORY_LOADED,
getTotalHours(batteryLevelData));
if (batteryLevelData == null) {
mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
mDailyViewModel = null;
mHourlyViewModels = null;
refreshUi();
return;
}
mDailyViewModel =
new BatteryChartViewModel(
batteryLevelData.getDailyBatteryLevels().getLevels(),
batteryLevelData.getDailyBatteryLevels().getTimestamps(),
BatteryChartViewModel.AxisLabelPosition.CENTER_OF_TRAPEZOIDS,
mDailyChartLabelTextGenerator);
mHourlyViewModels = new ArrayList<>();
for (BatteryLevelData.PeriodBatteryLevelData hourlyBatteryLevelsPerDay :
batteryLevelData.getHourlyBatteryLevelsPerDay()) {
mHourlyViewModels.add(
new BatteryChartViewModel(
hourlyBatteryLevelsPerDay.getLevels(),
hourlyBatteryLevelsPerDay.getTimestamps(),
BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS,
mHourlyChartLabelTextGenerator.updateSpecialCaseContext(
batteryLevelData)));
}
refreshUi();
}
boolean isHighlightSlotFocused() {
return (mDailyHighlightSlotIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID
&& mDailyHighlightSlotIndex == mDailyChartIndex
&& mHourlyHighlightSlotIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID
&& mHourlyHighlightSlotIndex == mHourlyChartIndex);
}
void onHighlightSlotIndexUpdate(int dailyHighlightSlotIndex, int hourlyHighlightSlotIndex) {
mDailyHighlightSlotIndex = dailyHighlightSlotIndex;
mHourlyHighlightSlotIndex = hourlyHighlightSlotIndex;
refreshUi();
if (mOnSelectedIndexUpdatedListener != null) {
mOnSelectedIndexUpdatedListener.onSelectedIndexUpdated();
}
}
void selectHighlightSlotIndex() {
if (mDailyHighlightSlotIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID
|| mHourlyHighlightSlotIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID) {
return;
}
if (mDailyHighlightSlotIndex == mDailyChartIndex
&& mHourlyHighlightSlotIndex == mHourlyChartIndex) {
return;
}
mDailyChartIndex = mDailyHighlightSlotIndex;
mHourlyChartIndex = mHourlyHighlightSlotIndex;
Log.d(
TAG,
String.format(
"onDailyChartSelect:%d, onHourlyChartSelect:%d",
mDailyChartIndex, mHourlyChartIndex));
refreshUi();
mHandler.post(
() -> mDailyChartView.announceForAccessibility(getAccessibilityAnnounceMessage()));
if (mOnSelectedIndexUpdatedListener != null) {
mOnSelectedIndexUpdatedListener.onSelectedIndexUpdated();
}
}
void setBatteryChartView(
@NonNull final BatteryChartView dailyChartView,
@NonNull final BatteryChartView hourlyChartView) {
final View parentView = (View) dailyChartView.getParent();
if (parentView != null && parentView.getId() == R.id.battery_chart_group) {
mBatteryChartViewGroup = (View) dailyChartView.getParent();
}
if (mDailyChartView != dailyChartView || mHourlyChartView != hourlyChartView) {
mHandler.post(() -> setBatteryChartViewInner(dailyChartView, hourlyChartView));
animateBatteryChartViewGroup();
}
if (mBatteryChartViewGroup != null) {
final View grandparentView = (View) mBatteryChartViewGroup.getParent();
mChartSummaryTextView =
grandparentView != null
? grandparentView.findViewById(R.id.chart_summary)
: null;
}
}
private void setBatteryChartViewInner(
@NonNull final BatteryChartView dailyChartView,
@NonNull final BatteryChartView hourlyChartView) {
mDailyChartView = dailyChartView;
mDailyChartView.setOnSelectListener(
trapezoidIndex -> {
if (mDailyChartIndex == trapezoidIndex) {
return;
}
Log.d(TAG, "onDailyChartSelect:" + trapezoidIndex);
mDailyChartIndex = trapezoidIndex;
mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
refreshUi();
mHandler.post(
() ->
mDailyChartView.announceForAccessibility(
getAccessibilityAnnounceMessage()));
mMetricsFeatureProvider.action(
mPrefContext,
trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
? SettingsEnums.ACTION_BATTERY_USAGE_DAILY_SHOW_ALL
: SettingsEnums.ACTION_BATTERY_USAGE_DAILY_TIME_SLOT,
mDailyChartIndex);
if (mOnSelectedIndexUpdatedListener != null) {
mOnSelectedIndexUpdatedListener.onSelectedIndexUpdated();
}
});
mHourlyChartView = hourlyChartView;
mHourlyChartView.setOnSelectListener(
trapezoidIndex -> {
if (mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
// This will happen when a daily slot and an hour slot are clicked together.
return;
}
if (mHourlyChartIndex == trapezoidIndex) {
return;
}
Log.d(TAG, "onHourlyChartSelect:" + trapezoidIndex);
mHourlyChartIndex = trapezoidIndex;
refreshUi();
mHandler.post(
() ->
mHourlyChartView.announceForAccessibility(
getAccessibilityAnnounceMessage()));
mMetricsFeatureProvider.action(
mPrefContext,
trapezoidIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
? SettingsEnums.ACTION_BATTERY_USAGE_SHOW_ALL
: SettingsEnums.ACTION_BATTERY_USAGE_TIME_SLOT,
mHourlyChartIndex);
if (mOnSelectedIndexUpdatedListener != null) {
mOnSelectedIndexUpdatedListener.onSelectedIndexUpdated();
}
});
refreshUi();
}
// Show empty hourly chart view only if there is no valid battery usage data.
void showEmptyChart() {
if (mDailyChartView == null || mHourlyChartView == null) {
// Chart views are not initialized.
return;
}
setChartSummaryVisible(true);
mDailyChartView.setVisibility(View.GONE);
mHourlyChartView.setVisibility(View.VISIBLE);
mHourlyChartView.setViewModel(null);
}
@VisibleForTesting
void refreshUi() {
if (mDailyChartView == null || mHourlyChartView == null) {
// Chart views are not initialized.
return;
}
if (mDailyViewModel == null || mHourlyViewModels == null) {
setChartSummaryVisible(false);
mDailyChartView.setVisibility(View.GONE);
mHourlyChartView.setVisibility(View.GONE);
mDailyChartView.setViewModel(null);
mHourlyChartView.setViewModel(null);
return;
}
setChartSummaryVisible(true);
// Gets valid battery level data.
if (isBatteryLevelDataInOneDay()) {
// Only 1 day data, hide the daily chart view.
mDailyChartView.setVisibility(View.GONE);
mDailyChartIndex = 0;
} else {
mDailyChartView.setVisibility(View.VISIBLE);
if (mDailyChartIndex >= mDailyViewModel.size()) {
mDailyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
}
mDailyViewModel.setSelectedIndex(mDailyChartIndex);
mDailyViewModel.setHighlightSlotIndex(mDailyHighlightSlotIndex);
mDailyChartView.setViewModel(mDailyViewModel);
}
if (mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
// Multiple days are selected, hide the hourly chart view.
animateBatteryHourlyChartView(/* visible= */ false);
} else {
animateBatteryHourlyChartView(/* visible= */ true);
final BatteryChartViewModel hourlyViewModel = mHourlyViewModels.get(mDailyChartIndex);
if (mHourlyChartIndex >= hourlyViewModel.size()) {
mHourlyChartIndex = BatteryChartViewModel.SELECTED_INDEX_ALL;
}
hourlyViewModel.setSelectedIndex(mHourlyChartIndex);
hourlyViewModel.setHighlightSlotIndex(
(mDailyChartIndex == mDailyHighlightSlotIndex)
? mHourlyHighlightSlotIndex
: BatteryChartViewModel.SELECTED_INDEX_INVALID);
mHourlyChartView.setViewModel(hourlyViewModel);
}
}
String getSlotInformation() {
if (mDailyViewModel == null || mHourlyViewModels == null) {
// No data
return null;
}
if (isAllSelected()) {
return null;
}
final String selectedDayText = mDailyViewModel.getFullText(mDailyChartIndex);
if (mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
return selectedDayText;
}
final String selectedHourText =
mHourlyViewModels.get(mDailyChartIndex).getFullText(mHourlyChartIndex);
if (isBatteryLevelDataInOneDay()) {
return selectedHourText;
}
return mContext.getString(
R.string.battery_usage_day_and_hour, selectedDayText, selectedHourText);
}
@VisibleForTesting
String getBatteryLevelPercentageInfo() {
if (mDailyViewModel == null || mHourlyViewModels == null) {
// No data
return "";
}
if (mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL
|| mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL) {
return mDailyViewModel.getSlotBatteryLevelText(mDailyChartIndex);
}
return mHourlyViewModels.get(mDailyChartIndex).getSlotBatteryLevelText(mHourlyChartIndex);
}
private String getAccessibilityAnnounceMessage() {
final String slotInformation = getSlotInformation();
final String slotInformationMessage =
slotInformation == null
? mPrefContext.getString(
R.string.battery_usage_breakdown_title_since_last_full_charge)
: mPrefContext.getString(
R.string.battery_usage_breakdown_title_for_slot, slotInformation);
final String batteryLevelPercentageMessage = getBatteryLevelPercentageInfo();
return mPrefContext.getString(
R.string.battery_usage_time_info_and_battery_level,
slotInformationMessage,
batteryLevelPercentageMessage);
}
private void animateBatteryChartViewGroup() {
if (mBatteryChartViewGroup != null && mBatteryChartViewGroup.getAlpha() == 0) {
mBatteryChartViewGroup
.animate()
.alpha(1f)
.setDuration(FADE_IN_ANIMATION_DURATION)
.start();
}
}
private void animateBatteryHourlyChartView(final boolean visible) {
if (mHourlyChartView == null
|| (mHourlyChartView.getVisibility() == View.VISIBLE) == visible) {
return;
}
if (visible) {
mHourlyChartView.setVisibility(View.VISIBLE);
mHourlyChartView
.animate()
.alpha(1f)
.setDuration(FADE_IN_ANIMATION_DURATION)
.setListener(mHourlyChartFadeInAdapter)
.start();
} else {
mHourlyChartView
.animate()
.alpha(0f)
.setDuration(FADE_OUT_ANIMATION_DURATION)
.setListener(mHourlyChartFadeOutAdapter)
.start();
}
}
private void setChartSummaryVisible(final boolean visible) {
if (mChartSummaryTextView != null) {
mChartSummaryTextView.setVisibility(visible ? View.VISIBLE : View.GONE);
}
}
private AnimatorListenerAdapter createHourlyChartAnimatorListenerAdapter(
final boolean visible) {
final int visibility = visible ? View.VISIBLE : View.GONE;
return new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (mHourlyChartView != null) {
mHourlyChartView.setVisibility(visibility);
}
}
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
if (mHourlyChartView != null) {
mHourlyChartView.setVisibility(visibility);
}
}
};
}
private boolean isBatteryLevelDataInOneDay() {
return mHourlyViewModels != null && mHourlyViewModels.size() == 1;
}
private boolean isAllSelected() {
return (isBatteryLevelDataInOneDay()
|| mDailyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL)
&& mHourlyChartIndex == BatteryChartViewModel.SELECTED_INDEX_ALL;
}
@VisibleForTesting
static int getTotalHours(final BatteryLevelData batteryLevelData) {
if (batteryLevelData == null) {
return 0;
}
List<Long> dailyTimestamps = batteryLevelData.getDailyBatteryLevels().getTimestamps();
return (int)
((dailyTimestamps.get(dailyTimestamps.size() - 1) - dailyTimestamps.get(0))
/ DateUtils.HOUR_IN_MILLIS);
}
/** Used for {@link AppBatteryPreferenceController}. */
public static List<BatteryDiffEntry> getAppBatteryUsageData(Context context) {
final long start = System.currentTimeMillis();
final Map<Long, Map<String, BatteryHistEntry>> batteryHistoryMap =
DatabaseUtils.getHistoryMapSinceLastFullCharge(context, Calendar.getInstance());
if (batteryHistoryMap == null || batteryHistoryMap.isEmpty()) {
return null;
}
Log.d(
TAG,
String.format(
"getBatterySinceLastFullChargeUsageData() size=%d time=%d/ms",
batteryHistoryMap.size(), (System.currentTimeMillis() - start)));
final Map<Integer, Map<Integer, BatteryDiffData>> batteryUsageData =
DataProcessor.getBatteryUsageData(context, batteryHistoryMap);
if (batteryUsageData == null) {
return null;
}
BatteryDiffData allBatteryDiffData =
batteryUsageData
.get(BatteryChartViewModel.SELECTED_INDEX_ALL)
.get(BatteryChartViewModel.SELECTED_INDEX_ALL);
return allBatteryDiffData == null ? null : allBatteryDiffData.getAppDiffEntryList();
}
private static <T> T getLast(List<T> list) {
if (list == null || list.isEmpty()) {
return null;
}
return list.get(list.size() - 1);
}
/** Used for {@link AppBatteryPreferenceController}. */
public static BatteryDiffEntry getAppBatteryUsageData(
Context context, String packageName, int userId) {
if (packageName == null) {
return null;
}
final List<BatteryDiffEntry> entries = getAppBatteryUsageData(context);
if (entries == null) {
return null;
}
for (BatteryDiffEntry entry : entries) {
if (!entry.isSystemEntry()
&& entry.mUserId == userId
&& packageName.equals(entry.getPackageName())) {
return entry;
}
}
return null;
}
private abstract class BaseLabelTextGenerator
implements BatteryChartViewModel.LabelTextGenerator {
@Override
public String generateSlotBatteryLevelText(List<Integer> levels, int index) {
final int fromBatteryLevelIndex =
index == BatteryChartViewModel.SELECTED_INDEX_ALL ? 0 : index;
final int toBatteryLevelIndex =
index == BatteryChartViewModel.SELECTED_INDEX_ALL
? levels.size() - 1
: index + 1;
return mPrefContext.getString(
R.string.battery_level_percentage,
generateBatteryLevelText(levels.get(fromBatteryLevelIndex)),
generateBatteryLevelText(levels.get(toBatteryLevelIndex)));
}
@VisibleForTesting
private static String generateBatteryLevelText(Integer level) {
return Utils.formatPercentage(level);
}
}
private final class DailyChartLabelTextGenerator extends BaseLabelTextGenerator
implements BatteryChartViewModel.LabelTextGenerator {
@Override
public String generateText(List<Long> timestamps, int index) {
return ConvertUtils.utcToLocalTimeDayOfWeek(
mContext, timestamps.get(index), /* isAbbreviation= */ true);
}
@Override
public String generateFullText(List<Long> timestamps, int index) {
return ConvertUtils.utcToLocalTimeDayOfWeek(
mContext, timestamps.get(index), /* isAbbreviation= */ false);
}
}
private final class HourlyChartLabelTextGenerator extends BaseLabelTextGenerator
implements BatteryChartViewModel.LabelTextGenerator {
private static final int FULL_CHARGE_BATTERY_LEVEL = 100;
private boolean mIsFromFullCharge;
private long mFistTimestamp;
private long mLatestTimestamp;
@Override
public String generateText(List<Long> timestamps, int index) {
if (Objects.equal(timestamps.get(index), mLatestTimestamp)) {
// Replaces the latest timestamp text to "now".
return mContext.getString(R.string.battery_usage_chart_label_now);
}
long timestamp = timestamps.get(index);
boolean showMinute = false;
if (Objects.equal(timestamp, mFistTimestamp)) {
if (mIsFromFullCharge) {
showMinute = true;
} else {
// starts from 7 days ago
timestamp = TimestampUtils.getLastEvenHourTimestamp(timestamp);
}
}
return ConvertUtils.utcToLocalTimeHour(
mContext, timestamp, mIs24HourFormat, showMinute);
}
@Override
public String generateFullText(List<Long> timestamps, int index) {
return index == timestamps.size() - 1
? generateText(timestamps, index)
: mContext.getString(
R.string.battery_usage_timestamps_hyphen,
generateText(timestamps, index),
generateText(timestamps, index + 1));
}
HourlyChartLabelTextGenerator updateSpecialCaseContext(
@NonNull final BatteryLevelData batteryLevelData) {
BatteryLevelData.PeriodBatteryLevelData firstDayLevelData =
batteryLevelData.getHourlyBatteryLevelsPerDay().get(0);
this.mIsFromFullCharge =
firstDayLevelData.getLevels().get(0) == FULL_CHARGE_BATTERY_LEVEL;
this.mFistTimestamp = firstDayLevelData.getTimestamps().get(0);
this.mLatestTimestamp =
getLast(
getLast(batteryLevelData.getHourlyBatteryLevelsPerDay())
.getTimestamps());
return this;
}
}
}

View File

@@ -0,0 +1,843 @@
/*
* Copyright (C) 2022 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.batteryusage;
import static com.android.settings.Utils.formatPercentage;
import static com.android.settings.fuelgauge.batteryusage.BatteryChartViewModel.AxisLabelPosition.BETWEEN_TRAPEZOIDS;
import static com.android.settingslib.fuelgauge.BatteryStatus.BATTERY_LEVEL_UNKNOWN;
import static java.lang.Math.abs;
import static java.lang.Math.round;
import static java.util.Objects.requireNonNull;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.widget.AppCompatImageView;
import com.android.settings.R;
import com.android.settingslib.Utils;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/** A widget component to draw chart graph. */
public class BatteryChartView extends AppCompatImageView implements View.OnClickListener {
private static final String TAG = "BatteryChartView";
private static final int DIVIDER_COLOR = Color.parseColor("#CDCCC5");
private static final int HORIZONTAL_DIVIDER_COUNT = 5;
/** A callback listener for selected group index is updated. */
public interface OnSelectListener {
/** The callback function for selected group index is updated. */
void onSelect(int trapezoidIndex);
}
private final String[] mPercentages = getPercentages();
private final Rect mIndent = new Rect();
private final Rect[] mPercentageBounds = new Rect[] {new Rect(), new Rect(), new Rect()};
private final List<Rect> mAxisLabelsBounds = new ArrayList<>();
private final Set<Integer> mLabelDrawnIndexes = new ArraySet<>();
private final int mLayoutDirection =
getContext().getResources().getConfiguration().getLayoutDirection();
private BatteryChartViewModel mViewModel;
private int mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID;
private int mDividerWidth;
private int mDividerHeight;
private float mTrapezoidVOffset;
private float mTrapezoidHOffset;
private int mTrapezoidColor;
private int mTrapezoidSolidColor;
private int mTrapezoidHoverColor;
private int mDefaultTextColor;
private int mTextPadding;
private int mTransomIconSize;
private int mTransomTop;
private int mTransomViewHeight;
private int mTransomLineDefaultColor;
private int mTransomLineSelectedColor;
private float mTransomPadding;
private Drawable mTransomIcon;
private Paint mTransomLinePaint;
private Paint mTransomSelectedSlotPaint;
private Paint mDividerPaint;
private Paint mTrapezoidPaint;
private Paint mTextPaint;
private AccessibilityNodeProvider mAccessibilityNodeProvider;
private BatteryChartView.OnSelectListener mOnSelectListener;
@VisibleForTesting TrapezoidSlot[] mTrapezoidSlots;
// Records the location to calculate selected index.
@VisibleForTesting float mTouchUpEventX = Float.MIN_VALUE;
public BatteryChartView(Context context) {
super(context, null);
}
public BatteryChartView(Context context, AttributeSet attrs) {
super(context, attrs);
initializeColors(context);
// Registers the click event listener.
setOnClickListener(this);
setClickable(false);
requestLayout();
}
/** Sets the data model of this view. */
public void setViewModel(BatteryChartViewModel viewModel) {
if (viewModel == null) {
mViewModel = null;
invalidate();
return;
}
Log.d(
TAG,
String.format(
"setViewModel(): size: %d, selectedIndex: %d, getHighlightSlotIndex: %d",
viewModel.size(),
viewModel.selectedIndex(),
viewModel.getHighlightSlotIndex()));
mViewModel = viewModel;
initializeAxisLabelsBounds();
initializeTrapezoidSlots(viewModel.size() - 1);
setClickable(hasAnyValidTrapezoid(viewModel));
requestLayout();
}
/** Sets the callback to monitor the selected group index. */
public void setOnSelectListener(BatteryChartView.OnSelectListener listener) {
mOnSelectListener = listener;
}
/** Sets the companion {@link TextView} for percentage information. */
public void setCompanionTextView(TextView textView) {
if (textView != null) {
// Pre-draws the view first to load style atttributions into paint.
textView.draw(new Canvas());
mTextPaint = textView.getPaint();
mDefaultTextColor = mTextPaint.getColor();
} else {
mTextPaint = null;
}
requestLayout();
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// Measures text bounds and updates indent configuration.
if (mTextPaint != null) {
mTextPaint.setTextAlign(Paint.Align.LEFT);
for (int index = 0; index < mPercentages.length; index++) {
mTextPaint.getTextBounds(
mPercentages[index],
0,
mPercentages[index].length(),
mPercentageBounds[index]);
}
// Updates the indent configurations.
mIndent.top = mPercentageBounds[0].height() + mTransomViewHeight;
final int textWidth = mPercentageBounds[0].width() + mTextPadding;
if (isRTL()) {
mIndent.left = textWidth;
} else {
mIndent.right = textWidth;
}
if (mViewModel != null) {
int maxTop = 0;
for (int index = 0; index < mViewModel.size(); index++) {
final String text = mViewModel.getText(index);
mTextPaint.getTextBounds(text, 0, text.length(), mAxisLabelsBounds.get(index));
maxTop = Math.max(maxTop, -mAxisLabelsBounds.get(index).top);
}
mIndent.bottom = maxTop + round(mTextPadding * 2f);
}
Log.d(TAG, "setIndent:" + mPercentageBounds[0]);
} else {
mIndent.set(0, 0, 0, 0);
}
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
// Before mLevels initialized, the count of trapezoids is unknown. Only draws the
// horizontal percentages and dividers.
drawHorizontalDividers(canvas);
if (mViewModel == null) {
return;
}
drawVerticalDividers(canvas);
drawTrapezoids(canvas);
drawTransomLine(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Caches the location to calculate selected trapezoid index.
final int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_UP:
mTouchUpEventX = event.getX();
break;
case MotionEvent.ACTION_CANCEL:
mTouchUpEventX = Float.MIN_VALUE; // reset
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean onHoverEvent(MotionEvent event) {
final int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_HOVER_ENTER:
case MotionEvent.ACTION_HOVER_MOVE:
final int trapezoidIndex = getTrapezoidIndex(event.getX());
if (mHoveredIndex != trapezoidIndex) {
mHoveredIndex = trapezoidIndex;
invalidate();
sendAccessibilityEventForHover(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
}
// Ignore the super.onHoverEvent() because the hovered trapezoid has already been
// sent here.
return true;
case MotionEvent.ACTION_HOVER_EXIT:
if (mHoveredIndex != BatteryChartViewModel.SELECTED_INDEX_INVALID) {
sendAccessibilityEventForHover(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset
invalidate();
}
// Ignore the super.onHoverEvent() because the hovered trapezoid has already been
// sent here.
return true;
default:
return super.onTouchEvent(event);
}
}
@Override
public void onHoverChanged(boolean hovered) {
super.onHoverChanged(hovered);
if (!hovered) {
mHoveredIndex = BatteryChartViewModel.SELECTED_INDEX_INVALID; // reset
invalidate();
}
}
@Override
public void onClick(View view) {
if (mTouchUpEventX == Float.MIN_VALUE) {
Log.w(TAG, "invalid motion event for onClick() callback");
return;
}
onTrapezoidClicked(view, getTrapezoidIndex(mTouchUpEventX));
}
@Override
public AccessibilityNodeProvider getAccessibilityNodeProvider() {
if (mViewModel == null) {
return super.getAccessibilityNodeProvider();
}
if (mAccessibilityNodeProvider == null) {
mAccessibilityNodeProvider = new BatteryChartAccessibilityNodeProvider();
}
return mAccessibilityNodeProvider;
}
private void onTrapezoidClicked(View view, int index) {
// Ignores the click event if the level is zero.
if (!isValidToDraw(mViewModel, index)) {
return;
}
if (mOnSelectListener != null) {
// Selects all if users click the same trapezoid item two times.
mOnSelectListener.onSelect(
index == mViewModel.selectedIndex()
? BatteryChartViewModel.SELECTED_INDEX_ALL
: index);
}
view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
}
private boolean sendAccessibilityEvent(int virtualDescendantId, int eventType) {
ViewParent parent = getParent();
if (parent == null || !AccessibilityManager.getInstance(mContext).isEnabled()) {
return false;
}
AccessibilityEvent accessibilityEvent = new AccessibilityEvent(eventType);
accessibilityEvent.setSource(this, virtualDescendantId);
accessibilityEvent.setEnabled(true);
accessibilityEvent.setClassName(getAccessibilityClassName());
accessibilityEvent.setPackageName(getContext().getPackageName());
return parent.requestSendAccessibilityEvent(this, accessibilityEvent);
}
private void sendAccessibilityEventForHover(int eventType) {
if (isTrapezoidIndexValid(mViewModel, mHoveredIndex)) {
sendAccessibilityEvent(mHoveredIndex, eventType);
}
}
private void initializeTrapezoidSlots(int count) {
mTrapezoidSlots = new TrapezoidSlot[count];
for (int index = 0; index < mTrapezoidSlots.length; index++) {
mTrapezoidSlots[index] = new TrapezoidSlot();
}
}
private void initializeColors(Context context) {
setBackgroundColor(Color.TRANSPARENT);
mTrapezoidSolidColor = Utils.getColorAccentDefaultColor(context);
mTrapezoidColor = Utils.getDisabled(context, mTrapezoidSolidColor);
mTrapezoidHoverColor =
Utils.getColorAttrDefaultColor(
context, com.android.internal.R.attr.materialColorSecondaryContainer);
// Initializes the divider line paint.
final Resources resources = getContext().getResources();
mDividerWidth = resources.getDimensionPixelSize(R.dimen.chartview_divider_width);
mDividerHeight = resources.getDimensionPixelSize(R.dimen.chartview_divider_height);
mDividerPaint = new Paint();
mDividerPaint.setAntiAlias(true);
mDividerPaint.setColor(DIVIDER_COLOR);
mDividerPaint.setStyle(Paint.Style.STROKE);
mDividerPaint.setStrokeWidth(mDividerWidth);
Log.i(TAG, "mDividerWidth:" + mDividerWidth);
Log.i(TAG, "mDividerHeight:" + mDividerHeight);
// Initializes the trapezoid paint.
mTrapezoidHOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_start);
mTrapezoidVOffset = resources.getDimension(R.dimen.chartview_trapezoid_margin_bottom);
mTrapezoidPaint = new Paint();
mTrapezoidPaint.setAntiAlias(true);
mTrapezoidPaint.setColor(mTrapezoidSolidColor);
mTrapezoidPaint.setStyle(Paint.Style.FILL);
mTrapezoidPaint.setPathEffect(
new CornerPathEffect(
resources.getDimensionPixelSize(R.dimen.chartview_trapezoid_radius)));
// Initializes for drawing text information.
mTextPadding = resources.getDimensionPixelSize(R.dimen.chartview_text_padding);
// Initializes the padding top for drawing text information.
mTransomViewHeight =
resources.getDimensionPixelSize(R.dimen.chartview_transom_layout_height);
}
private void initializeTransomPaint() {
if (mTransomLinePaint != null
&& mTransomSelectedSlotPaint != null
&& mTransomIcon != null) {
return;
}
// Initializes the transom line paint.
final Resources resources = getContext().getResources();
final int transomLineWidth =
resources.getDimensionPixelSize(R.dimen.chartview_transom_width);
final int transomRadius = resources.getDimensionPixelSize(R.dimen.chartview_transom_radius);
mTransomPadding = transomRadius * .5f;
mTransomTop = resources.getDimensionPixelSize(R.dimen.chartview_transom_padding_top);
mTransomLineDefaultColor = Utils.getDisabled(mContext, DIVIDER_COLOR);
mTransomLineSelectedColor =
resources.getColor(R.color.color_battery_anomaly_app_warning_selector);
final int slotHighlightColor = Utils.getDisabled(mContext, mTransomLineSelectedColor);
mTransomIconSize = resources.getDimensionPixelSize(R.dimen.chartview_transom_icon_size);
mTransomLinePaint = new Paint();
mTransomLinePaint.setAntiAlias(true);
mTransomLinePaint.setStyle(Paint.Style.STROKE);
mTransomLinePaint.setStrokeWidth(transomLineWidth);
mTransomLinePaint.setStrokeCap(Paint.Cap.ROUND);
mTransomLinePaint.setPathEffect(new CornerPathEffect(transomRadius));
mTransomSelectedSlotPaint = new Paint();
mTransomSelectedSlotPaint.setAntiAlias(true);
mTransomSelectedSlotPaint.setColor(slotHighlightColor);
mTransomSelectedSlotPaint.setStyle(Paint.Style.FILL);
// Get the companion icon beside transom line
mTransomIcon = getResources().getDrawable(R.drawable.ic_battery_tips_warning_icon);
}
private void drawHorizontalDividers(Canvas canvas) {
final int width = getWidth() - abs(mIndent.width());
final int height = getHeight() - mIndent.top - mIndent.bottom;
final float topOffsetY = mIndent.top + mDividerWidth * .5f;
final float bottomOffsetY = mIndent.top + (height - mDividerHeight - mDividerWidth * .5f);
final float availableSpace = bottomOffsetY - topOffsetY;
mDividerPaint.setColor(DIVIDER_COLOR);
final float dividerOffsetUnit = availableSpace / (float) (HORIZONTAL_DIVIDER_COUNT - 1);
// Draws 5 divider lines.
for (int index = 0; index < HORIZONTAL_DIVIDER_COUNT; index++) {
float offsetY = topOffsetY + dividerOffsetUnit * index;
canvas.drawLine(mIndent.left, offsetY, mIndent.left + width, offsetY, mDividerPaint);
// Draws percentage text only for 100% / 50% / 0%
if (index % 2 == 0) {
drawPercentage(canvas, /* index= */ (index + 1) / 2, offsetY);
}
}
}
private void drawPercentage(Canvas canvas, int index, float offsetY) {
if (mTextPaint != null) {
mTextPaint.setTextAlign(isRTL() ? Paint.Align.RIGHT : Paint.Align.LEFT);
mTextPaint.setColor(mDefaultTextColor);
canvas.drawText(
mPercentages[index],
isRTL()
? mIndent.left - mTextPadding
: getWidth() - mIndent.width() + mTextPadding,
offsetY + mPercentageBounds[index].height() * .5f,
mTextPaint);
}
}
private void drawVerticalDividers(Canvas canvas) {
final int width = getWidth() - abs(mIndent.width());
final int dividerCount = mTrapezoidSlots.length + 1;
final float dividerSpace = dividerCount * mDividerWidth;
final float unitWidth = (width - dividerSpace) / (float) mTrapezoidSlots.length;
final float bottomY = getHeight() - mIndent.bottom;
final float startY = bottomY - mDividerHeight;
final float trapezoidSlotOffset = mTrapezoidHOffset + mDividerWidth * .5f;
// Draws the axis label slot information.
if (mViewModel != null) {
final float baselineY = getHeight() - mTextPadding;
Rect[] axisLabelDisplayAreas;
switch (mViewModel.axisLabelPosition()) {
case CENTER_OF_TRAPEZOIDS:
axisLabelDisplayAreas =
getAxisLabelDisplayAreas(
/* size= */ mViewModel.size() - 1,
/* baselineX= */ mIndent.left + mDividerWidth + unitWidth * .5f,
/* offsetX= */ mDividerWidth + unitWidth,
baselineY,
/* shiftFirstAndLast= */ false);
break;
case BETWEEN_TRAPEZOIDS:
default:
axisLabelDisplayAreas =
getAxisLabelDisplayAreas(
/* size= */ mViewModel.size(),
/* baselineX= */ mIndent.left + mDividerWidth * .5f,
/* offsetX= */ mDividerWidth + unitWidth,
baselineY,
/* shiftFirstAndLast= */ true);
break;
}
drawAxisLabels(canvas, axisLabelDisplayAreas, baselineY);
}
// Draws each vertical dividers.
float startX = mDividerWidth * .5f + mIndent.left;
for (int index = 0; index < dividerCount; index++) {
float dividerY = bottomY;
if (mViewModel.axisLabelPosition() == BETWEEN_TRAPEZOIDS
&& mLabelDrawnIndexes.contains(index)) {
mDividerPaint.setColor(mTrapezoidSolidColor);
dividerY += mDividerHeight / 4f;
} else {
mDividerPaint.setColor(DIVIDER_COLOR);
}
canvas.drawLine(startX, startY, startX, dividerY, mDividerPaint);
final float nextX = startX + mDividerWidth + unitWidth;
// Updates the trapezoid slots for drawing.
if (index < mTrapezoidSlots.length) {
final int trapezoidIndex = isRTL() ? mTrapezoidSlots.length - index - 1 : index;
mTrapezoidSlots[trapezoidIndex].mLeft = round(startX + trapezoidSlotOffset);
mTrapezoidSlots[trapezoidIndex].mRight = round(nextX - trapezoidSlotOffset);
}
startX = nextX;
}
}
/** Gets all the axis label texts displaying area positions if they are shown. */
private Rect[] getAxisLabelDisplayAreas(
final int size,
final float baselineX,
final float offsetX,
final float baselineY,
final boolean shiftFirstAndLast) {
final Rect[] result = new Rect[size];
for (int index = 0; index < result.length; index++) {
final float width = mAxisLabelsBounds.get(index).width();
float middle = baselineX + index * offsetX;
if (shiftFirstAndLast) {
if (index == 0) {
middle += width * .5f;
}
if (index == size - 1) {
middle -= width * .5f;
}
}
final float left = middle - width * .5f;
final float right = left + width;
final float top = baselineY + mAxisLabelsBounds.get(index).top;
final float bottom = top + mAxisLabelsBounds.get(index).height();
result[index] = new Rect(round(left), round(top), round(right), round(bottom));
}
return result;
}
private void drawAxisLabels(Canvas canvas, final Rect[] displayAreas, final float baselineY) {
final int lastIndex = displayAreas.length - 1;
mLabelDrawnIndexes.clear();
// Suppose first and last labels are always able to draw.
drawAxisLabelText(canvas, 0, displayAreas[0], baselineY);
mLabelDrawnIndexes.add(0);
drawAxisLabelText(canvas, lastIndex, displayAreas[lastIndex], baselineY);
mLabelDrawnIndexes.add(lastIndex);
drawAxisLabelsBetweenStartIndexAndEndIndex(canvas, displayAreas, 0, lastIndex, baselineY);
}
/**
* Recursively draws axis labels between the start index and the end index. If the inner number
* can be exactly divided into 2 parts, check and draw the middle index label and then
* recursively draw the 2 parts. Otherwise, divide into 3 parts. Check and draw the middle two
* labels and then recursively draw the 3 parts. If there are any overlaps, skip drawing and go
* back to the uplevel of the recursion.
*/
private void drawAxisLabelsBetweenStartIndexAndEndIndex(
Canvas canvas,
final Rect[] displayAreas,
final int startIndex,
final int endIndex,
final float baselineY) {
if (endIndex - startIndex <= 1) {
return;
}
if ((endIndex - startIndex) % 2 == 0) {
int middleIndex = (startIndex + endIndex) / 2;
if (hasOverlap(displayAreas, startIndex, middleIndex)
|| hasOverlap(displayAreas, middleIndex, endIndex)) {
return;
}
drawAxisLabelText(canvas, middleIndex, displayAreas[middleIndex], baselineY);
mLabelDrawnIndexes.add(middleIndex);
drawAxisLabelsBetweenStartIndexAndEndIndex(
canvas, displayAreas, startIndex, middleIndex, baselineY);
drawAxisLabelsBetweenStartIndexAndEndIndex(
canvas, displayAreas, middleIndex, endIndex, baselineY);
} else {
int middleIndex1 = startIndex + round((endIndex - startIndex) / 3f);
int middleIndex2 = startIndex + round((endIndex - startIndex) * 2 / 3f);
if (hasOverlap(displayAreas, startIndex, middleIndex1)
|| hasOverlap(displayAreas, middleIndex1, middleIndex2)
|| hasOverlap(displayAreas, middleIndex2, endIndex)) {
return;
}
drawAxisLabelText(canvas, middleIndex1, displayAreas[middleIndex1], baselineY);
mLabelDrawnIndexes.add(middleIndex1);
drawAxisLabelText(canvas, middleIndex2, displayAreas[middleIndex2], baselineY);
mLabelDrawnIndexes.add(middleIndex2);
drawAxisLabelsBetweenStartIndexAndEndIndex(
canvas, displayAreas, startIndex, middleIndex1, baselineY);
drawAxisLabelsBetweenStartIndexAndEndIndex(
canvas, displayAreas, middleIndex1, middleIndex2, baselineY);
drawAxisLabelsBetweenStartIndexAndEndIndex(
canvas, displayAreas, middleIndex2, endIndex, baselineY);
}
}
private boolean hasOverlap(
final Rect[] displayAreas, final int leftIndex, final int rightIndex) {
return displayAreas[leftIndex].right + mTextPadding * 2.3f > displayAreas[rightIndex].left;
}
private boolean isRTL() {
return mLayoutDirection == View.LAYOUT_DIRECTION_RTL;
}
private void drawAxisLabelText(
Canvas canvas, int index, final Rect displayArea, final float baselineY) {
mTextPaint.setColor(mTrapezoidSolidColor);
mTextPaint.setTextAlign(Paint.Align.CENTER);
// Reverse the sort of axis labels for RTL
if (isRTL()) {
index =
mViewModel.axisLabelPosition() == BETWEEN_TRAPEZOIDS
? mViewModel.size() - index - 1 // for hourly
: mViewModel.size() - index - 2; // for daily
}
canvas.drawText(mViewModel.getText(index), displayArea.centerX(), baselineY, mTextPaint);
mLabelDrawnIndexes.add(index);
}
private void drawTrapezoids(Canvas canvas) {
// Ignores invalid trapezoid data.
if (mViewModel == null) {
return;
}
final float trapezoidBottom =
getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth - mTrapezoidVOffset;
final float availableSpace =
trapezoidBottom - mDividerWidth * .5f - mIndent.top - mTrapezoidVOffset;
final float unitHeight = availableSpace / 100f;
// Draws all trapezoid shapes into the canvas.
final Path trapezoidPath = new Path();
Path trapezoidCurvePath = null;
for (int index = 0; index < mTrapezoidSlots.length; index++) {
// Not draws the trapezoid for corner or not initialization cases.
if (!isValidToDraw(mViewModel, index)) {
continue;
}
// Configures the trapezoid paint color.
final int trapezoidColor =
(mViewModel.selectedIndex() == index
|| mViewModel.selectedIndex()
== BatteryChartViewModel.SELECTED_INDEX_ALL)
? mTrapezoidSolidColor
: mTrapezoidColor;
final boolean isHoverState =
mHoveredIndex == index && isValidToDraw(mViewModel, mHoveredIndex);
mTrapezoidPaint.setColor(isHoverState ? mTrapezoidHoverColor : trapezoidColor);
float leftTop =
round(
trapezoidBottom
- requireNonNull(mViewModel.getLevel(index)) * unitHeight);
float rightTop =
round(
trapezoidBottom
- requireNonNull(mViewModel.getLevel(index + 1)) * unitHeight);
// Mirror the shape of the trapezoid for RTL
if (isRTL()) {
float temp = leftTop;
leftTop = rightTop;
rightTop = temp;
}
trapezoidPath.reset();
trapezoidPath.moveTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
trapezoidPath.lineTo(mTrapezoidSlots[index].mRight, rightTop);
trapezoidPath.lineTo(mTrapezoidSlots[index].mRight, trapezoidBottom);
// A tricky way to make the trapezoid shape drawing the rounded corner.
trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, trapezoidBottom);
trapezoidPath.lineTo(mTrapezoidSlots[index].mLeft, leftTop);
// Draws the trapezoid shape into canvas.
canvas.drawPath(trapezoidPath, mTrapezoidPaint);
}
}
private boolean isHighlightSlotValid() {
return mViewModel != null
&& mViewModel.getHighlightSlotIndex()
!= BatteryChartViewModel.SELECTED_INDEX_INVALID;
}
private void drawTransomLine(Canvas canvas) {
if (!isHighlightSlotValid()) {
return;
}
initializeTransomPaint();
// Draw the whole transom line and a warning icon
mTransomLinePaint.setColor(mTransomLineDefaultColor);
final int width = getWidth() - abs(mIndent.width());
final float transomOffset = mTrapezoidHOffset + mDividerWidth * .5f + mTransomPadding;
final float trapezoidBottom =
getHeight() - mIndent.bottom - mDividerHeight - mDividerWidth - mTrapezoidVOffset;
canvas.drawLine(
mIndent.left + transomOffset,
mTransomTop,
mIndent.left + width - transomOffset,
mTransomTop,
mTransomLinePaint);
drawTransomIcon(canvas);
// Draw selected segment of transom line and a highlight slot
mTransomLinePaint.setColor(mTransomLineSelectedColor);
final int index = mViewModel.getHighlightSlotIndex();
final float startX = mTrapezoidSlots[index].mLeft;
final float endX = mTrapezoidSlots[index].mRight;
canvas.drawLine(
startX + mTransomPadding,
mTransomTop,
endX - mTransomPadding,
mTransomTop,
mTransomLinePaint);
canvas.drawRect(startX, mTransomTop, endX, trapezoidBottom, mTransomSelectedSlotPaint);
}
private void drawTransomIcon(Canvas canvas) {
if (mTransomIcon == null) {
return;
}
final int left =
isRTL()
? mIndent.left - mTextPadding - mTransomIconSize
: getWidth() - abs(mIndent.width()) + mTextPadding;
mTransomIcon.setBounds(
left,
mTransomTop - mTransomIconSize / 2,
left + mTransomIconSize,
mTransomTop + mTransomIconSize / 2);
mTransomIcon.draw(canvas);
}
// Searches the corresponding trapezoid index from x location.
private int getTrapezoidIndex(float x) {
if (mTrapezoidSlots == null) {
return BatteryChartViewModel.SELECTED_INDEX_INVALID;
}
for (int index = 0; index < mTrapezoidSlots.length; index++) {
final TrapezoidSlot slot = mTrapezoidSlots[index];
if (x >= slot.mLeft - mTrapezoidHOffset && x <= slot.mRight + mTrapezoidHOffset) {
return index;
}
}
return BatteryChartViewModel.SELECTED_INDEX_INVALID;
}
private void initializeAxisLabelsBounds() {
mAxisLabelsBounds.clear();
for (int i = 0; i < mViewModel.size(); i++) {
mAxisLabelsBounds.add(new Rect());
}
}
private static boolean isTrapezoidValid(
@NonNull BatteryChartViewModel viewModel, int trapezoidIndex) {
return viewModel.getLevel(trapezoidIndex) != BATTERY_LEVEL_UNKNOWN
&& viewModel.getLevel(trapezoidIndex + 1) != BATTERY_LEVEL_UNKNOWN;
}
private static boolean isTrapezoidIndexValid(
@NonNull BatteryChartViewModel viewModel, int trapezoidIndex) {
return viewModel != null && trapezoidIndex >= 0 && trapezoidIndex < viewModel.size() - 1;
}
private static boolean isValidToDraw(BatteryChartViewModel viewModel, int trapezoidIndex) {
return isTrapezoidIndexValid(viewModel, trapezoidIndex)
&& isTrapezoidValid(viewModel, trapezoidIndex);
}
private static boolean hasAnyValidTrapezoid(@NonNull BatteryChartViewModel viewModel) {
// Sets the chart is clickable if there is at least one valid item in it.
for (int trapezoidIndex = 0; trapezoidIndex < viewModel.size() - 1; trapezoidIndex++) {
if (isTrapezoidValid(viewModel, trapezoidIndex)) {
return true;
}
}
return false;
}
private static String[] getPercentages() {
return new String[] {
formatPercentage(/* percentage= */ 100, /* round= */ true),
formatPercentage(/* percentage= */ 50, /* round= */ true),
formatPercentage(/* percentage= */ 0, /* round= */ true)
};
}
private class BatteryChartAccessibilityNodeProvider extends AccessibilityNodeProvider {
@Override
public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
if (virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) {
final AccessibilityNodeInfo hostInfo =
new AccessibilityNodeInfo(BatteryChartView.this);
for (int index = 0; index < mViewModel.size() - 1; index++) {
hostInfo.addChild(BatteryChartView.this, index);
}
return hostInfo;
}
final int index = virtualViewId;
if (!isTrapezoidIndexValid(mViewModel, index)) {
Log.w(TAG, "Invalid virtual view id:" + index);
return null;
}
final AccessibilityNodeInfo childInfo =
new AccessibilityNodeInfo(BatteryChartView.this, index);
final String slotTimeInfo = mViewModel.getFullText(index);
final String batteryLevelInfo = mViewModel.getSlotBatteryLevelText(index);
onInitializeAccessibilityNodeInfo(childInfo);
childInfo.setClickable(isValidToDraw(mViewModel, index));
childInfo.setText(slotTimeInfo);
childInfo.setContentDescription(
mContext.getString(
R.string.battery_usage_time_info_and_battery_level,
slotTimeInfo,
batteryLevelInfo));
final Rect bounds = new Rect();
getBoundsOnScreen(bounds, true);
final int hostLeft = bounds.left;
bounds.left = round(hostLeft + mTrapezoidSlots[index].mLeft);
bounds.right = round(hostLeft + mTrapezoidSlots[index].mRight);
childInfo.setBoundsInScreen(bounds);
return childInfo;
}
@Override
public boolean performAction(int virtualViewId, int action, @Nullable Bundle arguments) {
if (virtualViewId == AccessibilityNodeProvider.HOST_VIEW_ID) {
return performAccessibilityAction(action, arguments);
}
switch (action) {
case AccessibilityNodeInfo.ACTION_CLICK:
onTrapezoidClicked(BatteryChartView.this, virtualViewId);
return true;
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
return sendAccessibilityEvent(
virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
return sendAccessibilityEvent(
virtualViewId,
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
default:
return performAccessibilityAction(action, arguments);
}
}
}
// A container class for each trapezoid left and right location.
@VisibleForTesting
static final class TrapezoidSlot {
public float mLeft;
public float mRight;
@Override
public String toString() {
return String.format(Locale.US, "TrapezoidSlot[%f,%f]", mLeft, mRight);
}
}
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright (C) 2022 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.batteryusage;
import androidx.annotation.NonNull;
import androidx.core.util.Preconditions;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
/** The view model of {@code BatteryChartView} */
class BatteryChartViewModel {
private static final String TAG = "BatteryChartViewModel";
public static final int SELECTED_INDEX_ALL = -1;
public static final int SELECTED_INDEX_INVALID = -2;
// We need at least 2 levels to draw a trapezoid.
private static final int MIN_LEVELS_DATA_SIZE = 2;
enum AxisLabelPosition {
BETWEEN_TRAPEZOIDS,
CENTER_OF_TRAPEZOIDS,
}
interface LabelTextGenerator {
/** Generates the label text. The text may be abbreviated to save space. */
String generateText(List<Long> timestamps, int index);
/** Generates the full text for accessibility. */
String generateFullText(List<Long> timestamps, int index);
/** Generates the battery level text of a slot for accessibility.*/
String generateSlotBatteryLevelText(List<Integer> levels, int index);
}
private final List<Integer> mLevels;
private final List<Long> mTimestamps;
private final AxisLabelPosition mAxisLabelPosition;
private final LabelTextGenerator mLabelTextGenerator;
private final String[] mTexts;
private final String[] mFullTexts;
private final String[] mBatteryLevelTexts;
private int mSelectedIndex = SELECTED_INDEX_ALL;
private int mHighlightSlotIndex = SELECTED_INDEX_INVALID;
BatteryChartViewModel(
@NonNull List<Integer> levels,
@NonNull List<Long> timestamps,
@NonNull AxisLabelPosition axisLabelPosition,
@NonNull LabelTextGenerator labelTextGenerator) {
Preconditions.checkArgument(
levels.size() == timestamps.size() && levels.size() >= MIN_LEVELS_DATA_SIZE,
String.format(
Locale.ENGLISH,
"Invalid BatteryChartViewModel levels.size: %d, timestamps.size: %d.",
levels.size(),
timestamps.size()));
mLevels = levels;
mTimestamps = timestamps;
mAxisLabelPosition = axisLabelPosition;
mLabelTextGenerator = labelTextGenerator;
mTexts = new String[size()];
mFullTexts = new String[size()];
// Last one for SELECTED_INDEX_ALL
mBatteryLevelTexts = new String[size() + 1];
}
public int size() {
return mLevels.size();
}
public Integer getLevel(int index) {
return mLevels.get(index);
}
public String getText(int index) {
if (mTexts[index] == null) {
mTexts[index] = mLabelTextGenerator.generateText(mTimestamps, index);
}
return mTexts[index];
}
public String getFullText(int index) {
if (mFullTexts[index] == null) {
mFullTexts[index] = mLabelTextGenerator.generateFullText(mTimestamps, index);
}
return mFullTexts[index];
}
public String getSlotBatteryLevelText(int index) {
final int textIndex = index != SELECTED_INDEX_ALL ? index : size();
if (mBatteryLevelTexts[textIndex] == null) {
mBatteryLevelTexts[textIndex] =
mLabelTextGenerator.generateSlotBatteryLevelText(mLevels, index);
}
return mBatteryLevelTexts[textIndex];
}
public AxisLabelPosition axisLabelPosition() {
return mAxisLabelPosition;
}
public int selectedIndex() {
return mSelectedIndex;
}
public void setSelectedIndex(int index) {
mSelectedIndex = index;
}
public int getHighlightSlotIndex() {
return mHighlightSlotIndex;
}
public void setHighlightSlotIndex(int index) {
mHighlightSlotIndex = index;
}
@Override
public int hashCode() {
return Objects.hash(mLevels, mTimestamps, mSelectedIndex, mAxisLabelPosition);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
} else if (!(other instanceof BatteryChartViewModel)) {
return false;
}
final BatteryChartViewModel batteryChartViewModel = (BatteryChartViewModel) other;
return Objects.equals(mLevels, batteryChartViewModel.mLevels)
&& Objects.equals(mTimestamps, batteryChartViewModel.mTimestamps)
&& mAxisLabelPosition == batteryChartViewModel.mAxisLabelPosition
&& mSelectedIndex == batteryChartViewModel.mSelectedIndex;
}
@Override
public String toString() {
// Generate all the texts and full texts.
for (int i = 0; i < size(); i++) {
getText(i);
getFullText(i);
}
return new StringBuilder()
.append("levels: " + Objects.toString(mLevels))
.append(", timestamps: " + Objects.toString(mTimestamps))
.append(", texts: " + Arrays.toString(mTexts))
.append(", fullTexts: " + Arrays.toString(mFullTexts))
.append(", axisLabelPosition: " + mAxisLabelPosition)
.append(", selectedIndex: " + mSelectedIndex)
.toString();
}
}

View File

@@ -0,0 +1,338 @@
/*
* Copyright (C) 2022 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.batteryusage;
import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.utcToLocalTimeForLogging;
import android.content.Context;
import android.os.BatteryConsumer;
import androidx.annotation.NonNull;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.fuelgauge.PowerUsageFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/** Wraps the battery usage diff data for each entry used for battery usage app list. */
public class BatteryDiffData {
static final double SMALL_PERCENTAGE_THRESHOLD = 1f;
private final long mStartTimestamp;
private final long mEndTimestamp;
private final int mStartBatteryLevel;
private final int mEndBatteryLevel;
private final long mScreenOnTime;
private final List<BatteryDiffEntry> mAppEntries;
private final List<BatteryDiffEntry> mSystemEntries;
/** Constructor for the diff entries. */
public BatteryDiffData(
final Context context,
final long startTimestamp,
final long endTimestamp,
final int startBatteryLevel,
final int endBatteryLevel,
final long screenOnTime,
final @NonNull List<BatteryDiffEntry> appDiffEntries,
final @NonNull List<BatteryDiffEntry> systemDiffEntries,
final @NonNull Set<String> systemAppsPackageNames,
final @NonNull Set<Integer> systemAppsUids,
final boolean isAccumulated) {
mStartTimestamp = startTimestamp;
mEndTimestamp = endTimestamp;
mStartBatteryLevel = startBatteryLevel;
mEndBatteryLevel = endBatteryLevel;
mScreenOnTime = screenOnTime;
mAppEntries = appDiffEntries;
mSystemEntries = systemDiffEntries;
if (!isAccumulated) {
final PowerUsageFeatureProvider featureProvider =
FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider();
purgeBatteryDiffData(featureProvider);
combineBatteryDiffEntry(
context, featureProvider, systemAppsPackageNames, systemAppsUids);
}
processAndSortEntries(mAppEntries);
processAndSortEntries(mSystemEntries);
}
long getStartTimestamp() {
return mStartTimestamp;
}
long getEndTimestamp() {
return mEndTimestamp;
}
int getStartBatteryLevel() {
return mStartBatteryLevel;
}
int getEndBatteryLevel() {
return mEndBatteryLevel;
}
long getScreenOnTime() {
return mScreenOnTime;
}
List<BatteryDiffEntry> getAppDiffEntryList() {
return mAppEntries;
}
List<BatteryDiffEntry> getSystemDiffEntryList() {
return mSystemEntries;
}
@Override
public String toString() {
return new StringBuilder("BatteryDiffData{")
.append("startTimestamp:" + utcToLocalTimeForLogging(mStartTimestamp))
.append("|endTimestamp:" + utcToLocalTimeForLogging(mEndTimestamp))
.append("|startLevel:" + mStartBatteryLevel)
.append("|endLevel:" + mEndBatteryLevel)
.append("|screenOnTime:" + mScreenOnTime)
.append("|appEntries.size:" + mAppEntries.size())
.append("|systemEntries.size:" + mSystemEntries.size())
.append("}")
.toString();
}
/** Removes fake usage data and hidden packages. */
private void purgeBatteryDiffData(final PowerUsageFeatureProvider featureProvider) {
purgeBatteryDiffData(featureProvider, mAppEntries);
purgeBatteryDiffData(featureProvider, mSystemEntries);
}
/** Combines into SystemAppsBatteryDiffEntry and OthersBatteryDiffEntry. */
private void combineBatteryDiffEntry(
final Context context,
final PowerUsageFeatureProvider featureProvider,
final @NonNull Set<String> systemAppsPackageNames,
final @NonNull Set<Integer> systemAppsUids) {
combineIntoUninstalledApps(context, mAppEntries);
combineIntoSystemApps(
context, featureProvider, systemAppsPackageNames, systemAppsUids, mAppEntries);
combineSystemItemsIntoOthers(context, featureProvider, mSystemEntries);
}
private static void purgeBatteryDiffData(
final PowerUsageFeatureProvider featureProvider, final List<BatteryDiffEntry> entries) {
final double screenOnTimeThresholdInMs =
featureProvider.getBatteryUsageListScreenOnTimeThresholdInMs();
final double consumePowerThreshold =
featureProvider.getBatteryUsageListConsumePowerThreshold();
final Set<Integer> hideSystemComponentSet = featureProvider.getHideSystemComponentSet();
final Set<String> hideBackgroundUsageTimeSet =
featureProvider.getHideBackgroundUsageTimeSet();
final Set<String> hideApplicationSet = featureProvider.getHideApplicationSet();
final Iterator<BatteryDiffEntry> iterator = entries.iterator();
while (iterator.hasNext()) {
final BatteryDiffEntry entry = iterator.next();
final long screenOnTimeInMs = entry.mScreenOnTimeInMs;
final double comsumePower = entry.mConsumePower;
final String packageName = entry.getPackageName();
final Integer componentId = entry.mComponentId;
if ((screenOnTimeInMs < screenOnTimeThresholdInMs
&& comsumePower < consumePowerThreshold)
|| ConvertUtils.FAKE_PACKAGE_NAME.equals(packageName)
|| hideSystemComponentSet.contains(componentId)
|| (packageName != null && hideApplicationSet.contains(packageName))) {
iterator.remove();
}
if (packageName != null && hideBackgroundUsageTimeSet.contains(packageName)) {
entry.mBackgroundUsageTimeInMs = 0;
}
}
}
private static void combineIntoSystemApps(
final Context context,
final PowerUsageFeatureProvider featureProvider,
final @NonNull Set<String> systemAppsPackageNames,
final @NonNull Set<Integer> systemAppsUids,
final @NonNull List<BatteryDiffEntry> appEntries) {
final List<String> systemAppsAllowlist = featureProvider.getSystemAppsAllowlist();
BatteryDiffEntry systemAppsDiffEntry = null;
final Iterator<BatteryDiffEntry> appListIterator = appEntries.iterator();
while (appListIterator.hasNext()) {
final BatteryDiffEntry batteryDiffEntry = appListIterator.next();
if (needsCombineInSystemApp(
batteryDiffEntry,
systemAppsAllowlist,
systemAppsPackageNames,
systemAppsUids)) {
if (systemAppsDiffEntry == null) {
systemAppsDiffEntry =
new BatteryDiffEntry(
context,
BatteryDiffEntry.SYSTEM_APPS_KEY,
BatteryDiffEntry.SYSTEM_APPS_KEY,
ConvertUtils.CONSUMER_TYPE_UID_BATTERY);
}
systemAppsDiffEntry.mConsumePower += batteryDiffEntry.mConsumePower;
systemAppsDiffEntry.mForegroundUsageTimeInMs +=
batteryDiffEntry.mForegroundUsageTimeInMs;
systemAppsDiffEntry.setTotalConsumePower(batteryDiffEntry.getTotalConsumePower());
appListIterator.remove();
}
}
if (systemAppsDiffEntry != null) {
appEntries.add(systemAppsDiffEntry);
}
}
private static void combineIntoUninstalledApps(
final Context context, final @NonNull List<BatteryDiffEntry> appEntries) {
BatteryDiffEntry uninstalledAppDiffEntry = null;
final Iterator<BatteryDiffEntry> appListIterator = appEntries.iterator();
while (appListIterator.hasNext()) {
final BatteryDiffEntry batteryDiffEntry = appListIterator.next();
if (!batteryDiffEntry.isUninstalledEntry()) {
continue;
}
if (uninstalledAppDiffEntry == null) {
uninstalledAppDiffEntry =
new BatteryDiffEntry(
context,
BatteryDiffEntry.UNINSTALLED_APPS_KEY,
BatteryDiffEntry.UNINSTALLED_APPS_KEY,
ConvertUtils.CONSUMER_TYPE_UID_BATTERY);
}
uninstalledAppDiffEntry.mConsumePower += batteryDiffEntry.mConsumePower;
uninstalledAppDiffEntry.mForegroundUsageTimeInMs +=
batteryDiffEntry.mForegroundUsageTimeInMs;
uninstalledAppDiffEntry.setTotalConsumePower(batteryDiffEntry.getTotalConsumePower());
appListIterator.remove();
}
if (uninstalledAppDiffEntry != null) {
appEntries.add(uninstalledAppDiffEntry);
}
}
private static void combineSystemItemsIntoOthers(
final Context context,
final PowerUsageFeatureProvider featureProvider,
final List<BatteryDiffEntry> systemEntries) {
final Set<Integer> othersSystemComponentSet = featureProvider.getOthersSystemComponentSet();
final Set<String> othersCustomComponentNameSet =
featureProvider.getOthersCustomComponentNameSet();
BatteryDiffEntry othersDiffEntry = null;
final Iterator<BatteryDiffEntry> systemListIterator = systemEntries.iterator();
while (systemListIterator.hasNext()) {
final BatteryDiffEntry batteryDiffEntry = systemListIterator.next();
final int componentId = batteryDiffEntry.mComponentId;
if (othersSystemComponentSet.contains(componentId)
|| (componentId >= BatteryConsumer.FIRST_CUSTOM_POWER_COMPONENT_ID
&& othersCustomComponentNameSet.contains(
batteryDiffEntry.getAppLabel()))) {
if (othersDiffEntry == null) {
othersDiffEntry =
new BatteryDiffEntry(
context,
BatteryDiffEntry.OTHERS_KEY,
BatteryDiffEntry.OTHERS_KEY,
ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY);
}
othersDiffEntry.mConsumePower += batteryDiffEntry.mConsumePower;
othersDiffEntry.setTotalConsumePower(batteryDiffEntry.getTotalConsumePower());
systemListIterator.remove();
}
}
if (othersDiffEntry != null) {
systemEntries.add(othersDiffEntry);
}
}
@VisibleForTesting
static boolean needsCombineInSystemApp(
final BatteryDiffEntry batteryDiffEntry,
final @NonNull List<String> systemAppsAllowlist,
final @NonNull Set<String> systemAppsPackageNames,
final @NonNull Set<Integer> systemAppsUids) {
if (batteryDiffEntry.mIsHidden) {
return true;
}
final String packageName = batteryDiffEntry.getPackageName();
if (packageName == null || packageName.isEmpty()) {
return false;
}
if (systemAppsAllowlist.contains(packageName)) {
return true;
}
int uid = (int) batteryDiffEntry.mUid;
return systemAppsPackageNames.contains(packageName) || systemAppsUids.contains(uid);
}
/**
* Sets total consume power, and adjusts the percentages to ensure the total round percentage
* could be 100%, and then sorts entries based on the sorting key.
*/
@VisibleForTesting
static void processAndSortEntries(final List<BatteryDiffEntry> batteryDiffEntries) {
if (batteryDiffEntries.isEmpty()) {
return;
}
// Sets total consume power.
double totalConsumePower = 0.0;
for (BatteryDiffEntry batteryDiffEntry : batteryDiffEntries) {
totalConsumePower += batteryDiffEntry.mConsumePower;
}
for (BatteryDiffEntry batteryDiffEntry : batteryDiffEntries) {
batteryDiffEntry.setTotalConsumePower(totalConsumePower);
}
// Adjusts percentages to show.
// The lower bound is treating all the small percentages as 0.
// The upper bound is treating all the small percentages as 1.
int totalLowerBound = 0;
int totalUpperBound = 0;
for (BatteryDiffEntry entry : batteryDiffEntries) {
if (entry.getPercentage() < SMALL_PERCENTAGE_THRESHOLD) {
totalUpperBound += 1;
} else {
int roundPercentage = Math.round((float) entry.getPercentage());
totalLowerBound += roundPercentage;
totalUpperBound += roundPercentage;
}
}
if (totalLowerBound > 100 || totalUpperBound < 100) {
Collections.sort(batteryDiffEntries, BatteryDiffEntry.COMPARATOR);
for (int i = 0; i < totalLowerBound - 100 && i < batteryDiffEntries.size(); i++) {
batteryDiffEntries.get(i).setAdjustPercentageOffset(-1);
}
for (int i = 0; i < 100 - totalUpperBound && i < batteryDiffEntries.size(); i++) {
batteryDiffEntries.get(i).setAdjustPercentageOffset(1);
}
}
// Sorts entries.
Collections.sort(batteryDiffEntries, BatteryDiffEntry.COMPARATOR);
}
}

View File

@@ -0,0 +1,593 @@
/*
* Copyright (C) 2022 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.batteryusage;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.GuardedBy;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settings.fuelgauge.batteryusage.BatteryEntry.NameAndIcon;
import com.android.settingslib.utils.StringUtil;
import java.util.Comparator;
import java.util.Locale;
import java.util.Map;
/** A container class to carry battery data in a specific time slot. */
public class BatteryDiffEntry {
private static final String TAG = "BatteryDiffEntry";
private static final Object sResourceCacheLock = new Object();
private static final Object sPackageNameAndUidCacheLock = new Object();
private static final Object sValidForRestrictionLock = new Object();
static Locale sCurrentLocale = null;
// Caches app label and icon to improve loading performance.
@GuardedBy("sResourceCacheLock")
static final Map<String, NameAndIcon> sResourceCache = new ArrayMap<>();
// Caches package name and uid to improve loading performance.
@GuardedBy("sPackageNameAndUidCacheLock")
static final Map<String, Integer> sPackageNameAndUidCache = new ArrayMap<>();
// Whether a specific item is valid to launch restriction page?
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
@GuardedBy("sValidForRestrictionLock")
static final Map<String, Boolean> sValidForRestriction = new ArrayMap<>();
/** A comparator for {@link BatteryDiffEntry} based on the sorting key. */
static final Comparator<BatteryDiffEntry> COMPARATOR =
(a, b) -> Double.compare(b.getSortingKey(), a.getSortingKey());
static final String SYSTEM_APPS_KEY = "A|SystemApps";
static final String UNINSTALLED_APPS_KEY = "A|UninstalledApps";
static final String OTHERS_KEY = "S|Others";
// key -> (label_id, icon_id)
private static final Map<String, Pair<Integer, Integer>> SPECIAL_ENTRY_MAP =
Map.of(
SYSTEM_APPS_KEY,
Pair.create(R.string.battery_usage_system_apps, R.drawable.ic_power_system),
UNINSTALLED_APPS_KEY,
Pair.create(
R.string.battery_usage_uninstalled_apps,
R.drawable.ic_battery_uninstalled),
OTHERS_KEY,
Pair.create(
R.string.battery_usage_others,
R.drawable.ic_settings_battery_usage_others));
public long mUid;
public long mUserId;
public String mKey;
public boolean mIsHidden;
public int mComponentId;
public String mLegacyPackageName;
public String mLegacyLabel;
public int mConsumerType;
public long mForegroundUsageTimeInMs;
public long mForegroundServiceUsageTimeInMs;
public long mBackgroundUsageTimeInMs;
public long mScreenOnTimeInMs;
public double mConsumePower;
public double mForegroundUsageConsumePower;
public double mForegroundServiceUsageConsumePower;
public double mBackgroundUsageConsumePower;
public double mCachedUsageConsumePower;
protected Context mContext;
private double mTotalConsumePower;
private double mPercentage;
private int mAdjustPercentageOffset;
private UserManager mUserManager;
private String mDefaultPackageName = null;
@VisibleForTesting int mAppIconId;
@VisibleForTesting String mAppLabel = null;
@VisibleForTesting Drawable mAppIcon = null;
@VisibleForTesting boolean mIsLoaded = false;
@VisibleForTesting boolean mValidForRestriction = true;
public BatteryDiffEntry(
Context context,
long uid,
long userId,
String key,
boolean isHidden,
int componentId,
String legacyPackageName,
String legacyLabel,
int consumerType,
long foregroundUsageTimeInMs,
long foregroundServiceUsageTimeInMs,
long backgroundUsageTimeInMs,
long screenOnTimeInMs,
double consumePower,
double foregroundUsageConsumePower,
double foregroundServiceUsageConsumePower,
double backgroundUsageConsumePower,
double cachedUsageConsumePower) {
mContext = context;
mUid = uid;
mUserId = userId;
mKey = key;
mIsHidden = isHidden;
mComponentId = componentId;
mLegacyPackageName = legacyPackageName;
mLegacyLabel = legacyLabel;
mConsumerType = consumerType;
mForegroundUsageTimeInMs = foregroundUsageTimeInMs;
mForegroundServiceUsageTimeInMs = foregroundServiceUsageTimeInMs;
mBackgroundUsageTimeInMs = backgroundUsageTimeInMs;
mScreenOnTimeInMs = screenOnTimeInMs;
mConsumePower = consumePower;
mForegroundUsageConsumePower = foregroundUsageConsumePower;
mForegroundServiceUsageConsumePower = foregroundServiceUsageConsumePower;
mBackgroundUsageConsumePower = backgroundUsageConsumePower;
mCachedUsageConsumePower = cachedUsageConsumePower;
mUserManager = context.getSystemService(UserManager.class);
}
public BatteryDiffEntry(Context context, String key, String legacyLabel, int consumerType) {
this(
context,
/* uid= */ 0,
/* userId= */ 0,
key,
/* isHidden= */ false,
/* componentId= */ -1,
/* legacyPackageName= */ null,
legacyLabel,
consumerType,
/* foregroundUsageTimeInMs= */ 0,
/* foregroundServiceUsageTimeInMs= */ 0,
/* backgroundUsageTimeInMs= */ 0,
/* screenOnTimeInMs= */ 0,
/* consumePower= */ 0,
/* foregroundUsageConsumePower= */ 0,
/* foregroundServiceUsageConsumePower= */ 0,
/* backgroundUsageConsumePower= */ 0,
/* cachedUsageConsumePower= */ 0);
}
/** Sets the total consumed power in a specific time slot. */
public void setTotalConsumePower(double totalConsumePower) {
mTotalConsumePower = totalConsumePower;
mPercentage = totalConsumePower == 0 ? 0 : (mConsumePower / mTotalConsumePower) * 100.0;
mAdjustPercentageOffset = 0;
}
/** Gets the total consumed power in a specific time slot. */
public double getTotalConsumePower() {
return mTotalConsumePower;
}
/** Gets the percentage of total consumed power. */
public double getPercentage() {
return mPercentage;
}
/** Gets the percentage offset to adjust. */
public double getAdjustPercentageOffset() {
return mAdjustPercentageOffset;
}
/** Sets the percentage offset to adjust. */
public void setAdjustPercentageOffset(int offset) {
mAdjustPercentageOffset = offset;
}
/** Gets the key for sorting */
public double getSortingKey() {
String key = getKey();
if (key == null) {
return getPercentage() + getAdjustPercentageOffset();
}
// For special entries, put them to the end of the list.
switch (key) {
case UNINSTALLED_APPS_KEY:
case OTHERS_KEY:
return -1;
case SYSTEM_APPS_KEY:
return -2;
default:
return getPercentage() + getAdjustPercentageOffset();
}
}
/** Clones a new instance. */
public BatteryDiffEntry clone() {
return new BatteryDiffEntry(
this.mContext,
this.mUid,
this.mUserId,
this.mKey,
this.mIsHidden,
this.mComponentId,
this.mLegacyPackageName,
this.mLegacyLabel,
this.mConsumerType,
this.mForegroundUsageTimeInMs,
this.mForegroundServiceUsageTimeInMs,
this.mBackgroundUsageTimeInMs,
this.mScreenOnTimeInMs,
this.mConsumePower,
this.mForegroundUsageConsumePower,
this.mForegroundServiceUsageConsumePower,
this.mBackgroundUsageConsumePower,
this.mCachedUsageConsumePower);
}
/** Gets the app label name for this entry. */
public String getAppLabel() {
loadLabelAndIcon();
// Returns default application label if we cannot find it.
return mAppLabel == null || mAppLabel.length() == 0 ? mLegacyLabel : mAppLabel;
}
/** Gets the app icon {@link Drawable} for this entry. */
public Drawable getAppIcon() {
loadLabelAndIcon();
return mAppIcon != null && mAppIcon.getConstantState() != null
? mAppIcon.getConstantState().newDrawable()
: null;
}
/** Gets the app icon id for this entry. */
public int getAppIconId() {
loadLabelAndIcon();
return mAppIconId;
}
/** Gets the searching package name for UID battery type. */
public String getPackageName() {
final String packageName =
mDefaultPackageName != null ? mDefaultPackageName : mLegacyPackageName;
if (packageName == null) {
return packageName;
}
// Removes potential appended process name in the PackageName.
// From "com.opera.browser:privileged_process0" to "com.opera.browser"
final String[] splitPackageNames = packageName.split(":");
return splitPackageNames != null && splitPackageNames.length > 0
? splitPackageNames[0]
: packageName;
}
/** Whether this item is valid for users to launch restriction page? */
public boolean validForRestriction() {
loadLabelAndIcon();
return mValidForRestriction;
}
/** Whether the current BatteryDiffEntry is system component or not. */
public boolean isSystemEntry() {
if (mIsHidden) {
return false;
}
switch (mConsumerType) {
case ConvertUtils.CONSUMER_TYPE_USER_BATTERY:
case ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY:
return true;
case ConvertUtils.CONSUMER_TYPE_UID_BATTERY:
default:
return false;
}
}
/** Whether the current BatteryDiffEntry is uninstalled app or not. */
public boolean isUninstalledEntry() {
final String packageName = getPackageName();
if (TextUtils.isEmpty(packageName)
|| isSystemEntry()
// Some special package UIDs could be 0. Those packages are not installed by users.
|| mUid == BatteryUtils.UID_ZERO) {
return false;
}
final int uid = getPackageUid(packageName);
return uid == BatteryUtils.UID_REMOVED_APPS || uid == BatteryUtils.UID_NULL;
}
private int getPackageUid(String packageName) {
synchronized (sPackageNameAndUidCacheLock) {
if (sPackageNameAndUidCache.containsKey(packageName)) {
return sPackageNameAndUidCache.get(packageName);
}
}
int uid =
BatteryUtils.getInstance(mContext).getPackageUidAsUser(packageName, (int) mUserId);
synchronized (sPackageNameAndUidCacheLock) {
sPackageNameAndUidCache.put(packageName, uid);
}
return uid;
}
void loadLabelAndIcon() {
if (mIsLoaded) {
return;
}
// Checks whether we have cached data or not first before fetching.
final NameAndIcon nameAndIcon = getCache();
if (nameAndIcon != null) {
mAppLabel = nameAndIcon.mName;
mAppIcon = nameAndIcon.mIcon;
mAppIconId = nameAndIcon.mIconId;
}
Boolean validForRestriction = null;
synchronized (sValidForRestrictionLock) {
validForRestriction = sValidForRestriction.get(getKey());
}
if (validForRestriction != null) {
mValidForRestriction = validForRestriction;
}
// Both nameAndIcon and restriction configuration have cached data.
if (nameAndIcon != null && validForRestriction != null) {
return;
}
mIsLoaded = true;
// Configures whether we can launch restriction page or not.
updateRestrictionFlagState();
synchronized (sValidForRestrictionLock) {
sValidForRestriction.put(getKey(), Boolean.valueOf(mValidForRestriction));
}
if (getKey() != null && SPECIAL_ENTRY_MAP.containsKey(getKey())) {
Pair<Integer, Integer> pair = SPECIAL_ENTRY_MAP.get(getKey());
mAppLabel = mContext.getString(pair.first);
mAppIconId = pair.second;
mAppIcon = mContext.getDrawable(mAppIconId);
putResourceCache(getKey(), new NameAndIcon(mAppLabel, mAppIcon, mAppIconId));
return;
}
// Loads application icon and label based on consumer type.
switch (mConsumerType) {
case ConvertUtils.CONSUMER_TYPE_USER_BATTERY:
final NameAndIcon nameAndIconForUser =
BatteryEntry.getNameAndIconFromUserId(mContext, (int) mUserId);
if (nameAndIconForUser != null) {
mAppIcon = nameAndIconForUser.mIcon;
mAppLabel = nameAndIconForUser.mName;
putResourceCache(
getKey(), new NameAndIcon(mAppLabel, mAppIcon, /* iconId= */ 0));
}
break;
case ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY:
final NameAndIcon nameAndIconForSystem =
BatteryEntry.getNameAndIconFromPowerComponent(mContext, mComponentId);
if (nameAndIconForSystem != null) {
mAppLabel = nameAndIconForSystem.mName;
if (nameAndIconForSystem.mIconId != 0) {
mAppIconId = nameAndIconForSystem.mIconId;
mAppIcon = mContext.getDrawable(nameAndIconForSystem.mIconId);
}
putResourceCache(getKey(), new NameAndIcon(mAppLabel, mAppIcon, mAppIconId));
}
break;
case ConvertUtils.CONSUMER_TYPE_UID_BATTERY:
loadNameAndIconForUid();
// Uses application default icon if we cannot find it from package.
if (mAppIcon == null) {
mAppIcon = mContext.getPackageManager().getDefaultActivityIcon();
}
// Adds badge icon into app icon for work profile.
mAppIcon = getBadgeIconForUser(mAppIcon);
if (mAppLabel != null || mAppIcon != null) {
putResourceCache(
getKey(), new NameAndIcon(mAppLabel, mAppIcon, /* iconId= */ 0));
}
break;
}
}
String getKey() {
return mKey;
}
@VisibleForTesting
void updateRestrictionFlagState() {
if (isSystemEntry()) {
mValidForRestriction = false;
return;
}
final boolean isValidPackage =
BatteryUtils.getInstance(mContext).getPackageUid(getPackageName())
!= BatteryUtils.UID_NULL;
if (!isValidPackage) {
mValidForRestriction = false;
return;
}
try {
mValidForRestriction =
mContext.getPackageManager()
.getPackageInfo(
getPackageName(),
PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.MATCH_ANY_USER
| PackageManager.GET_SIGNATURES
| PackageManager.GET_PERMISSIONS)
!= null;
} catch (Exception e) {
Log.e(
TAG,
String.format(
"getPackageInfo() error %s for package=%s",
e.getCause(), getPackageName()));
mValidForRestriction = false;
}
}
private NameAndIcon getCache() {
final Locale locale = Locale.getDefault();
if (sCurrentLocale != locale) {
Log.d(
TAG,
String.format(
"clearCache() locale is changed from %s to %s",
sCurrentLocale, locale));
sCurrentLocale = locale;
clearCache();
}
synchronized (sResourceCacheLock) {
return sResourceCache.get(getKey());
}
}
private void loadNameAndIconForUid() {
final String packageName = getPackageName();
final PackageManager packageManager = mContext.getPackageManager();
// Gets the application label from PackageManager.
if (packageName != null && packageName.length() != 0) {
try {
final ApplicationInfo appInfo =
packageManager.getApplicationInfo(packageName, /*no flags*/ 0);
if (appInfo != null) {
mAppLabel = packageManager.getApplicationLabel(appInfo).toString();
mAppIcon = packageManager.getApplicationIcon(appInfo);
}
} catch (NameNotFoundException e) {
Log.e(TAG, "failed to retrieve ApplicationInfo for: " + packageName);
mAppLabel = packageName;
}
}
// Early return if we found the app label and icon resource.
if (mAppLabel != null && mAppIcon != null) {
return;
}
final int uid = (int) mUid;
final String[] packages = packageManager.getPackagesForUid(uid);
// Loads special defined application label and icon if available.
if (packages == null || packages.length == 0) {
final NameAndIcon nameAndIcon =
BatteryEntry.getNameAndIconFromUid(mContext, mAppLabel, uid);
mAppLabel = nameAndIcon.mName;
mAppIcon = nameAndIcon.mIcon;
}
final NameAndIcon nameAndIcon =
BatteryEntry.loadNameAndIcon(
mContext, uid, /* batteryEntry= */ null, packageName, mAppLabel, mAppIcon);
// Clears BatteryEntry internal cache since we will have another one.
BatteryEntry.clearUidCache();
if (nameAndIcon != null) {
mAppLabel = nameAndIcon.mName;
mAppIcon = nameAndIcon.mIcon;
mDefaultPackageName = nameAndIcon.mPackageName;
if (mDefaultPackageName != null
&& !mDefaultPackageName.equals(nameAndIcon.mPackageName)) {
Log.w(
TAG,
String.format(
"found different package: %s | %s",
mDefaultPackageName, nameAndIcon.mPackageName));
}
}
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("BatteryDiffEntry{");
builder.append(
String.format("\n\tname=%s restrictable=%b", mAppLabel, mValidForRestriction));
builder.append(
String.format(
"\n\tconsume=%.2f%% %f/%f",
mPercentage, mConsumePower, mTotalConsumePower));
builder.append(
String.format(
"\n\tconsume power= foreground:%f foregroundService:%f",
mForegroundUsageConsumePower, mForegroundServiceUsageConsumePower));
builder.append(
String.format(
"\n\tconsume power= background:%f cached:%f",
mBackgroundUsageConsumePower, mCachedUsageConsumePower));
builder.append(
String.format(
"\n\ttime= foreground:%s foregroundService:%s "
+ "background:%s screen-on:%s",
StringUtil.formatElapsedTime(
mContext,
(double) mForegroundUsageTimeInMs,
/* withSeconds= */ true,
/* collapseTimeUnit= */ false),
StringUtil.formatElapsedTime(
mContext,
(double) mForegroundServiceUsageTimeInMs,
/* withSeconds= */ true,
/* collapseTimeUnit= */ false),
StringUtil.formatElapsedTime(
mContext,
(double) mBackgroundUsageTimeInMs,
/* withSeconds= */ true,
/* collapseTimeUnit= */ false),
StringUtil.formatElapsedTime(
mContext,
(double) mScreenOnTimeInMs,
/* withSeconds= */ true,
/* collapseTimeUnit= */ false)));
builder.append(
String.format(
"\n\tpackage:%s|%s uid:%d userId:%d",
mLegacyPackageName, getPackageName(), mUid, mUserId));
return builder.toString();
}
/** Clears all cache data. */
public static void clearCache() {
synchronized (sResourceCacheLock) {
sResourceCache.clear();
}
synchronized (sValidForRestrictionLock) {
sValidForRestriction.clear();
}
synchronized (sPackageNameAndUidCacheLock) {
sPackageNameAndUidCache.clear();
}
}
private static void putResourceCache(String key, NameAndIcon nameAndIcon) {
synchronized (sResourceCacheLock) {
sResourceCache.put(key, nameAndIcon);
}
}
private Drawable getBadgeIconForUser(Drawable icon) {
final int userId = UserHandle.getUserId((int) mUid);
return userId == UserHandle.USER_OWNER
? icon
: mUserManager.getBadgedIconForUser(icon, new UserHandle(userId));
}
}

View File

@@ -0,0 +1,658 @@
/*
* Copyright (C) 2022 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.batteryusage;
import android.app.AppGlobals;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.UserInfo;
import android.graphics.drawable.Drawable;
import android.os.BatteryConsumer;
import android.os.BatteryConsumer.Dimensions;
import android.os.Process;
import android.os.RemoteException;
import android.os.UidBatteryConsumer;
import android.os.UserBatteryConsumer;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.ArrayMap;
import android.util.DebugUtils;
import android.util.Log;
import com.android.settings.R;
import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settingslib.Utils;
import java.util.Comparator;
import java.util.Locale;
/**
* Wraps the power usage data of a BatterySipper with information about package name and icon image.
*/
public class BatteryEntry {
/** The app name and icon in app list. */
public static final class NameAndIcon {
public final String mName;
public final String mPackageName;
public final Drawable mIcon;
public final int mIconId;
public NameAndIcon(String name, Drawable icon, int iconId) {
this(name, /* packageName= */ null, icon, iconId);
}
public NameAndIcon(String name, String packageName, Drawable icon, int iconId) {
this.mName = name;
this.mIcon = icon;
this.mIconId = iconId;
this.mPackageName = packageName;
}
}
private static final String TAG = "BatteryEntry";
private static final String PACKAGE_SYSTEM = "android";
static final int BATTERY_USAGE_INDEX_FOREGROUND = 0;
static final int BATTERY_USAGE_INDEX_FOREGROUND_SERVICE = 1;
static final int BATTERY_USAGE_INDEX_BACKGROUND = 2;
static final int BATTERY_USAGE_INDEX_CACHED = 3;
static final Dimensions[] BATTERY_DIMENSIONS =
new Dimensions[] {
new Dimensions(
BatteryConsumer.POWER_COMPONENT_ANY,
BatteryConsumer.PROCESS_STATE_FOREGROUND),
new Dimensions(
BatteryConsumer.POWER_COMPONENT_ANY,
BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE),
new Dimensions(
BatteryConsumer.POWER_COMPONENT_ANY,
BatteryConsumer.PROCESS_STATE_BACKGROUND),
new Dimensions(
BatteryConsumer.POWER_COMPONENT_ANY, BatteryConsumer.PROCESS_STATE_CACHED),
};
static final ArrayMap<String, UidToDetail> sUidCache = new ArrayMap<>();
static Locale sCurrentLocale = null;
/** Clears the UID cache. */
public static void clearUidCache() {
sUidCache.clear();
}
public static final Comparator<BatteryEntry> COMPARATOR =
(a, b) -> Double.compare(b.getConsumedPower(), a.getConsumedPower());
private final Context mContext;
private final BatteryConsumer mBatteryConsumer;
private final int mUid;
private final boolean mIsHidden;
@ConvertUtils.ConsumerType private final int mConsumerType;
@BatteryConsumer.PowerComponent private final int mPowerComponentId;
private long mUsageDurationMs;
private long mTimeInForegroundMs;
private long mTimeInForegroundServiceMs;
private long mTimeInBackgroundMs;
public String mName;
public Drawable mIcon;
public int mIconId;
public double mPercent;
private String mDefaultPackageName;
private double mConsumedPower;
private double mConsumedPowerInForeground;
private double mConsumedPowerInForegroundService;
private double mConsumedPowerInBackground;
private double mConsumedPowerInCached;
static class UidToDetail {
String mName;
String mPackageName;
Drawable mIcon;
}
public BatteryEntry(
Context context,
UserManager um,
BatteryConsumer batteryConsumer,
boolean isHidden,
int uid,
String[] packages,
String packageName) {
this(context, um, batteryConsumer, isHidden, uid, packages, packageName, true);
}
public BatteryEntry(
Context context,
UserManager um,
BatteryConsumer batteryConsumer,
boolean isHidden,
int uid,
String[] packages,
String packageName,
boolean loadDataInBackground) {
mContext = context;
mBatteryConsumer = batteryConsumer;
mIsHidden = isHidden;
mDefaultPackageName = packageName;
mPowerComponentId = -1;
if (batteryConsumer instanceof UidBatteryConsumer) {
mUid = uid;
mConsumerType = ConvertUtils.CONSUMER_TYPE_UID_BATTERY;
mConsumedPower = batteryConsumer.getConsumedPower();
UidBatteryConsumer uidBatteryConsumer = (UidBatteryConsumer) batteryConsumer;
if (mDefaultPackageName == null) {
// Apps should only have one package
if (packages != null && packages.length == 1) {
mDefaultPackageName = packages[0];
} else {
mDefaultPackageName =
isSystemUid(uid)
? PACKAGE_SYSTEM
: uidBatteryConsumer.getPackageWithHighestDrain();
}
}
if (mDefaultPackageName != null) {
PackageManager pm = context.getPackageManager();
try {
ApplicationInfo appInfo =
pm.getApplicationInfo(mDefaultPackageName, 0 /* no flags */);
mName = pm.getApplicationLabel(appInfo).toString();
} catch (NameNotFoundException e) {
Log.d(
TAG,
"PackageManager failed to retrieve ApplicationInfo for: "
+ mDefaultPackageName);
mName = mDefaultPackageName;
}
}
mTimeInForegroundMs =
uidBatteryConsumer.getTimeInProcessStateMs(
UidBatteryConsumer.PROCESS_STATE_FOREGROUND);
mTimeInForegroundServiceMs =
uidBatteryConsumer.getTimeInProcessStateMs(
UidBatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE);
mTimeInBackgroundMs =
uidBatteryConsumer.getTimeInProcessStateMs(
UidBatteryConsumer.PROCESS_STATE_BACKGROUND);
mConsumedPowerInForeground =
safeGetConsumedPower(
uidBatteryConsumer, BATTERY_DIMENSIONS[BATTERY_USAGE_INDEX_FOREGROUND]);
mConsumedPowerInForegroundService =
safeGetConsumedPower(
uidBatteryConsumer,
BATTERY_DIMENSIONS[BATTERY_USAGE_INDEX_FOREGROUND_SERVICE]);
mConsumedPowerInBackground =
safeGetConsumedPower(
uidBatteryConsumer, BATTERY_DIMENSIONS[BATTERY_USAGE_INDEX_BACKGROUND]);
mConsumedPowerInCached =
safeGetConsumedPower(
uidBatteryConsumer, BATTERY_DIMENSIONS[BATTERY_USAGE_INDEX_CACHED]);
} else if (batteryConsumer instanceof UserBatteryConsumer) {
mUid = Process.INVALID_UID;
mConsumerType = ConvertUtils.CONSUMER_TYPE_USER_BATTERY;
mConsumedPower = batteryConsumer.getConsumedPower();
final NameAndIcon nameAndIcon =
getNameAndIconFromUserId(
context, ((UserBatteryConsumer) batteryConsumer).getUserId());
mIcon = nameAndIcon.mIcon;
mName = nameAndIcon.mName;
} else {
throw new IllegalArgumentException("Unsupported: " + batteryConsumer);
}
}
/** Battery entry for a power component of AggregateBatteryConsumer */
public BatteryEntry(
Context context,
int powerComponentId,
double devicePowerMah,
long usageDurationMs,
boolean isHidden) {
mContext = context;
mBatteryConsumer = null;
mUid = Process.INVALID_UID;
mIsHidden = isHidden;
mPowerComponentId = powerComponentId;
mConsumedPower = devicePowerMah;
mUsageDurationMs = usageDurationMs;
mConsumerType = ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY;
final NameAndIcon nameAndIcon = getNameAndIconFromPowerComponent(context, powerComponentId);
mIconId = nameAndIcon.mIconId;
mName = nameAndIcon.mName;
if (mIconId != 0) {
mIcon = context.getDrawable(mIconId);
}
}
/** Battery entry for a custom power component of AggregateBatteryConsumer */
public BatteryEntry(
Context context,
int powerComponentId,
String powerComponentName,
double devicePowerMah) {
mContext = context;
mBatteryConsumer = null;
mUid = Process.INVALID_UID;
mIsHidden = false;
mPowerComponentId = powerComponentId;
mIconId = R.drawable.ic_power_system;
mIcon = context.getDrawable(mIconId);
mName = powerComponentName;
mConsumedPower = devicePowerMah;
mConsumerType = ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY;
}
public Drawable getIcon() {
return mIcon;
}
public String getLabel() {
return mName;
}
@ConvertUtils.ConsumerType
public int getConsumerType() {
return mConsumerType;
}
@BatteryConsumer.PowerComponent
public int getPowerComponentId() {
return mPowerComponentId;
}
/** Loads the app label and icon image and stores into the cache. */
public static NameAndIcon loadNameAndIcon(
Context context,
int uid,
BatteryEntry batteryEntry,
String defaultPackageName,
String name,
Drawable icon) {
// Bail out if the current sipper is not an App sipper.
if (uid == 0 || uid == Process.INVALID_UID) {
return null;
}
final PackageManager pm = context.getPackageManager();
final String[] packages =
isSystemUid(uid) ? new String[] {PACKAGE_SYSTEM} : pm.getPackagesForUid(uid);
if (packages != null) {
final String[] packageLabels = new String[packages.length];
System.arraycopy(packages, 0, packageLabels, 0, packages.length);
// Convert package names to user-facing labels where possible
final IPackageManager ipm = AppGlobals.getPackageManager();
final int userId = UserHandle.getUserId(uid);
for (int i = 0; i < packageLabels.length; i++) {
try {
final ApplicationInfo ai =
ipm.getApplicationInfo(packageLabels[i], 0 /* no flags */, userId);
if (ai == null) {
Log.d(
TAG,
"Retrieving null app info for package "
+ packageLabels[i]
+ ", user "
+ userId);
continue;
}
final CharSequence label = ai.loadLabel(pm);
if (label != null) {
packageLabels[i] = label.toString();
}
if (ai.icon != 0) {
defaultPackageName = packages[i];
icon = ai.loadIcon(pm);
break;
}
} catch (RemoteException e) {
Log.d(
TAG,
"Error while retrieving app info for package "
+ packageLabels[i]
+ ", user "
+ userId,
e);
}
}
if (packageLabels.length == 1) {
name = packageLabels[0];
} else {
// Look for an official name for this UID.
for (String pkgName : packages) {
try {
final PackageInfo pi = ipm.getPackageInfo(pkgName, 0, userId);
if (pi == null) {
Log.d(
TAG,
"Retrieving null package info for package "
+ pkgName
+ ", user "
+ userId);
continue;
}
if (pi.sharedUserLabel != 0) {
final CharSequence nm =
pm.getText(pkgName, pi.sharedUserLabel, pi.applicationInfo);
if (nm != null) {
name = nm.toString();
if (pi.applicationInfo.icon != 0) {
defaultPackageName = pkgName;
icon = pi.applicationInfo.loadIcon(pm);
}
break;
}
}
} catch (RemoteException e) {
Log.d(
TAG,
"Error while retrieving package info for package "
+ pkgName
+ ", user "
+ userId,
e);
}
}
}
}
final String uidString = Integer.toString(uid);
if (icon == null) {
icon = pm.getDefaultActivityIcon();
}
UidToDetail utd = new UidToDetail();
utd.mName = name;
utd.mIcon = icon;
utd.mPackageName = defaultPackageName;
sUidCache.put(uidString, utd);
return new NameAndIcon(name, defaultPackageName, icon, /* iconId= */ 0);
}
/** Returns a string that uniquely identifies this battery consumer. */
public String getKey() {
if (mBatteryConsumer instanceof UidBatteryConsumer) {
return Integer.toString(mUid);
} else if (mBatteryConsumer instanceof UserBatteryConsumer) {
return "U|" + ((UserBatteryConsumer) mBatteryConsumer).getUserId();
} else {
return "S|" + mPowerComponentId;
}
}
/** Returns true if the entry is hidden from the battery usage summary list. */
public boolean isHidden() {
return mIsHidden;
}
/** Returns true if this entry describes an app (UID). */
public boolean isAppEntry() {
return mBatteryConsumer instanceof UidBatteryConsumer;
}
/** Returns true if this entry describes a User. */
public boolean isUserEntry() {
if (mBatteryConsumer instanceof UserBatteryConsumer) {
return true;
}
return false;
}
/**
* Returns the package name that should be used to represent the UID described by this entry.
*/
public String getDefaultPackageName() {
return mDefaultPackageName;
}
/** Returns the UID of the app described by this entry. */
public int getUid() {
return mUid;
}
/** Returns foreground time/ms that is attributed to this entry. */
public long getTimeInForegroundMs() {
return (mBatteryConsumer instanceof UidBatteryConsumer)
? mTimeInForegroundMs
: mUsageDurationMs;
}
/** Returns foreground service time/ms that is attributed to this entry. */
public long getTimeInForegroundServiceMs() {
return (mBatteryConsumer instanceof UidBatteryConsumer) ? mTimeInForegroundServiceMs : 0;
}
/** Returns background activity time/ms that is attributed to this entry. */
public long getTimeInBackgroundMs() {
return (mBatteryConsumer instanceof UidBatteryConsumer) ? mTimeInBackgroundMs : 0;
}
/** Returns total amount of power (in milli-amp-hours) that is attributed to this entry. */
public double getConsumedPower() {
return mConsumedPower;
}
/**
* Returns amount of power (in milli-amp-hours) used in foreground that is attributed to this
* entry.
*/
public double getConsumedPowerInForeground() {
if (mBatteryConsumer instanceof UidBatteryConsumer) {
return mConsumedPowerInForeground;
} else {
return 0;
}
}
/**
* Returns amount of power (in milli-amp-hours) used in foreground service that is attributed to
* this entry.
*/
public double getConsumedPowerInForegroundService() {
if (mBatteryConsumer instanceof UidBatteryConsumer) {
return mConsumedPowerInForegroundService;
} else {
return 0;
}
}
/**
* Returns amount of power (in milli-amp-hours) used in background that is attributed to this
* entry.
*/
public double getConsumedPowerInBackground() {
if (mBatteryConsumer instanceof UidBatteryConsumer) {
return mConsumedPowerInBackground;
} else {
return 0;
}
}
/**
* Returns amount of power (in milli-amp-hours) used in cached that is attributed to this entry.
*/
public double getConsumedPowerInCached() {
if (mBatteryConsumer instanceof UidBatteryConsumer) {
return mConsumedPowerInCached;
} else {
return 0;
}
}
/**
* Adds the consumed power of the supplied BatteryConsumer to this entry. Also uses its package
* with highest drain, if necessary.
*/
public void add(BatteryConsumer batteryConsumer) {
mConsumedPower += batteryConsumer.getConsumedPower();
if (batteryConsumer instanceof UidBatteryConsumer) {
UidBatteryConsumer uidBatteryConsumer = (UidBatteryConsumer) batteryConsumer;
mTimeInForegroundMs +=
uidBatteryConsumer.getTimeInProcessStateMs(
UidBatteryConsumer.PROCESS_STATE_FOREGROUND);
mTimeInForegroundServiceMs +=
uidBatteryConsumer.getTimeInProcessStateMs(
UidBatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE);
mTimeInBackgroundMs +=
uidBatteryConsumer.getTimeInProcessStateMs(
UidBatteryConsumer.PROCESS_STATE_BACKGROUND);
mConsumedPowerInForeground +=
safeGetConsumedPower(
uidBatteryConsumer, BATTERY_DIMENSIONS[BATTERY_USAGE_INDEX_FOREGROUND]);
mConsumedPowerInForegroundService +=
safeGetConsumedPower(
uidBatteryConsumer,
BATTERY_DIMENSIONS[BATTERY_USAGE_INDEX_FOREGROUND_SERVICE]);
mConsumedPowerInBackground +=
safeGetConsumedPower(
uidBatteryConsumer, BATTERY_DIMENSIONS[BATTERY_USAGE_INDEX_BACKGROUND]);
mConsumedPowerInCached +=
safeGetConsumedPower(
uidBatteryConsumer, BATTERY_DIMENSIONS[BATTERY_USAGE_INDEX_CACHED]);
if (mDefaultPackageName == null) {
mDefaultPackageName = uidBatteryConsumer.getPackageWithHighestDrain();
}
}
}
/** Gets name and icon resource from UserBatteryConsumer userId. */
public static NameAndIcon getNameAndIconFromUserId(Context context, final int userId) {
UserManager um = context.getSystemService(UserManager.class);
UserInfo info = um.getUserInfo(userId);
Drawable icon = null;
String name = null;
if (info != null) {
icon = Utils.getUserIcon(context, um, info);
name = Utils.getUserLabel(context, info);
} else {
name =
context.getResources()
.getString(R.string.running_process_item_removed_user_label);
}
return new NameAndIcon(name, icon, 0 /* iconId */);
}
/** Gets name and icon resource from UidBatteryConsumer uid. */
public static NameAndIcon getNameAndIconFromUid(Context context, String name, final int uid) {
Drawable icon = context.getDrawable(R.drawable.ic_power_system);
if (uid == 0) {
name =
context.getResources()
.getString(com.android.settingslib.R.string.process_kernel_label);
} else if (uid == BatteryUtils.UID_REMOVED_APPS) {
name = context.getResources().getString(R.string.process_removed_apps);
} else if (uid == BatteryUtils.UID_TETHERING) {
name = context.getResources().getString(R.string.process_network_tethering);
} else if ("mediaserver".equals(name)) {
name = context.getResources().getString(R.string.process_mediaserver_label);
} else if ("dex2oat".equals(name) || "dex2oat32".equals(name) || "dex2oat64".equals(name)) {
name = context.getResources().getString(R.string.process_dex2oat_label);
}
return new NameAndIcon(name, icon, 0 /* iconId */);
}
/** Gets name and icon resource from BatteryConsumer power component ID. */
public static NameAndIcon getNameAndIconFromPowerComponent(
Context context, @BatteryConsumer.PowerComponent int powerComponentId) {
String name;
int iconId;
switch (powerComponentId) {
// Please see go/battery-usage-system-component-map
case BatteryConsumer.POWER_COMPONENT_SCREEN: // id: 0
name = context.getResources().getString(R.string.power_screen);
iconId = R.drawable.ic_settings_display;
break;
case BatteryConsumer.POWER_COMPONENT_CPU: // id: 1
name = context.getResources().getString(R.string.power_cpu);
iconId = R.drawable.ic_settings_cpu;
break;
case BatteryConsumer.POWER_COMPONENT_BLUETOOTH: // id: 2
name = context.getResources().getString(R.string.power_bluetooth);
iconId = R.drawable.ic_settings_bluetooth;
break;
case BatteryConsumer.POWER_COMPONENT_CAMERA: // id: 3
name = context.getResources().getString(R.string.power_camera);
iconId = R.drawable.ic_settings_camera;
break;
case BatteryConsumer.POWER_COMPONENT_FLASHLIGHT: // id: 6
name = context.getResources().getString(R.string.power_flashlight);
iconId = R.drawable.ic_settings_flashlight;
break;
case BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO: // id: 8
name = context.getResources().getString(R.string.power_cell);
iconId = R.drawable.ic_settings_cellular;
break;
case BatteryConsumer.POWER_COMPONENT_GNSS: // id: 10
name = context.getResources().getString(R.string.power_gps);
iconId = R.drawable.ic_settings_gps;
break;
case BatteryConsumer.POWER_COMPONENT_WIFI: // id: 11
name = context.getResources().getString(R.string.power_wifi);
iconId = R.drawable.ic_settings_wireless_no_theme;
break;
case BatteryConsumer.POWER_COMPONENT_PHONE: // id: 14
name = context.getResources().getString(R.string.power_phone);
iconId = R.drawable.ic_settings_voice_calls;
break;
case BatteryConsumer.POWER_COMPONENT_AMBIENT_DISPLAY: // id :15
name = context.getResources().getString(R.string.ambient_display_screen_title);
iconId = R.drawable.ic_settings_aod;
break;
default:
Log.w(
TAG,
"unknown attribute:"
+ DebugUtils.constantToString(
BatteryConsumer.class,
"POWER_COMPONENT_",
powerComponentId));
name = null;
iconId = R.drawable.ic_power_system;
break;
}
return new NameAndIcon(name, null /* icon */, iconId);
}
/** Whether the uid is system uid. */
public static boolean isSystemUid(int uid) {
return uid == Process.SYSTEM_UID;
}
private static double safeGetConsumedPower(
final UidBatteryConsumer uidBatteryConsumer, final Dimensions dimension) {
try {
return uidBatteryConsumer.getConsumedPower(dimension);
} catch (IllegalArgumentException e) {
Log.e(TAG, "safeGetConsumedPower failed:" + e);
return 0.0d;
}
}
}

View File

@@ -0,0 +1,382 @@
/*
* Copyright (C) 2022 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.batteryusage;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.BatteryConsumer;
import android.util.Log;
/** A container class to carry data from {@link ContentValues}. */
public class BatteryHistEntry {
private static final boolean DEBUG = false;
private static final String TAG = "BatteryHistEntry";
/** Keys for accessing {@link ContentValues} or {@link Cursor}. */
public static final String KEY_UID = "uid";
public static final String KEY_USER_ID = "userId";
public static final String KEY_PACKAGE_NAME = "packageName";
public static final String KEY_TIMESTAMP = "timestamp";
public static final String KEY_CONSUMER_TYPE = "consumerType";
public static final String KEY_IS_FULL_CHARGE_CYCLE_START = "isFullChargeCycleStart";
public static final String KEY_BATTERY_INFORMATION = "batteryInformation";
public static final String KEY_BATTERY_INFORMATION_DEBUG = "batteryInformationDebug";
public final long mUid;
public final long mUserId;
public final String mAppLabel;
public final String mPackageName;
// Whether the data is represented as system component or not?
public final boolean mIsHidden;
// Records the timestamp relative information.
public final long mBootTimestamp;
public final long mTimestamp;
public final String mZoneId;
// Records the battery usage relative information.
public final double mTotalPower;
public final double mConsumePower;
public final double mForegroundUsageConsumePower;
public final double mForegroundServiceUsageConsumePower;
public final double mBackgroundUsageConsumePower;
public final double mCachedUsageConsumePower;
public final double mPercentOfTotal;
public final long mForegroundUsageTimeInMs;
public final long mForegroundServiceUsageTimeInMs;
public final long mBackgroundUsageTimeInMs;
@BatteryConsumer.PowerComponent public final int mDrainType;
@ConvertUtils.ConsumerType public final int mConsumerType;
// Records the battery intent relative information.
public final int mBatteryLevel;
public final int mBatteryStatus;
public final int mBatteryHealth;
private String mKey = null;
private boolean mIsValidEntry = true;
public BatteryHistEntry(ContentValues values) {
mUid = getLong(values, KEY_UID);
mUserId = getLong(values, KEY_USER_ID);
mPackageName = getString(values, KEY_PACKAGE_NAME);
mTimestamp = getLong(values, KEY_TIMESTAMP);
mConsumerType = getInteger(values, KEY_CONSUMER_TYPE);
final BatteryInformation batteryInformation =
ConvertUtils.getBatteryInformation(values, KEY_BATTERY_INFORMATION);
mAppLabel = batteryInformation.getAppLabel();
mIsHidden = batteryInformation.getIsHidden();
mBootTimestamp = batteryInformation.getBootTimestamp();
mZoneId = batteryInformation.getZoneId();
mTotalPower = batteryInformation.getTotalPower();
mConsumePower = batteryInformation.getConsumePower();
mForegroundUsageConsumePower = batteryInformation.getForegroundUsageConsumePower();
mForegroundServiceUsageConsumePower =
batteryInformation.getForegroundServiceUsageConsumePower();
mBackgroundUsageConsumePower = batteryInformation.getBackgroundUsageConsumePower();
mCachedUsageConsumePower = batteryInformation.getCachedUsageConsumePower();
mPercentOfTotal = batteryInformation.getPercentOfTotal();
mForegroundUsageTimeInMs = batteryInformation.getForegroundUsageTimeInMs();
mForegroundServiceUsageTimeInMs = batteryInformation.getForegroundServiceUsageTimeInMs();
mBackgroundUsageTimeInMs = batteryInformation.getBackgroundUsageTimeInMs();
mDrainType = batteryInformation.getDrainType();
final DeviceBatteryState deviceBatteryState = batteryInformation.getDeviceBatteryState();
mBatteryLevel = deviceBatteryState.getBatteryLevel();
mBatteryStatus = deviceBatteryState.getBatteryStatus();
mBatteryHealth = deviceBatteryState.getBatteryHealth();
}
public BatteryHistEntry(Cursor cursor) {
mUid = getLong(cursor, KEY_UID);
mUserId = getLong(cursor, KEY_USER_ID);
mPackageName = getString(cursor, KEY_PACKAGE_NAME);
mTimestamp = getLong(cursor, KEY_TIMESTAMP);
mConsumerType = getInteger(cursor, KEY_CONSUMER_TYPE);
final BatteryInformation batteryInformation =
ConvertUtils.getBatteryInformation(cursor, KEY_BATTERY_INFORMATION);
mAppLabel = batteryInformation.getAppLabel();
mIsHidden = batteryInformation.getIsHidden();
mBootTimestamp = batteryInformation.getBootTimestamp();
mZoneId = batteryInformation.getZoneId();
mTotalPower = batteryInformation.getTotalPower();
mConsumePower = batteryInformation.getConsumePower();
mForegroundUsageConsumePower = batteryInformation.getForegroundUsageConsumePower();
mForegroundServiceUsageConsumePower =
batteryInformation.getForegroundServiceUsageConsumePower();
mBackgroundUsageConsumePower = batteryInformation.getBackgroundUsageConsumePower();
mCachedUsageConsumePower = batteryInformation.getCachedUsageConsumePower();
mPercentOfTotal = batteryInformation.getPercentOfTotal();
mForegroundUsageTimeInMs = batteryInformation.getForegroundUsageTimeInMs();
mForegroundServiceUsageTimeInMs = batteryInformation.getForegroundServiceUsageTimeInMs();
mBackgroundUsageTimeInMs = batteryInformation.getBackgroundUsageTimeInMs();
mDrainType = batteryInformation.getDrainType();
final DeviceBatteryState deviceBatteryState = batteryInformation.getDeviceBatteryState();
mBatteryLevel = deviceBatteryState.getBatteryLevel();
mBatteryStatus = deviceBatteryState.getBatteryStatus();
mBatteryHealth = deviceBatteryState.getBatteryHealth();
}
private BatteryHistEntry(
BatteryHistEntry fromEntry,
long bootTimestamp,
long timestamp,
double totalPower,
double consumePower,
double foregroundUsageConsumePower,
double foregroundServiceUsageConsumePower,
double backgroundUsageConsumePower,
double cachedUsageConsumePower,
long foregroundUsageTimeInMs,
long foregroundServiceUsageTimeInMs,
long backgroundUsageTimeInMs,
int batteryLevel) {
mUid = fromEntry.mUid;
mUserId = fromEntry.mUserId;
mAppLabel = fromEntry.mAppLabel;
mPackageName = fromEntry.mPackageName;
mIsHidden = fromEntry.mIsHidden;
mBootTimestamp = bootTimestamp;
mTimestamp = timestamp;
mZoneId = fromEntry.mZoneId;
mTotalPower = totalPower;
mConsumePower = consumePower;
mForegroundUsageConsumePower = foregroundUsageConsumePower;
mForegroundServiceUsageConsumePower = foregroundServiceUsageConsumePower;
mBackgroundUsageConsumePower = backgroundUsageConsumePower;
mCachedUsageConsumePower = cachedUsageConsumePower;
mPercentOfTotal = fromEntry.mPercentOfTotal;
mForegroundUsageTimeInMs = foregroundUsageTimeInMs;
mForegroundServiceUsageTimeInMs = foregroundServiceUsageTimeInMs;
mBackgroundUsageTimeInMs = backgroundUsageTimeInMs;
mDrainType = fromEntry.mDrainType;
mConsumerType = fromEntry.mConsumerType;
mBatteryLevel = batteryLevel;
mBatteryStatus = fromEntry.mBatteryStatus;
mBatteryHealth = fromEntry.mBatteryHealth;
}
/** Whether this {@link BatteryHistEntry} is valid or not? */
public boolean isValidEntry() {
return mIsValidEntry;
}
/** Gets an identifier to represent this {@link BatteryHistEntry}. */
public String getKey() {
if (mKey == null) {
switch (mConsumerType) {
case ConvertUtils.CONSUMER_TYPE_UID_BATTERY:
mKey = Long.toString(mUid);
break;
case ConvertUtils.CONSUMER_TYPE_SYSTEM_BATTERY:
mKey = "S|" + mDrainType;
break;
case ConvertUtils.CONSUMER_TYPE_USER_BATTERY:
mKey = "U|" + mUserId;
break;
}
}
return mKey;
}
@Override
public String toString() {
final String recordAtDateTime = ConvertUtils.utcToLocalTimeForLogging(mTimestamp);
final StringBuilder builder = new StringBuilder();
builder.append("\nBatteryHistEntry{");
builder.append(
String.format(
"\n\tpackage=%s|label=%s|uid=%d|userId=%d|isHidden=%b",
mPackageName, mAppLabel, mUid, mUserId, mIsHidden));
builder.append(
String.format(
"\n\ttimestamp=%s|zoneId=%s|bootTimestamp=%d",
recordAtDateTime, mZoneId, TimestampUtils.getSeconds(mBootTimestamp)));
builder.append(
String.format(
"\n\tusage=%f|total=%f|consume=%f",
mPercentOfTotal, mTotalPower, mConsumePower));
builder.append(
String.format(
"\n\tforeground=%f|foregroundService=%f",
mForegroundUsageConsumePower, mForegroundServiceUsageConsumePower));
builder.append(
String.format(
"\n\tbackground=%f|cached=%f",
mBackgroundUsageConsumePower, mCachedUsageConsumePower));
builder.append(
String.format(
"\n\telapsedTime,fg=%d|fgs=%d|bg=%d",
TimestampUtils.getSeconds(mBackgroundUsageTimeInMs),
TimestampUtils.getSeconds(mForegroundServiceUsageTimeInMs),
TimestampUtils.getSeconds(mBackgroundUsageTimeInMs)));
builder.append(
String.format("\n\tdrainType=%d|consumerType=%d", mDrainType, mConsumerType));
builder.append(
String.format(
"\n\tbattery=%d|status=%d|health=%d\n}",
mBatteryLevel, mBatteryStatus, mBatteryHealth));
return builder.toString();
}
private int getInteger(ContentValues values, String key) {
if (values != null && values.containsKey(key)) {
return values.getAsInteger(key);
}
mIsValidEntry = false;
return 0;
}
private int getInteger(Cursor cursor, String key) {
final int columnIndex = cursor.getColumnIndex(key);
if (columnIndex >= 0) {
return cursor.getInt(columnIndex);
}
mIsValidEntry = false;
return 0;
}
private long getLong(ContentValues values, String key) {
if (values != null && values.containsKey(key)) {
return values.getAsLong(key);
}
mIsValidEntry = false;
return 0L;
}
private long getLong(Cursor cursor, String key) {
final int columnIndex = cursor.getColumnIndex(key);
if (columnIndex >= 0) {
return cursor.getLong(columnIndex);
}
mIsValidEntry = false;
return 0L;
}
private String getString(ContentValues values, String key) {
if (values != null && values.containsKey(key)) {
return values.getAsString(key);
}
mIsValidEntry = false;
return null;
}
private String getString(Cursor cursor, String key) {
final int columnIndex = cursor.getColumnIndex(key);
if (columnIndex >= 0) {
return cursor.getString(columnIndex);
}
mIsValidEntry = false;
return null;
}
/** Creates new {@link BatteryHistEntry} from interpolation. */
public static BatteryHistEntry interpolate(
long slotTimestamp,
long upperTimestamp,
double ratio,
BatteryHistEntry lowerHistEntry,
BatteryHistEntry upperHistEntry) {
final double totalPower =
interpolate(
lowerHistEntry == null ? 0 : lowerHistEntry.mTotalPower,
upperHistEntry.mTotalPower,
ratio);
final double consumePower =
interpolate(
lowerHistEntry == null ? 0 : lowerHistEntry.mConsumePower,
upperHistEntry.mConsumePower,
ratio);
final double foregroundUsageConsumePower =
interpolate(
lowerHistEntry == null ? 0 : lowerHistEntry.mForegroundUsageConsumePower,
upperHistEntry.mForegroundUsageConsumePower,
ratio);
final double foregroundServiceUsageConsumePower =
interpolate(
lowerHistEntry == null
? 0
: lowerHistEntry.mForegroundServiceUsageConsumePower,
upperHistEntry.mForegroundServiceUsageConsumePower,
ratio);
final double backgroundUsageConsumePower =
interpolate(
lowerHistEntry == null ? 0 : lowerHistEntry.mBackgroundUsageConsumePower,
upperHistEntry.mBackgroundUsageConsumePower,
ratio);
final double cachedUsageConsumePower =
interpolate(
lowerHistEntry == null ? 0 : lowerHistEntry.mCachedUsageConsumePower,
upperHistEntry.mCachedUsageConsumePower,
ratio);
final double foregroundUsageTimeInMs =
interpolate(
(lowerHistEntry == null ? 0 : lowerHistEntry.mForegroundUsageTimeInMs),
upperHistEntry.mForegroundUsageTimeInMs,
ratio);
final double foregroundServiceUsageTimeInMs =
interpolate(
(lowerHistEntry == null
? 0
: lowerHistEntry.mForegroundServiceUsageTimeInMs),
upperHistEntry.mForegroundServiceUsageTimeInMs,
ratio);
final double backgroundUsageTimeInMs =
interpolate(
(lowerHistEntry == null ? 0 : lowerHistEntry.mBackgroundUsageTimeInMs),
upperHistEntry.mBackgroundUsageTimeInMs,
ratio);
// Checks whether there is any abnormal cases!
if (upperHistEntry.mConsumePower < consumePower
|| upperHistEntry.mForegroundUsageConsumePower < foregroundUsageConsumePower
|| upperHistEntry.mForegroundServiceUsageConsumePower
< foregroundServiceUsageConsumePower
|| upperHistEntry.mBackgroundUsageConsumePower < backgroundUsageConsumePower
|| upperHistEntry.mCachedUsageConsumePower < cachedUsageConsumePower
|| upperHistEntry.mForegroundUsageTimeInMs < foregroundUsageTimeInMs
|| upperHistEntry.mForegroundServiceUsageTimeInMs < foregroundServiceUsageTimeInMs
|| upperHistEntry.mBackgroundUsageTimeInMs < backgroundUsageTimeInMs) {
if (DEBUG) {
Log.w(
TAG,
String.format(
"abnormal interpolation:\nupper:%s\nlower:%s",
upperHistEntry, lowerHistEntry));
}
}
final double batteryLevel =
lowerHistEntry == null
? upperHistEntry.mBatteryLevel
: interpolate(
lowerHistEntry.mBatteryLevel, upperHistEntry.mBatteryLevel, ratio);
return new BatteryHistEntry(
upperHistEntry,
/* bootTimestamp= */ upperHistEntry.mBootTimestamp
- (upperTimestamp - slotTimestamp),
/* timestamp= */ slotTimestamp,
totalPower,
consumePower,
foregroundUsageConsumePower,
foregroundServiceUsageConsumePower,
backgroundUsageConsumePower,
cachedUsageConsumePower,
Math.round(foregroundUsageTimeInMs),
Math.round(foregroundServiceUsageTimeInMs),
Math.round(backgroundUsageTimeInMs),
(int) Math.round(batteryLevel));
}
private static double interpolate(double v1, double v2, double ratio) {
return v1 + ratio * (v2 - v1);
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright (C) 2022 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.batteryusage;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.TextView;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settings.fuelgauge.BatteryUtils;
/** Custom preference for displaying the battery level as chart graph. */
public class BatteryHistoryPreference extends Preference {
private static final String TAG = "BatteryHistoryPreference";
private BatteryChartView mDailyChartView;
private BatteryChartView mHourlyChartView;
private BatteryChartPreferenceController mChartPreferenceController;
public BatteryHistoryPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setLayoutResource(R.layout.battery_chart_graph);
setSelectable(false);
}
void setChartPreferenceController(BatteryChartPreferenceController controller) {
mChartPreferenceController = controller;
if (mDailyChartView != null && mHourlyChartView != null) {
mChartPreferenceController.setBatteryChartView(mDailyChartView, mHourlyChartView);
}
}
@Override
public void onBindViewHolder(PreferenceViewHolder view) {
super.onBindViewHolder(view);
final long startTime = System.currentTimeMillis();
final TextView companionTextView = (TextView) view.findViewById(R.id.companion_text);
mDailyChartView = (BatteryChartView) view.findViewById(R.id.daily_battery_chart);
mDailyChartView.setCompanionTextView(companionTextView);
mHourlyChartView = (BatteryChartView) view.findViewById(R.id.hourly_battery_chart);
mHourlyChartView.setCompanionTextView(companionTextView);
if (mChartPreferenceController != null) {
mChartPreferenceController.setBatteryChartView(mDailyChartView, mHourlyChartView);
}
BatteryUtils.logRuntime(TAG, "onBindViewHolder", startTime);
}
}

View File

@@ -0,0 +1,215 @@
/*
* Copyright (C) 2022 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.batteryusage;
import static com.android.settingslib.fuelgauge.BatteryStatus.BATTERY_LEVEL_UNKNOWN;
import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.util.Preconditions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
/** Wraps the battery timestamp and level data used for battery usage chart. */
public final class BatteryLevelData {
private static final long MIN_SIZE = 2;
private static final long TIME_SLOT = DateUtils.HOUR_IN_MILLIS * 2;
/** A container for the battery timestamp and level data. */
public static final class PeriodBatteryLevelData {
// The length of mTimestamps and mLevels must be the same. mLevels[index] might be null when
// there is no level data for the corresponding timestamp.
private final List<Long> mTimestamps;
private final List<Integer> mLevels;
public PeriodBatteryLevelData(
@NonNull Map<Long, Integer> batteryLevelMap, @NonNull List<Long> timestamps) {
mTimestamps = timestamps;
mLevels = new ArrayList<>(timestamps.size());
for (Long timestamp : timestamps) {
mLevels.add(
batteryLevelMap.containsKey(timestamp)
? batteryLevelMap.get(timestamp)
: BATTERY_LEVEL_UNKNOWN);
}
}
public List<Long> getTimestamps() {
return mTimestamps;
}
public List<Integer> getLevels() {
return mLevels;
}
@Override
public String toString() {
return String.format(
Locale.ENGLISH,
"timestamps: %s; levels: %s",
Objects.toString(mTimestamps),
Objects.toString(mLevels));
}
private int getIndexByTimestamps(long startTimestamp, long endTimestamp) {
for (int index = 0; index < mTimestamps.size() - 1; index++) {
if (mTimestamps.get(index) <= startTimestamp
&& endTimestamp <= mTimestamps.get(index + 1)) {
return index;
}
}
return BatteryChartViewModel.SELECTED_INDEX_INVALID;
}
}
/**
* There could be 2 cases for the daily battery levels: <br>
* 1) length is 2: The usage data is within 1 day. Only contains start and end data, such as
* data of 2022-01-01 06:00 and 2022-01-01 16:00. <br>
* 2) length > 2: The usage data is more than 1 days. The data should be the start, end and 0am
* data of every day between the start and end, such as data of 2022-01-01 06:00, 2022-01-02
* 00:00, 2022-01-03 00:00 and 2022-01-03 08:00.
*/
private final PeriodBatteryLevelData mDailyBatteryLevels;
// The size of hourly data must be the size of daily data - 1.
private final List<PeriodBatteryLevelData> mHourlyBatteryLevelsPerDay;
public BatteryLevelData(@NonNull Map<Long, Integer> batteryLevelMap) {
final int mapSize = batteryLevelMap.size();
Preconditions.checkArgument(mapSize >= MIN_SIZE, "batteryLevelMap size:" + mapSize);
final List<Long> timestampList = new ArrayList<>(batteryLevelMap.keySet());
Collections.sort(timestampList);
final List<Long> dailyTimestamps = getDailyTimestamps(timestampList);
final List<List<Long>> hourlyTimestamps = getHourlyTimestamps(dailyTimestamps);
mDailyBatteryLevels = new PeriodBatteryLevelData(batteryLevelMap, dailyTimestamps);
mHourlyBatteryLevelsPerDay = new ArrayList<>(hourlyTimestamps.size());
for (List<Long> hourlyTimestampsPerDay : hourlyTimestamps) {
mHourlyBatteryLevelsPerDay.add(
new PeriodBatteryLevelData(batteryLevelMap, hourlyTimestampsPerDay));
}
}
/** Gets daily and hourly index between start and end timestamps. */
public Pair<Integer, Integer> getIndexByTimestamps(long startTimestamp, long endTimestamp) {
final int dailyHighlightIndex =
mDailyBatteryLevels.getIndexByTimestamps(startTimestamp, endTimestamp);
final int hourlyHighlightIndex =
(dailyHighlightIndex == BatteryChartViewModel.SELECTED_INDEX_INVALID)
? BatteryChartViewModel.SELECTED_INDEX_INVALID
: mHourlyBatteryLevelsPerDay
.get(dailyHighlightIndex)
.getIndexByTimestamps(startTimestamp, endTimestamp);
return Pair.create(dailyHighlightIndex, hourlyHighlightIndex);
}
public PeriodBatteryLevelData getDailyBatteryLevels() {
return mDailyBatteryLevels;
}
public List<PeriodBatteryLevelData> getHourlyBatteryLevelsPerDay() {
return mHourlyBatteryLevelsPerDay;
}
@Override
public String toString() {
return String.format(
Locale.ENGLISH,
"dailyBatteryLevels: %s; hourlyBatteryLevelsPerDay: %s",
Objects.toString(mDailyBatteryLevels),
Objects.toString(mHourlyBatteryLevelsPerDay));
}
@Nullable
static BatteryLevelData combine(
@Nullable BatteryLevelData existingBatteryLevelData,
List<BatteryEvent> batteryLevelRecordEvents) {
final Map<Long, Integer> batteryLevelMap = new ArrayMap<>(batteryLevelRecordEvents.size());
for (BatteryEvent event : batteryLevelRecordEvents) {
batteryLevelMap.put(event.getTimestamp(), event.getBatteryLevel());
}
if (existingBatteryLevelData != null) {
List<PeriodBatteryLevelData> multiDaysData =
existingBatteryLevelData.getHourlyBatteryLevelsPerDay();
for (int dayIndex = 0; dayIndex < multiDaysData.size(); dayIndex++) {
PeriodBatteryLevelData oneDayData = multiDaysData.get(dayIndex);
for (int hourIndex = 0; hourIndex < oneDayData.getLevels().size(); hourIndex++) {
batteryLevelMap.put(
oneDayData.getTimestamps().get(hourIndex),
oneDayData.getLevels().get(hourIndex));
}
}
}
return batteryLevelMap.size() < MIN_SIZE ? null : new BatteryLevelData(batteryLevelMap);
}
/**
* Computes expected daily timestamp slots.
*
* <p>The valid result should be composed of 3 parts: <br>
* 1) start timestamp <br>
* 2) every 00:00 timestamp (default timezone) between the start and end <br>
* 3) end timestamp Otherwise, returns an empty list.
*/
@VisibleForTesting
static List<Long> getDailyTimestamps(final List<Long> timestampList) {
Preconditions.checkArgument(
timestampList.size() >= MIN_SIZE, "timestampList size:" + timestampList.size());
final List<Long> dailyTimestampList = new ArrayList<>();
final long startTimestamp = timestampList.get(0);
final long endTimestamp = timestampList.get(timestampList.size() - 1);
for (long timestamp = startTimestamp;
timestamp < endTimestamp;
timestamp = TimestampUtils.getNextDayTimestamp(timestamp)) {
dailyTimestampList.add(timestamp);
}
dailyTimestampList.add(endTimestamp);
return dailyTimestampList;
}
private static List<List<Long>> getHourlyTimestamps(final List<Long> dailyTimestamps) {
final List<List<Long>> hourlyTimestamps = new ArrayList<>();
for (int dailyIndex = 0; dailyIndex < dailyTimestamps.size() - 1; dailyIndex++) {
final List<Long> hourlyTimestampsPerDay = new ArrayList<>();
final long startTime = dailyTimestamps.get(dailyIndex);
final long endTime = dailyTimestamps.get(dailyIndex + 1);
hourlyTimestampsPerDay.add(startTime);
for (long timestamp = TimestampUtils.getNextEvenHourTimestamp(startTime);
timestamp < endTime;
timestamp += TIME_SLOT) {
hourlyTimestampsPerDay.add(timestamp);
}
hourlyTimestampsPerDay.add(endTime);
hourlyTimestamps.add(hourlyTimestampsPerDay);
}
return hourlyTimestamps;
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright (C) 2023 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.batteryusage;
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.google.android.material.button.MaterialButton;
/** A preference for displaying the battery tips card view. */
public class BatteryTipsCardPreference extends Preference implements View.OnClickListener {
private static final String TAG = "BatteryTipsCardPreference";
interface OnConfirmListener {
void onConfirm();
}
interface OnRejectListener {
void onReject();
}
private OnConfirmListener mOnConfirmListener;
private OnRejectListener mOnRejectListener;
private int mIconResourceId = 0;
private int mButtonColorResourceId = 0;
@VisibleForTesting CharSequence mMainButtonLabel;
@VisibleForTesting CharSequence mDismissButtonLabel;
public BatteryTipsCardPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setLayoutResource(R.layout.battery_tips_card);
setViewId(R.id.battery_tips_card);
setSelectable(false);
}
public void setOnConfirmListener(OnConfirmListener listener) {
mOnConfirmListener = listener;
}
public void setOnRejectListener(OnRejectListener listener) {
mOnRejectListener = listener;
}
/**
* Sets the icon in tips card.
*/
public void setIconResourceId(int resourceId) {
if (mIconResourceId != resourceId) {
mIconResourceId = resourceId;
notifyChanged();
}
}
/**
* Sets the background color for main button and the text color for dismiss button.
*/
public void setButtonColorResourceId(int resourceId) {
if (mButtonColorResourceId != resourceId) {
mButtonColorResourceId = resourceId;
notifyChanged();
}
}
/**
* Sets the label of main button in tips card.
*/
public void setMainButtonLabel(CharSequence label) {
if (!TextUtils.equals(mMainButtonLabel, label)) {
mMainButtonLabel = label;
notifyChanged();
}
}
/**
* Sets the label of dismiss button in tips card.
*/
public void setDismissButtonLabel(CharSequence label) {
if (!TextUtils.equals(mDismissButtonLabel, label)) {
mDismissButtonLabel = label;
notifyChanged();
}
}
@Override
public void onClick(View view) {
final int viewId = view.getId();
if (viewId == R.id.main_button || viewId == R.id.battery_tips_card) {
if (mOnConfirmListener != null) {
mOnConfirmListener.onConfirm();
}
} else if (viewId == R.id.dismiss_button) {
if (mOnRejectListener != null) {
mOnRejectListener.onReject();
}
}
}
@Override
public void onBindViewHolder(PreferenceViewHolder view) {
super.onBindViewHolder(view);
((TextView) view.findViewById(R.id.title)).setText(getTitle());
final LinearLayout tipsCard = (LinearLayout) view.findViewById(R.id.battery_tips_card);
tipsCard.setOnClickListener(this);
final MaterialButton mainButton = (MaterialButton) view.findViewById(R.id.main_button);
mainButton.setOnClickListener(this);
mainButton.setText(mMainButtonLabel);
final MaterialButton dismissButton =
(MaterialButton) view.findViewById(R.id.dismiss_button);
dismissButton.setOnClickListener(this);
dismissButton.setText(mDismissButtonLabel);
if (mButtonColorResourceId != 0) {
final int colorInt = getContext().getColor(mButtonColorResourceId);
mainButton.setBackgroundColor(colorInt);
dismissButton.setTextColor(colorInt);
}
if (mIconResourceId != 0) {
((ImageView) view.findViewById(R.id.icon)).setImageResource(mIconResourceId);
}
}
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright (C) 2023 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.batteryusage;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.text.TextUtils;
import androidx.preference.PreferenceScreen;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
/** Controls the update for battery tips card */
public class BatteryTipsController extends BasePreferenceController {
private static final String TAG = "BatteryTipsController";
private static final String ROOT_PREFERENCE_KEY = "battery_tips_category";
private static final String CARD_PREFERENCE_KEY = "battery_tips_card";
private final MetricsFeatureProvider mMetricsFeatureProvider;
/** A callback listener for the battery tips is confirmed. */
interface OnAnomalyConfirmListener {
/** The callback function for the battery tips is confirmed. */
void onAnomalyConfirm();
}
/** A callback listener for the battery tips is rejected. */
interface OnAnomalyRejectListener {
/** The callback function for the battery tips is rejected. */
void onAnomalyReject();
}
private OnAnomalyConfirmListener mOnAnomalyConfirmListener;
private OnAnomalyRejectListener mOnAnomalyRejectListener;
@VisibleForTesting BatteryTipsCardPreference mCardPreference;
@VisibleForTesting AnomalyEventWrapper mAnomalyEventWrapper = null;
@VisibleForTesting Boolean mIsAcceptable = false;
public BatteryTipsController(Context context) {
super(context, ROOT_PREFERENCE_KEY);
final FeatureFactory featureFactory = FeatureFactory.getFeatureFactory();
mMetricsFeatureProvider = featureFactory.getMetricsFeatureProvider();
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mCardPreference = screen.findPreference(CARD_PREFERENCE_KEY);
}
void setOnAnomalyConfirmListener(OnAnomalyConfirmListener listener) {
mOnAnomalyConfirmListener = listener;
}
void setOnAnomalyRejectListener(OnAnomalyRejectListener listener) {
mOnAnomalyRejectListener = listener;
}
void acceptTipsCard() {
if (mAnomalyEventWrapper == null || !mIsAcceptable) {
return;
}
// For anomaly events with same record key, dismissed until next time full charged.
final String dismissRecordKey = mAnomalyEventWrapper.getDismissRecordKey();
if (!TextUtils.isEmpty(dismissRecordKey)) {
DatabaseUtils.setDismissedPowerAnomalyKeys(mContext, dismissRecordKey);
}
mCardPreference.setVisible(false);
mMetricsFeatureProvider.action(
/* attribution= */ SettingsEnums.FUELGAUGE_BATTERY_HISTORY_DETAIL,
/* action= */ SettingsEnums.ACTION_BATTERY_TIPS_CARD_ACCEPT,
/* pageId= */ SettingsEnums.FUELGAUGE_BATTERY_HISTORY_DETAIL,
/* key= */ mAnomalyEventWrapper.getEventId(),
/* value= */ mAnomalyEventWrapper.getAnomalyKeyNumber());
}
void handleBatteryTipsCardUpdated(
AnomalyEventWrapper anomalyEventWrapper, boolean isAcceptable) {
mAnomalyEventWrapper = anomalyEventWrapper;
mIsAcceptable = isAcceptable;
if (mAnomalyEventWrapper == null) {
mCardPreference.setVisible(false);
return;
}
final String eventId = mAnomalyEventWrapper.getEventId();
final int anomalyKeyNumber = mAnomalyEventWrapper.getAnomalyKeyNumber();
// Update card & buttons preference
if (!mAnomalyEventWrapper.updateTipsCardPreference(mCardPreference)) {
mCardPreference.setVisible(false);
return;
}
// Set battery tips card listener
mCardPreference.setOnConfirmListener(
() -> {
mCardPreference.setVisible(false);
if (mOnAnomalyConfirmListener != null) {
mOnAnomalyConfirmListener.onAnomalyConfirm();
} else if (mAnomalyEventWrapper.launchSubSetting()) {
mMetricsFeatureProvider.action(
/* attribution= */ SettingsEnums.FUELGAUGE_BATTERY_HISTORY_DETAIL,
/* action= */ SettingsEnums.ACTION_BATTERY_TIPS_CARD_ACCEPT,
/* pageId= */ SettingsEnums.FUELGAUGE_BATTERY_HISTORY_DETAIL,
/* key= */ eventId,
/* value= */ anomalyKeyNumber);
}
});
mCardPreference.setOnRejectListener(
() -> {
mCardPreference.setVisible(false);
if (mOnAnomalyRejectListener != null) {
mOnAnomalyRejectListener.onAnomalyReject();
}
// For anomaly events with same record key, dismissed until next time full
// charged.
final String dismissRecordKey = mAnomalyEventWrapper.getDismissRecordKey();
if (!TextUtils.isEmpty(dismissRecordKey)) {
DatabaseUtils.setDismissedPowerAnomalyKeys(mContext, dismissRecordKey);
}
mMetricsFeatureProvider.action(
/* attribution= */ SettingsEnums.FUELGAUGE_BATTERY_HISTORY_DETAIL,
/* action= */ SettingsEnums.ACTION_BATTERY_TIPS_CARD_DISMISS,
/* pageId= */ SettingsEnums.FUELGAUGE_BATTERY_HISTORY_DETAIL,
/* key= */ eventId,
/* value= */ anomalyKeyNumber);
});
mCardPreference.setVisible(true);
mMetricsFeatureProvider.action(
/* attribution= */ SettingsEnums.FUELGAUGE_BATTERY_HISTORY_DETAIL,
/* action= */ SettingsEnums.ACTION_BATTERY_TIPS_CARD_SHOW,
/* pageId= */ SettingsEnums.FUELGAUGE_BATTERY_HISTORY_DETAIL,
/* key= */ eventId,
/* value= */ anomalyKeyNumber);
}
}

View File

@@ -0,0 +1,415 @@
/*
* Copyright (C) 2022 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.batteryusage;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.Utils;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settings.fuelgauge.AdvancedPowerUsageDetail;
import com.android.settings.fuelgauge.BatteryUtils;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
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.OnResume;
import com.android.settingslib.widget.FooterPreference;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/** Controller for battery usage breakdown preference group. */
public class BatteryUsageBreakdownController extends BasePreferenceController
implements LifecycleObserver, OnResume, OnDestroy {
private static final String TAG = "BatteryUsageBreakdownController";
private static final String ROOT_PREFERENCE_KEY = "battery_usage_breakdown";
private static final String FOOTER_PREFERENCE_KEY = "battery_usage_footer";
private static final String SPINNER_PREFERENCE_KEY = "battery_usage_spinner";
private static final String APP_LIST_PREFERENCE_KEY = "app_list";
private static final String PACKAGE_NAME_NONE = "none";
private static final String SLOT_TIMESTAMP = "slot_timestamp";
private static final String ANOMALY_KEY = "anomaly_key";
private static final List<BatteryDiffEntry> EMPTY_ENTRY_LIST = new ArrayList<>();
private static int sUiMode = Configuration.UI_MODE_NIGHT_UNDEFINED;
private final SettingsActivity mActivity;
private final InstrumentedPreferenceFragment mFragment;
private final MetricsFeatureProvider mMetricsFeatureProvider;
private final Handler mHandler = new Handler(Looper.getMainLooper());
@VisibleForTesting final Map<String, Preference> mPreferenceCache = new ArrayMap<>();
private int mSpinnerPosition;
private String mSlotInformation;
@VisibleForTesting Context mPrefContext;
@VisibleForTesting PreferenceCategory mRootPreference;
@VisibleForTesting SpinnerPreference mSpinnerPreference;
@VisibleForTesting PreferenceGroup mAppListPreferenceGroup;
@VisibleForTesting FooterPreference mFooterPreference;
@VisibleForTesting BatteryDiffData mBatteryDiffData;
@VisibleForTesting String mPercentLessThanThresholdText;
@VisibleForTesting boolean mIsHighlightSlot;
@VisibleForTesting int mAnomalyKeyNumber;
@VisibleForTesting String mAnomalyEntryKey;
@VisibleForTesting String mAnomalyHintString;
@VisibleForTesting String mAnomalyHintPrefKey;
public BatteryUsageBreakdownController(
Context context,
Lifecycle lifecycle,
SettingsActivity activity,
InstrumentedPreferenceFragment fragment) {
super(context, ROOT_PREFERENCE_KEY);
mActivity = activity;
mFragment = fragment;
mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider();
if (lifecycle != null) {
lifecycle.addObserver(this);
}
}
@Override
public void onResume() {
final int currentUiMode =
mContext.getResources().getConfiguration().uiMode
& Configuration.UI_MODE_NIGHT_MASK;
if (sUiMode != currentUiMode) {
sUiMode = currentUiMode;
BatteryDiffEntry.clearCache();
mPreferenceCache.clear();
Log.d(TAG, "clear icon and label cache since uiMode is changed");
}
}
@Override
public void onDestroy() {
mHandler.removeCallbacksAndMessages(/* token= */ null);
mPreferenceCache.clear();
mAppListPreferenceGroup.removeAll();
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE;
}
@Override
public boolean isSliceable() {
return false;
}
private boolean isAnomalyBatteryDiffEntry(BatteryDiffEntry entry) {
return mIsHighlightSlot
&& mAnomalyEntryKey != null
&& mAnomalyEntryKey.equals(entry.getKey());
}
private void logPreferenceClickedMetrics(BatteryDiffEntry entry) {
final int attribution = SettingsEnums.OPEN_BATTERY_USAGE;
final int action = entry.isSystemEntry()
? SettingsEnums.ACTION_BATTERY_USAGE_SYSTEM_ITEM
: SettingsEnums.ACTION_BATTERY_USAGE_APP_ITEM;
final int pageId = SettingsEnums.OPEN_BATTERY_USAGE;
final String packageName =
TextUtils.isEmpty(entry.getPackageName())
? PACKAGE_NAME_NONE
: entry.getPackageName();
final int percentage = (int) Math.round(entry.getPercentage());
final int slotTimestamp = (int) (mBatteryDiffData.getStartTimestamp() / 1000);
mMetricsFeatureProvider.action(attribution, action, pageId, packageName, percentage);
mMetricsFeatureProvider.action(attribution, action, pageId, SLOT_TIMESTAMP, slotTimestamp);
if (isAnomalyBatteryDiffEntry(entry)) {
mMetricsFeatureProvider.action(
attribution, action, pageId, ANOMALY_KEY, mAnomalyKeyNumber);
}
}
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
if (!(preference instanceof PowerGaugePreference)) {
return false;
}
final PowerGaugePreference powerPref = (PowerGaugePreference) preference;
final BatteryDiffEntry diffEntry = powerPref.getBatteryDiffEntry();
logPreferenceClickedMetrics(diffEntry);
Log.d(
TAG,
String.format(
"handleClick() label=%s key=%s package=%s",
diffEntry.getAppLabel(), diffEntry.getKey(), diffEntry.getPackageName()));
final String anomalyHintPrefKey =
isAnomalyBatteryDiffEntry(diffEntry) ? mAnomalyHintPrefKey : null;
final String anomalyHintText =
isAnomalyBatteryDiffEntry(diffEntry) ? mAnomalyHintString : null;
AdvancedPowerUsageDetail.startBatteryDetailPage(
mActivity,
mFragment.getMetricsCategory(),
diffEntry,
powerPref.getPercentage(),
mSlotInformation,
/* showTimeInformation= */ true,
anomalyHintPrefKey,
anomalyHintText);
return true;
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPrefContext = screen.getContext();
mRootPreference = screen.findPreference(ROOT_PREFERENCE_KEY);
mSpinnerPreference = screen.findPreference(SPINNER_PREFERENCE_KEY);
mAppListPreferenceGroup = screen.findPreference(APP_LIST_PREFERENCE_KEY);
mFooterPreference = screen.findPreference(FOOTER_PREFERENCE_KEY);
mPercentLessThanThresholdText =
mPrefContext.getString(
R.string.battery_usage_less_than_percent,
Utils.formatPercentage(BatteryDiffData.SMALL_PERCENTAGE_THRESHOLD, false));
mAppListPreferenceGroup.setOrderingAsAdded(false);
mSpinnerPreference.initializeSpinner(
new String[] {
mPrefContext.getString(R.string.battery_usage_spinner_view_by_apps),
mPrefContext.getString(R.string.battery_usage_spinner_view_by_systems)
},
new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(
AdapterView<?> parent, View view, int position, long id) {
if (mSpinnerPosition != position) {
mSpinnerPosition = position;
mHandler.post(
() -> {
removeAndCacheAllUnusedPreferences();
addAllPreferences();
mMetricsFeatureProvider.action(
mPrefContext,
SettingsEnums.ACTION_BATTERY_USAGE_SPINNER,
mSpinnerPosition);
});
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
}
/**
* Updates UI when the battery usage is updated.
*
* @param slotUsageData The battery usage diff data for the selected slot. This is used in the
* app list.
* @param slotTimestamp The selected slot timestamp information. This is used in the battery
* usage breakdown category.
* @param isAllUsageDataEmpty Whether all the battery usage data is null or empty. This is used
* when showing the footer.
*/
void handleBatteryUsageUpdated(
BatteryDiffData slotUsageData,
String slotTimestamp,
boolean isAllUsageDataEmpty,
boolean isHighlightSlot,
Optional<AnomalyEventWrapper> optionalAnomalyEventWrapper) {
mBatteryDiffData = slotUsageData;
mSlotInformation = slotTimestamp;
mIsHighlightSlot = isHighlightSlot;
if (optionalAnomalyEventWrapper != null) {
final AnomalyEventWrapper anomalyEventWrapper =
optionalAnomalyEventWrapper.orElse(null);
mAnomalyKeyNumber =
anomalyEventWrapper != null ? anomalyEventWrapper.getAnomalyKeyNumber() : -1;
mAnomalyEntryKey =
anomalyEventWrapper != null ? anomalyEventWrapper.getAnomalyEntryKey() : null;
mAnomalyHintString =
anomalyEventWrapper != null ? anomalyEventWrapper.getAnomalyHintString() : null;
mAnomalyHintPrefKey =
anomalyEventWrapper != null
? anomalyEventWrapper.getAnomalyHintPrefKey()
: null;
}
showCategoryTitle(slotTimestamp);
showSpinnerAndAppList();
showFooterPreference(isAllUsageDataEmpty);
}
private void showCategoryTitle(String slotTimestamp) {
mRootPreference.setTitle(
slotTimestamp == null
? mPrefContext.getString(
R.string.battery_usage_breakdown_title_since_last_full_charge)
: mPrefContext.getString(
R.string.battery_usage_breakdown_title_for_slot, slotTimestamp));
mRootPreference.setVisible(true);
}
private void showFooterPreference(boolean isAllBatteryUsageEmpty) {
mFooterPreference.setTitle(
mPrefContext.getString(
isAllBatteryUsageEmpty
? R.string.battery_usage_screen_footer_empty
: R.string.battery_usage_screen_footer));
mFooterPreference.setVisible(true);
}
private void showSpinnerAndAppList() {
if (mBatteryDiffData == null) {
mHandler.post(
() -> {
removeAndCacheAllUnusedPreferences();
});
return;
}
mSpinnerPreference.setVisible(true);
mAppListPreferenceGroup.setVisible(true);
mHandler.post(
() -> {
removeAndCacheAllUnusedPreferences();
addAllPreferences();
});
}
private List<BatteryDiffEntry> getBatteryDiffEntries() {
if (mBatteryDiffData == null) {
return EMPTY_ENTRY_LIST;
}
return mSpinnerPosition == 0
? mBatteryDiffData.getAppDiffEntryList()
: mBatteryDiffData.getSystemDiffEntryList();
}
@VisibleForTesting
void addAllPreferences() {
if (mBatteryDiffData == null) {
return;
}
final long start = System.currentTimeMillis();
final List<BatteryDiffEntry> entries = getBatteryDiffEntries();
int prefIndex = mAppListPreferenceGroup.getPreferenceCount();
for (BatteryDiffEntry entry : entries) {
boolean isAdded = false;
final String appLabel = entry.getAppLabel();
final Drawable appIcon = entry.getAppIcon();
if (TextUtils.isEmpty(appLabel) || appIcon == null) {
Log.w(TAG, "cannot find app resource for:" + entry.getPackageName());
continue;
}
final String prefKey = entry.getKey();
AnomalyAppItemPreference pref = mAppListPreferenceGroup.findPreference(prefKey);
if (pref != null) {
isAdded = true;
} else {
pref = (AnomalyAppItemPreference) mPreferenceCache.get(prefKey);
}
// Creates new instance if cached preference is not found.
if (pref == null) {
pref = new AnomalyAppItemPreference(mPrefContext);
pref.setKey(prefKey);
mPreferenceCache.put(prefKey, pref);
}
pref.setIcon(appIcon);
pref.setTitle(appLabel);
pref.setOrder(prefIndex);
pref.setSingleLineTitle(true);
// Updates App item preference style
pref.setAnomalyHint(isAnomalyBatteryDiffEntry(entry) ? mAnomalyHintString : null);
// Sets the BatteryDiffEntry to preference for launching detailed page.
pref.setBatteryDiffEntry(entry);
pref.setSelectable(entry.validForRestriction());
setPreferencePercentage(pref, entry);
setPreferenceSummary(pref, entry);
if (!isAdded) {
mAppListPreferenceGroup.addPreference(pref);
}
prefIndex++;
}
Log.d(
TAG,
String.format(
"addAllPreferences() is finished in %d/ms",
(System.currentTimeMillis() - start)));
}
@VisibleForTesting
void removeAndCacheAllUnusedPreferences() {
List<BatteryDiffEntry> entries = getBatteryDiffEntries();
Set<String> entryKeySet = new ArraySet<>(entries.size());
entries.forEach(entry -> entryKeySet.add(entry.getKey()));
final int prefsCount = mAppListPreferenceGroup.getPreferenceCount();
for (int index = prefsCount - 1; index >= 0; index--) {
final Preference pref = mAppListPreferenceGroup.getPreference(index);
if (entryKeySet.contains(pref.getKey())) {
// The pref is still used, don't remove.
continue;
}
if (!TextUtils.isEmpty(pref.getKey())) {
mPreferenceCache.put(pref.getKey(), pref);
}
mAppListPreferenceGroup.removePreference(pref);
}
}
@VisibleForTesting
void setPreferencePercentage(PowerGaugePreference preference, BatteryDiffEntry entry) {
preference.setPercentage(
entry.getPercentage() < BatteryDiffData.SMALL_PERCENTAGE_THRESHOLD
? mPercentLessThanThresholdText
: Utils.formatPercentage(
entry.getPercentage() + entry.getAdjustPercentageOffset(),
/* round= */ true));
}
@VisibleForTesting
void setPreferenceSummary(PowerGaugePreference preference, BatteryDiffEntry entry) {
preference.setSummary(
BatteryUtils.buildBatteryUsageTimeSummary(
mPrefContext,
entry.isSystemEntry(),
entry.mForegroundUsageTimeInMs,
entry.mBackgroundUsageTimeInMs + entry.mForegroundServiceUsageTimeInMs,
entry.mScreenOnTimeInMs));
}
}

Some files were not shown because too many files have changed in this diff Show More