sdk: Implement Lineage health service

Change-Id: I772ccf6d323c24d681aa8468bf4318c7b73bd3f5
This commit is contained in:
Luofan Chen
2023-03-01 19:12:53 +08:00
committed by Michael Bestas
parent 89941a9622
commit 3ee210210d
12 changed files with 1525 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
//
// Copyright (C) 2018-2022 The LineageOS Project
// Copyright (C) 2018-2023 The LineageOS Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -66,6 +66,7 @@ lineage_sdk_internal_src = "sdk/src/java/org/lineageos/internal"
library_src = "lineage/lib/main/java"
lineage_sdk_LOCAL_STATIC_JAVA_LIBRARIES = [
"vendor.lineage.health-V1-java",
"vendor.lineage.livedisplay-V2.0-java",
"vendor.lineage.livedisplay-V2.1-java",
"vendor.lineage.touch-V1.0-java",

View File

@@ -0,0 +1,873 @@
/*
* Copyright (C) 2023 The LineageOS 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 org.lineageos.platform.internal.health;
import static java.time.format.FormatStyle.SHORT;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.BatteryStatsManager;
import android.os.BatteryUsageStats;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.text.format.DateUtils;
import android.util.Log;
import org.lineageos.platform.internal.R;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Calendar;
import lineageos.providers.LineageSettings;
import vendor.lineage.health.ChargingControlSupportedMode;
import vendor.lineage.health.IChargingControl;
import static lineageos.health.HealthInterface.MODE_NONE;
import static lineageos.health.HealthInterface.MODE_AUTO;
import static lineageos.health.HealthInterface.MODE_MANUAL;
import static lineageos.health.HealthInterface.MODE_LIMIT;
public class ChargingControlController extends LineageHealthFeature {
private final IChargingControl mChargingControl;
private final ContentResolver mContentResolver;
private final ChargingControlNotification mChargingNotification;
private LineageHealthBatteryBroadcastReceiver mBattReceiver;
// Defaults
private final boolean mDefaultEnabled;
private final int mDefaultMode;
private final int mDefaultLimit;
private final int mDefaultStartTime;
private final int mDefaultTargetTime;
// User configs
private boolean mConfigEnabled = false;
private int mConfigMode = MODE_NONE;
private int mConfigLimit = 100;
private int mConfigStartTime = 0;
private int mConfigTargetTime = 0;
// Settings uris
private final Uri MODE_URI = LineageSettings.System.getUriFor(
LineageSettings.System.CHARGING_CONTROL_MODE);
private final Uri LIMIT_URI = LineageSettings.System.getUriFor(
LineageSettings.System.CHARGING_CONTROL_LIMIT);
private final Uri ENABLED_URI = LineageSettings.System.getUriFor(
LineageSettings.System.CHARGING_CONTROL_ENABLED);
private final Uri START_TIME_URI = LineageSettings.System.getUriFor(
LineageSettings.System.CHARGING_CONTROL_START_TIME);
private final Uri TARGET_TIME_URI = LineageSettings.System.getUriFor(
LineageSettings.System.CHARGING_CONTROL_TARGET_TIME);
// Internal state
private float mBatteryPct = 0;
private boolean mIsPowerConnected = false;
private int mChargingStopReason = 0;
private long mEstimatedFullTime = 0;
private long mSavedAlarmTime = 0;
private long mSavedTargetTime = 0;
private boolean mIsControlCancelledOnce = false;
private final boolean mIsChargingToggleSupported;
private final boolean mIsChargingBypassSupported;
private final boolean mIsChargingDeadlineSupported;
private final int mChargingTimeMargin;
private final int mChargingLimitMargin;
private static final DateTimeFormatter mFormatter = DateTimeFormatter.ofLocalizedTime(SHORT);
private static final SimpleDateFormat mDateFormatter = new SimpleDateFormat("hh:mm:ss a");
// Only when the battery level is above this limit will the charging control be activated.
private static int CHARGE_CTRL_MIN_LEVEL = 80;
private static final String INTENT_PARTS =
"org.lineageos.lineageparts.CHARGING_CONTROL_SETTINGS";
private static class ChargingStopReason {
private static int BIT(int shift) {
return 1 << shift;
}
/**
* No stop charging
*/
public static final int NONE = 0;
/**
* The charging stopped because it reaches limit
*/
public static final int REACH_LIMIT = BIT(0);
/**
* The charging stopped because the battery level is decent, and we are waiting to resume
* charging when the time approaches the target time.
*/
public static final int WAITING = BIT(1);
}
public ChargingControlController(Context context, Handler handler) {
super(context, handler);
mContentResolver = mContext.getContentResolver();
mChargingControl = IChargingControl.Stub.asInterface(
ServiceManager.getService(IChargingControl.DESCRIPTOR + "/default"));
if (mChargingControl == null) {
Log.i(TAG, "Lineage Health HAL not found");
}
mChargingNotification = new ChargingControlNotification(context);
mChargingTimeMargin = mContext.getResources().getInteger(
R.integer.config_chargingControlTimeMargin) * 60 * 1000;
mChargingLimitMargin = mContext.getResources().getInteger(
R.integer.config_chargingControlBatteryRechargeMargin);
mDefaultEnabled = mContext.getResources().getBoolean(
R.bool.config_chargingControlEnabled);
mDefaultMode = mContext.getResources().getInteger(
R.integer.config_defaultChargingControlMode);
mDefaultStartTime = mContext.getResources().getInteger(
R.integer.config_defaultChargingControlStartTime);
mDefaultTargetTime = mContext.getResources().getInteger(
R.integer.config_defaultChargingControlTargetTime);
mDefaultLimit = mContext.getResources().getInteger(
R.integer.config_defaultChargingControlLimit);
mIsChargingToggleSupported = isChargingModeSupported(ChargingControlSupportedMode.TOGGLE);
mIsChargingBypassSupported = isChargingModeSupported(ChargingControlSupportedMode.BYPASS);
mIsChargingDeadlineSupported = isChargingModeSupported(
ChargingControlSupportedMode.DEADLINE);
}
@Override
public boolean isSupported() {
return mChargingControl != null;
}
public boolean isEnabled() {
return mConfigEnabled;
}
public boolean setEnabled(boolean enabled) {
putBoolean(LineageSettings.System.CHARGING_CONTROL_ENABLED, enabled);
return true;
}
public int getMode() {
return mConfigMode;
}
public boolean setMode(int mode) {
if (mode < MODE_NONE || mode > MODE_LIMIT) {
return false;
}
putInt(LineageSettings.System.CHARGING_CONTROL_MODE, mode);
return true;
}
public int getStartTime() {
return mConfigStartTime;
}
public boolean setStartTime(int time) {
if (time < 0 || time > 24 * 60 * 60) {
return false;
}
putInt(LineageSettings.System.CHARGING_CONTROL_START_TIME, time);
return true;
}
public int getTargetTime() {
return mConfigTargetTime;
}
public boolean setTargetTime(int time) {
if (time < 0 || time > 24 * 60 * 60) {
return false;
}
putInt(LineageSettings.System.CHARGING_CONTROL_TARGET_TIME, time);
return true;
}
public int getLimit() {
return mConfigLimit;
}
public boolean setLimit(int limit) {
if (limit < 0 || limit > 100) {
return false;
}
putInt(LineageSettings.System.CHARGING_CONTROL_LIMIT, limit);
return true;
}
public boolean reset() {
return setEnabled(mDefaultEnabled) && setMode(mDefaultMode) && setLimit(mDefaultLimit)
&& setStartTime(mDefaultStartTime) && setTargetTime(mDefaultTargetTime);
}
public boolean isChargingModeSupported(int mode) {
try {
return (mChargingControl.getSupportedMode() & mode) != 0;
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
@Override
public void onStart() {
if (mChargingControl == null) {
return;
}
// Register setting observer
registerSettings(MODE_URI, LIMIT_URI, ENABLED_URI, START_TIME_URI, TARGET_TIME_URI);
// For devices that do not support bypass, we can only always listen to battery change
// because we can't distinguish between "unplugged" and "plugged in but not charging".
if (mIsChargingToggleSupported && !mIsChargingBypassSupported) {
mIsPowerConnected = true;
onPowerStatus(true);
handleSettingChange();
return;
}
// Start monitor battery status when power connected
IntentFilter connectedFilter = new IntentFilter(Intent.ACTION_POWER_CONNECTED);
mContext.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "Power connected, start monitoring battery");
mIsPowerConnected = true;
onPowerStatus(true);
}
}, connectedFilter);
// Stop monitor battery status when power disconnected
IntentFilter disconnectedFilter = new IntentFilter(Intent.ACTION_POWER_DISCONNECTED);
mContext.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "Power disconnected, stop monitoring battery");
mIsPowerConnected = false;
onPowerStatus(false);
}
}, disconnectedFilter);
// Initial monitor
IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus = mContext.registerReceiver(null, ifilter);
mIsPowerConnected = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) != 0;
if (mIsPowerConnected) {
onPowerConnected();
}
// Restore settings
handleSettingChange();
}
private void resetInternalState() {
mSavedAlarmTime = 0;
mSavedTargetTime = 0;
mEstimatedFullTime = 0;
mChargingStopReason = 0;
mIsControlCancelledOnce = false;
mChargingNotification.cancel();
}
private void onPowerConnected() {
if (mBattReceiver == null) {
mBattReceiver = new LineageHealthBatteryBroadcastReceiver();
}
IntentFilter battFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
mContext.registerReceiver(mBattReceiver, battFilter);
}
private void onPowerDisconnected() {
if (mBattReceiver != null) {
mContext.unregisterReceiver(mBattReceiver);
}
// On disconnected, reset internal state
resetInternalState();
}
private void onPowerStatus(boolean enable) {
if (enable) {
onPowerConnected();
} else {
onPowerDisconnected();
}
updateChargeControl();
}
private void updateChargingReasonBitmask(int flag, boolean set) {
if (set) {
mChargingStopReason |= flag;
} else {
mChargingStopReason &= ~flag;
}
}
private boolean isChargingReasonSet(int flag) {
return (mChargingStopReason & flag) != 0;
}
private ChargeTime getChargeTime() {
// Get duration to target full time
final long currentTime = System.currentTimeMillis();
Log.i(TAG, "Current time is " + msToString(currentTime));
long targetTime = 0, startTime = currentTime;
if (mConfigMode == MODE_AUTO) {
// Use alarm as the target time. Maybe someday we can use a model.
AlarmManager m = mContext.getSystemService(AlarmManager.class);
if (m == null) {
Log.e(TAG, "Failed to get alarm service!");
mChargingNotification.cancel();
return null;
}
AlarmManager.AlarmClockInfo alarmClockInfo = m.getNextAlarmClock();
if (alarmClockInfo == null) {
// We didn't find an alarm. Clear waiting flags because we can't predict anyway
mChargingNotification.cancel();
return null;
}
targetTime = alarmClockInfo.getTriggerTime();
} else if (mConfigMode == MODE_MANUAL) {
// User manually controlled time
startTime = getTimeMillisFromSecondOfDay(mConfigStartTime);
targetTime = getTimeMillisFromSecondOfDay(mConfigTargetTime);
if (startTime > targetTime) {
if (currentTime > targetTime) {
targetTime += DateUtils.DAY_IN_MILLIS;
} else {
startTime -= DateUtils.DAY_IN_MILLIS;
}
}
} else {
Log.e(TAG, "invalid charging control mode " + mConfigMode);
return null;
}
return new ChargeTime(startTime, targetTime);
}
private void updateChargeControl() {
if (mIsChargingToggleSupported) {
updateChargeToggle();
} else if (mIsChargingDeadlineSupported) {
updateChargeDeadline();
}
}
private boolean shouldSetLimitFlag() {
if (mConfigMode != MODE_LIMIT) {
return false;
}
if (!mIsChargingBypassSupported
&& isChargingReasonSet(ChargingStopReason.REACH_LIMIT)) {
return mBatteryPct >= mConfigLimit - mChargingLimitMargin;
}
if (mBatteryPct >= mConfigLimit) {
mChargingNotification.post(null, true);
return true;
} else {
mChargingNotification.post(null, false);
return false;
}
}
private boolean shouldSetWaitFlag() {
if (mConfigMode != MODE_AUTO && mConfigMode != MODE_MANUAL) {
return false;
}
// Now it is time to see whether charging should be stopped. We make decisions in the
// following manner:
//
// 1. If STOP_REASON_WAITING is set, compare the remaining time with the saved estimated
// full time. Resume charging the remain time <= saved estimated time
// 2. If the system estimated remaining time already exceeds the target full time, continue
// 3. Otherwise, stop charging, save the estimated time, set stop reason to
// STOP_REASON_WAITING.
final ChargeTime t = getChargeTime();
if (t == null) {
mChargingNotification.cancel();
return false;
}
final long targetTime = t.getTargetTime();
final long startTime = t.getStartTime();
final long currentTime = System.currentTimeMillis();
Log.i(TAG, "Got target time " + msToString(targetTime) + ", start time " +
msToString(startTime) + ", current time " + msToString(currentTime));
if (mConfigMode == MODE_AUTO) {
if (mSavedAlarmTime != targetTime) {
mChargingNotification.cancel();
if (mSavedAlarmTime != 0 && mSavedAlarmTime < currentTime) {
Log.i(TAG, "Not fully charged when alarm goes off, continue charging.");
mIsControlCancelledOnce = true;
return false;
}
Log.i(TAG, "User changed alarm, reconstruct notification");
mSavedAlarmTime = targetTime;
}
// Don't activate if we are more than 9 hrs away from the target alarm
if (targetTime - currentTime >= 9 * 60 * 60 * 1000) {
mChargingNotification.cancel();
return false;
}
} else if (mConfigMode == MODE_MANUAL) {
if (startTime > currentTime) {
// Not yet entering user configured time frame
mChargingNotification.cancel();
return false;
}
}
if (mBatteryPct == 100) {
mChargingNotification.post(targetTime, true);
return true;
}
// Now we have the target time and current time, we can post a notification stating that
// the system will be charged by targetTime.
mChargingNotification.post(targetTime, false);
// If current battery level is less than the fast charge limit, don't set this flag
if (mBatteryPct < CHARGE_CTRL_MIN_LEVEL) {
return false;
}
long deltaTime = targetTime - currentTime;
Log.i(TAG, "Current time to target: " + msToString(deltaTime));
if (isChargingReasonSet(ChargingStopReason.WAITING)) {
Log.i(TAG, "Current saved estimation to full: " + msToString(mEstimatedFullTime));
if (deltaTime <= mEstimatedFullTime) {
Log.i(TAG, "Unset waiting flag");
return false;
}
return true;
}
final BatteryUsageStats batteryUsageStats = mContext.getSystemService(
BatteryStatsManager.class).getBatteryUsageStats();
if (batteryUsageStats == null) {
Log.e(TAG, "Failed to get battery usage stats");
return false;
}
long remaining = batteryUsageStats.getChargeTimeRemainingMs();
if (remaining == -1) {
Log.i(TAG, "not enough data for prediction for now, waiting for more data");
return false;
}
// Add margin here
remaining += mChargingTimeMargin;
Log.i(TAG, "Current estimated time to full: " + msToString(remaining));
if (deltaTime > remaining) {
Log.i(TAG, "Stop charging and wait, saving remaining time");
mEstimatedFullTime = remaining;
return true;
}
return false;
}
private void updateChargingStopReason() {
if (mIsControlCancelledOnce) {
mChargingStopReason = ChargingStopReason.NONE;
return;
}
if (!mConfigEnabled) {
mChargingStopReason = ChargingStopReason.NONE;
return;
}
if (!mIsPowerConnected) {
mChargingStopReason = ChargingStopReason.NONE;
return;
}
updateChargingReasonBitmask(ChargingStopReason.REACH_LIMIT, shouldSetLimitFlag());
updateChargingReasonBitmask(ChargingStopReason.WAITING, shouldSetWaitFlag());
}
private void updateChargeToggle() {
updateChargingStopReason();
Log.i(TAG, "Current mChargingStopReason: " + mChargingStopReason);
boolean isChargingEnabled = false;
try {
isChargingEnabled = mChargingControl.getChargingEnabled();
} catch (IllegalStateException | RemoteException | UnsupportedOperationException e) {
Log.e(TAG, "Failed to get charging enabled status!");
}
if (isChargingEnabled != (mChargingStopReason == 0)) {
try {
mChargingControl.setChargingEnabled(!isChargingEnabled);
} catch (IllegalStateException | RemoteException | UnsupportedOperationException e) {
Log.e(TAG, "Failed to set charging status");
}
}
}
private void updateChargeDeadline() {
if (!mIsPowerConnected) {
return;
}
final ChargeTime t = getChargeTime();
if (t != null && t.getTargetTime() == mSavedTargetTime) {
return;
}
long deadline = 0;
if (t == null || mIsControlCancelledOnce) {
deadline = -1;
} else {
mSavedTargetTime = t.getTargetTime();
final long targetTime = t.getTargetTime();
final long currentTime = System.currentTimeMillis();
deadline = (targetTime - currentTime) / 1000;
}
try {
mChargingControl.setChargingDeadline(deadline);
} catch (IllegalStateException | RemoteException | UnsupportedOperationException e) {
Log.e(TAG, "Failed to set charge deadline");
}
}
private String msToString(long ms) {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(ms);
return mDateFormatter.format(calendar.getTime());
}
/**
* Convert the seconds of the day to UTC milliseconds from epoch.
*
* @param time seconds of the day
* @return UTC milliseconds from epoch
*/
private long getTimeMillisFromSecondOfDay(int time) {
ZoneId utcZone = ZoneOffset.UTC;
LocalDate currentDate = LocalDate.now();
LocalTime timeOfDay = LocalTime.ofSecondOfDay(time);
ZonedDateTime zonedDateTime = ZonedDateTime.of(currentDate, timeOfDay,
ZoneId.systemDefault())
.withZoneSameInstant(utcZone);
return zonedDateTime.toInstant().toEpochMilli();
}
private LocalTime getLocalTimeFromEpochMilli(long time) {
return Instant.ofEpochMilli(time).atZone(ZoneId.systemDefault()).toLocalTime();
}
private void handleSettingChange() {
mConfigEnabled = LineageSettings.System.getInt(mContentResolver,
LineageSettings.System.CHARGING_CONTROL_ENABLED, 0)
!= 0;
mConfigLimit = LineageSettings.System.getInt(mContentResolver,
LineageSettings.System.CHARGING_CONTROL_LIMIT,
mDefaultLimit);
mConfigMode = LineageSettings.System.getInt(mContentResolver,
LineageSettings.System.CHARGING_CONTROL_MODE,
mDefaultMode);
mConfigStartTime = LineageSettings.System.getInt(mContentResolver,
LineageSettings.System.CHARGING_CONTROL_START_TIME,
mDefaultStartTime);
mConfigTargetTime = LineageSettings.System.getInt(mContentResolver,
LineageSettings.System.CHARGING_CONTROL_TARGET_TIME,
mDefaultTargetTime);
// Cancel notification, so that it can be updated later
mChargingNotification.cancel();
// Update based on those values
updateChargeControl();
}
@Override
protected void onSettingsChanged(Uri uri) {
handleSettingChange();
}
@Override
public void dump(PrintWriter pw) {
pw.println();
pw.println("ChargingControlController Configuration:");
pw.println(" mConfigEnabled: " + mConfigEnabled);
pw.println(" mConfigMode: " + mConfigMode);
pw.println(" mConfigLimit: " + mConfigLimit);
pw.println(" mConfigStartTime: " + mConfigStartTime);
pw.println(" mConfigTargetTime: " + mConfigTargetTime);
pw.println(" mChargingTimeMargin: " + mChargingTimeMargin);
pw.println();
pw.println("ChargingControlController State:");
pw.println(" mBatteryPct: " + mBatteryPct);
pw.println(" mIsPowerConnected: " + mIsPowerConnected);
pw.println(" mChargingStopReason: " + mChargingStopReason);
pw.println(" mIsNotificationPosted: " + mChargingNotification.isPosted());
pw.println(" mIsDoneNotification: " + mChargingNotification.isDoneNotification());
pw.println(" mIsControlCancelledOnce: " + mIsControlCancelledOnce);
pw.println(" mSavedAlarmTime: " + msToString(mSavedAlarmTime));
if (mIsChargingDeadlineSupported) {
pw.println(" mSavedTargetTime (Deadline): " + msToString(mSavedTargetTime));
}
}
/* Battery Broadcast Receiver */
private class LineageHealthBatteryBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!mIsPowerConnected) {
return;
}
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
if (level == -1 || scale == -1) {
return;
}
mBatteryPct = level * 100 / (float) scale;
updateChargeControl();
}
}
/* Notification class */
class ChargingControlNotification {
private final NotificationManager mNotificationManager;
private final Context mContext;
private static final int CHARGING_CONTROL_NOTIFICATION_ID = 1000;
private static final String ACTION_CHARGING_CONTROL_CANCEL_ONCE =
"lineageos.platform.intent.action.CHARGING_CONTROL_CANCEL_ONCE";
private static final String CHARGING_CONTROL_CHANNEL_ID = "LineageHealthChargingControl";
private boolean mIsDoneNotification = false;
private boolean mIsNotificationPosted = false;
ChargingControlNotification(Context context) {
mContext = context;
// Get notification manager
mNotificationManager = mContext.getSystemService(NotificationManager.class);
// Register notification monitor
IntentFilter notificationFilter = new IntentFilter(ACTION_CHARGING_CONTROL_CANCEL_ONCE);
mContext.registerReceiver(new LineageHealthNotificationBroadcastReceiver(),
notificationFilter);
}
public void post(Long targetTime, boolean done) {
if (mIsNotificationPosted && mIsDoneNotification == done) {
return;
}
if (mIsNotificationPosted) {
cancel();
}
if (done) {
postChargingDoneNotification(targetTime);
} else {
postChargingControlNotification(targetTime);
}
mIsNotificationPosted = true;
mIsDoneNotification = done;
}
public void cancel() {
cancelChargingControlNotification();
mIsNotificationPosted = false;
}
public boolean isPosted() {
return mIsNotificationPosted;
}
public boolean isDoneNotification() {
return mIsDoneNotification;
}
private void handleNotificationIntent(Intent intent) {
if (intent.getAction().equals(ACTION_CHARGING_CONTROL_CANCEL_ONCE)) {
mIsControlCancelledOnce = true;
updateChargeControl();
cancelChargingControlNotification();
}
}
private void postChargingControlNotification(Long targetTime) {
String title = mContext.getString(R.string.charging_control_notification_title);
String message;
if (targetTime != null) {
message = String.format(
mContext.getString(R.string.charging_control_notification_content_target),
getLocalTimeFromEpochMilli(targetTime).format(mFormatter));
} else {
message = String.format(
mContext.getString(R.string.charging_control_notification_content_limit),
mConfigLimit);
}
Intent mainIntent = new Intent(INTENT_PARTS);
mainIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent mainPendingIntent = PendingIntent.getActivity(mContext, 0, mainIntent,
PendingIntent.FLAG_IMMUTABLE);
Intent cancelOnceIntent = new Intent(ACTION_CHARGING_CONTROL_CANCEL_ONCE);
PendingIntent cancelPendingIntent = PendingIntent.getBroadcast(mContext, 0,
cancelOnceIntent, PendingIntent.FLAG_IMMUTABLE);
Notification.Builder notification =
new Notification.Builder(mContext, CHARGING_CONTROL_CHANNEL_ID)
.setContentTitle(title)
.setContentText(message)
.setContentIntent(mainPendingIntent)
.setSmallIcon(R.drawable.ic_charging_control)
.setOngoing(true)
.addAction(R.drawable.ic_charging_control,
mContext.getString(
R.string.charging_control_notification_cancel_once),
cancelPendingIntent);
createNotificationChannelIfNeeded();
mNotificationManager.notify(CHARGING_CONTROL_NOTIFICATION_ID, notification.build());
}
private void postChargingDoneNotification(Long targetTime) {
cancelChargingControlNotification();
String title = mContext.getString(R.string.charging_control_notification_title);
String message;
if (targetTime != null) {
message = mContext.getString(
R.string.charging_control_notification_content_target_reached);
} else {
message = String.format(
mContext.getString(
R.string.charging_control_notification_content_limit_reached),
mConfigLimit);
}
Intent mainIntent = new Intent(INTENT_PARTS);
mainIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent mainPendingIntent = PendingIntent.getActivity(mContext, 0, mainIntent,
PendingIntent.FLAG_IMMUTABLE);
Notification.Builder notification = new Notification.Builder(mContext,
CHARGING_CONTROL_CHANNEL_ID)
.setContentTitle(title)
.setContentText(message)
.setContentIntent(mainPendingIntent)
.setSmallIcon(R.drawable.ic_charging_control)
.setOngoing(false);
createNotificationChannelIfNeeded();
mNotificationManager.notify(CHARGING_CONTROL_NOTIFICATION_ID, notification.build());
}
private void createNotificationChannelIfNeeded() {
String id = CHARGING_CONTROL_CHANNEL_ID;
NotificationChannel channel = mNotificationManager.getNotificationChannel(id);
if (channel != null) {
return;
}
String name = mContext.getString(R.string.charging_control_notification_channel);
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel batteryHealthChannel = new NotificationChannel(id, name,
importance);
batteryHealthChannel.setBlockable(true);
mNotificationManager.createNotificationChannel(batteryHealthChannel);
}
private void cancelChargingControlNotification() {
mNotificationManager.cancel(CHARGING_CONTROL_NOTIFICATION_ID);
}
/* Notification Broadcast Receiver */
private class LineageHealthNotificationBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
handleNotificationIntent(intent);
}
}
}
/* A representation of start and target time */
static final class ChargeTime {
private final long mStartTime;
private final long mTargetTime;
ChargeTime(long startTime, long targetTime) {
mStartTime = startTime;
mTargetTime = targetTime;
}
public long getStartTime() {
return mStartTime;
}
public long getTargetTime() {
return mTargetTime;
}
}
}

View File

@@ -0,0 +1,180 @@
/*
* Copyright (C) 2023 The LineageOS 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 org.lineageos.platform.internal.health;
import android.Manifest;
import android.content.Context;
import android.os.Handler;
import android.os.IBinder;
import android.os.Process;
import android.util.Log;
import com.android.server.ServiceThread;
import org.lineageos.platform.internal.LineageSystemService;
import lineageos.app.LineageContextConstants;
import lineageos.health.IHealthInterface;
import vendor.lineage.health.ChargingControlSupportedMode;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
public class HealthInterfaceService extends LineageSystemService {
private static final String TAG = "LineageHealth";
private final Context mContext;
private final Handler mHandler;
private final ServiceThread mHandlerThread;
private final List<LineageHealthFeature> mFeatures = new ArrayList<LineageHealthFeature>();
// Health features
private ChargingControlController mCCC;
public HealthInterfaceService(Context context) {
super(context);
mContext = context;
mHandlerThread = new ServiceThread(TAG, Process.THREAD_PRIORITY_DEFAULT, false);
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
}
@Override
public String getFeatureDeclaration() {
return LineageContextConstants.Features.HEALTH;
}
@Override
public boolean isCoreService() {
return false;
}
@Override
public void onStart() {
if (!mContext.getPackageManager().hasSystemFeature(
LineageContextConstants.Features.HEALTH)) {
Log.wtf(TAG, "Lineage Health service started by system server but feature xml "
+ "not declared. Not publishing binder service!");
return;
}
mCCC = new ChargingControlController(mContext, mHandler);
if (mCCC.isSupported()) {
mFeatures.add(mCCC);
}
if (!mFeatures.isEmpty()) {
publishBinderService(LineageContextConstants.LINEAGE_HEALTH_INTERFACE, mService);
}
}
@Override
public void onBootPhase(int phase) {
if (phase != PHASE_BOOT_COMPLETED) {
return;
}
// start and update all features
for (LineageHealthFeature feature : mFeatures) {
feature.start();
}
}
/* Service */
private final IBinder mService = new IHealthInterface.Stub() {
@Override
public boolean isChargingControlSupported() {
return mCCC.isSupported();
}
@Override
public boolean getChargingControlEnabled() {
return mCCC.isEnabled();
}
@Override
public boolean setChargingControlEnabled(boolean enabled) {
return mCCC.setEnabled(enabled);
}
@Override
public int getChargingControlMode() {
return mCCC.getMode();
}
@Override
public boolean setChargingControlMode(int mode) {
return mCCC.setMode(mode);
}
@Override
public int getChargingControlStartTime() {
return mCCC.getStartTime();
}
@Override
public boolean setChargingControlStartTime(int startTime) {
return mCCC.setStartTime(startTime);
}
@Override
public int getChargingControlTargetTime() {
return mCCC.getTargetTime();
}
@Override
public boolean setChargingControlTargetTime(int targetTime) {
return mCCC.setTargetTime(targetTime);
}
@Override
public int getChargingControlLimit() {
return mCCC.getLimit();
}
@Override
public boolean setChargingControlLimit(int limit) {
return mCCC.setLimit(limit);
}
@Override
public boolean resetChargingControl() {
return mCCC.reset();
}
@Override
public boolean allowFineGrainedSettings() {
// We allow fine-grained settings if allow toggle and bypass
return mCCC.isChargingModeSupported(ChargingControlSupportedMode.TOGGLE);
}
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
mContext.enforceCallingOrSelfPermission(Manifest.permission.DUMP, TAG);
pw.println();
pw.println("LineageHealth Service State:");
for (LineageHealthFeature feature : mFeatures) {
feature.dump(pw);
}
}
};
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 The LineageOS 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 org.lineageos.platform.internal.health;
import android.content.Context;
import android.os.Handler;
import org.lineageos.platform.internal.LineageBaseFeature;
public abstract class LineageHealthFeature extends LineageBaseFeature {
protected static final String TAG = "LineageHealth";
public LineageHealthFeature(Context context, Handler handler) {
super(context, handler);
}
public abstract boolean isSupported();
}

View File

@@ -2,7 +2,7 @@
<!--
/**
* Copyright (C) 2015 The CyanogenMod Project
* 2017-2022 The LineageOS Project
* 2017-2023 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,6 +37,8 @@
<protected-broadcast android:name="lineageos.platform.intent.action.UPDATE_TWILIGHT_STATE" />
<protected-broadcast android:name="lineageos.platform.intent.action.CHARGING_CONTROL_CANCEL_ONCE" />
<!-- Allows an application access to the Lineage hardware abstraction framework
<p>Not for use by third-party applications. -->
<permission android:name="lineageos.permission.HARDWARE_ABSTRACTION_ACCESS"

View File

@@ -0,0 +1,27 @@
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M16.2,22.5H7.8c-1.3,0 -2.3,-1 -2.3,-2.3V5.8c0,-1.3 1,-2.3 2.3,-2.3h0.7v-2h7v2h0.7c1.3,0 2.3,1.1 2.3,2.3v14.3C18.5,21.5 17.5,22.5 16.2,22.5zM7.8,5.5c-0.2,0 -0.3,0.2 -0.3,0.3v14.3c0,0.2 0.2,0.3 0.3,0.3h8.3c0.2,0 0.3,-0.1 0.3,-0.3V5.8c0,-0.2 -0.1,-0.3 -0.3,-0.3h-2.7v-2h-3v2H7.8z"/>
<path
android:fillColor="#FF000000"
android:pathData="M11.17,18.42v-4.58H9.5l3.33,-6.25v4.58h1.67L11.17,18.42z"/>
</vector>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 The CyanogenMod Project
2017-2022 The LineageOS Project
2017-2023 The LineageOS Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -106,6 +106,7 @@
<item>org.lineageos.platform.internal.TrustInterfaceService</item>
<item>org.lineageos.platform.internal.LineageSettingsService</item>
<item>org.lineageos.platform.internal.LineageGlobalActionsService</item>
<item>org.lineageos.platform.internal.health.HealthInterfaceService</item>
</string-array>
<!-- The LineageSystemServer class that is invoked from Android's SystemServer -->
@@ -306,4 +307,40 @@
<!-- The list of package IDs that are allowed to skip camera high frame rate checks. -->
<string-array name="config_cameraHFRPrivAppList" translatable="false" />
<!-- Whether charging control should be enabled by default -->
<bool name="config_chargingControlEnabled">false</bool>
<!-- Default charging control mode.
This integer should be set to:
1 - auto - Use the alarm to calculate the time range when to activate charging control
2 - custom - Use time range when the device is usually charging for hours
3 - limit - Just limit charging -->
<integer name="config_defaultChargingControlMode">1</integer>
<!-- Default time when charging control is activated.
Represented as seconds from midnight (e.g. 79200 == 10pm). -->
<integer name="config_defaultChargingControlStartTime">79200</integer>
<!-- Default time when battery will be fully charged.
Represented as seconds from midnight (e.g. 21600 == 6am). -->
<integer name="config_defaultChargingControlTargetTime">21600</integer>
<!-- Default charging limit. -->
<integer name="config_defaultChargingControlLimit">80</integer>
<!-- Considering the fact that the system might have an incorrect estimation of the time to
full. Set a time margin to make the device fully charged before the target time arrives.
The unit is minutes and the default value is 30 minutes. If you find that it is not enough
to make the device to be fully charged at the target time, increase the value
-->
<integer name="config_chargingControlTimeMargin">30</integer>
<!-- For a device that cannot bypass battery when charging stops (that is, the battery current
is 0mA when charging stops), the battery will gradually discharge. So we need to make it
recharge when the battery level is lower than a threshold. Set this so that the device
will be charged between (limit - val) and limit. -->
<integer name="config_chargingControlBatteryRechargeMargin">10</integer>
</resources>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 The CyanogenMod Project
2017-2022 The LineageOS Project
2017-2023 The LineageOS Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -124,4 +124,13 @@
<string name="trust_notification_title_onboarding">Discover Trust</string>
<string name="trust_notification_content_onboarding">Get to know how to assure your device is safe</string>
<string name="trust_notification_action_manage">Manage alerts</string>
<!-- Health interface -->
<string name="charging_control_notification_channel">Charging control</string>
<string name="charging_control_notification_title">Charging control</string>
<string name="charging_control_notification_cancel_once">Cancel</string>
<string name="charging_control_notification_content_limit">Battery will be charged to %1$d%%</string>
<string name="charging_control_notification_content_limit_reached">Battery is charged to %1$d%%</string>
<string name="charging_control_notification_content_target">Battery will be fully charged at %1$s</string>
<string name="charging_control_notification_content_target_reached">Battery is charged</string>
</resources>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 The CyanogenMod Project
2017-2022 The LineageOS Project
2017-2023 The LineageOS Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -182,4 +182,21 @@
<java-symbol type="array" name="config_cameraAuxPackageAllowList" />
<java-symbol type="array" name="config_cameraAuxPackageExcludeList" />
<java-symbol type="array" name="config_cameraHFRPrivAppList" />
<!-- Health interface -->
<java-symbol type="bool" name="config_chargingControlEnabled" />
<java-symbol type="integer" name="config_defaultChargingControlMode" />
<java-symbol type="integer" name="config_defaultChargingControlStartTime" />
<java-symbol type="integer" name="config_defaultChargingControlTargetTime" />
<java-symbol type="integer" name="config_defaultChargingControlLimit" />
<java-symbol type="drawable" name="ic_charging_control" />
<java-symbol type="integer" name="config_chargingControlTimeMargin" />
<java-symbol type="integer" name="config_chargingControlBatteryRechargeMargin" />
<java-symbol type="string" name="charging_control_notification_channel" />
<java-symbol type="string" name="charging_control_notification_title" />
<java-symbol type="string" name="charging_control_notification_cancel_once" />
<java-symbol type="string" name="charging_control_notification_content_limit" />
<java-symbol type="string" name="charging_control_notification_content_limit_reached" />
<java-symbol type="string" name="charging_control_notification_content_target" />
<java-symbol type="string" name="charging_control_notification_content_target_reached" />
</resources>

View File

@@ -1,6 +1,6 @@
/**
* Copyright (C) 2015, The CyanogenMod Project
* 2017-2022 The LineageOS Project
* 2017-2023 The LineageOS Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -81,6 +81,17 @@ public final class LineageContextConstants {
*/
public static final String LINEAGE_TRUST_INTERFACE = "lineagetrust";
/**
* Use with {@link android.content.Context#getSystemService} to retrieve a
* {@link lineageos.health.HealthInterface} to access the Health interface.
*
* @see android.content.Context#getSystemService
* @see lineageos.health.HealthInterface
*
* @hide
*/
public static final String LINEAGE_HEALTH_INTERFACE = "lineagehealth";
/**
* Update power menu (GlobalActions)
*
@@ -155,5 +166,13 @@ public final class LineageContextConstants {
*/
@SdkConstant(SdkConstant.SdkConstantType.FEATURE)
public static final String GLOBAL_ACTIONS = "org.lineageos.globalactions";
/**
* Feature for {@link PackageManager#getSystemAvailableFeatures} and
* {@link PackageManager#hasSystemFeature}: The device includes the lineage health
* service utilized by the lineage sdk and LineageParts.
*/
@SdkConstant(SdkConstant.SdkConstantType.FEATURE)
public static final String HEALTH = "org.lineageos.health";
}
}

View File

@@ -0,0 +1,282 @@
/*
* Copyright (C) 2023 The LineageOS 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 lineageos.health;
import android.content.Context;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.Log;
import lineageos.app.LineageContextConstants;
public class HealthInterface {
/**
* No config set. This value is invalid and does not have any effects
*/
public static final int MODE_NONE = 0;
/**
* Automatic config
*/
public static final int MODE_AUTO = 1;
/**
* Manual config mode
*/
public static final int MODE_MANUAL = 2;
/**
* Limit config mode
*/
public static final int MODE_LIMIT = 3;
private static final String TAG = "HealthInterface";
private static IHealthInterface sService;
private static HealthInterface sInstance;
private Context mContext;
private HealthInterface(Context context) {
Context appContext = context.getApplicationContext();
mContext = appContext == null ? context : appContext;
sService = getService();
if (context.getPackageManager().hasSystemFeature(
LineageContextConstants.Features.HEALTH) && sService == null) {
throw new RuntimeException("Unable to get HealthInterfaceService. The service" +
" either crashed, was not started, or the interface has been called too early" +
" in SystemServer init");
}
}
/**
* Get or create an instance of the {@link lineageos.health.HealthInterface}
*
* @param context Used to get the service
* @return {@link HealthInterface}
*/
public static synchronized HealthInterface getInstance(Context context) {
if (sInstance == null) {
sInstance = new HealthInterface(context);
}
return sInstance;
}
/** @hide **/
public static IHealthInterface getService() {
if (sService != null) {
return sService;
}
IBinder b = ServiceManager.getService(LineageContextConstants.LINEAGE_HEALTH_INTERFACE);
sService = IHealthInterface.Stub.asInterface(b);
if (sService == null) {
Log.e(TAG, "null health service, SAD!");
return null;
}
return sService;
}
/**
* @return true if service is valid
*/
private boolean checkService() {
if (sService == null) {
Log.w(TAG, "not connected to LineageHardwareManagerService");
return false;
}
return true;
}
/**
* Returns whether charging control is supported
*
* @return true if charging control is supported
*/
public boolean isChargingControlSupported() {
try {
return checkService() && sService.isChargingControlSupported();
} catch (RemoteException e) {
Log.e(TAG, e.getLocalizedMessage(), e);
}
return false;
}
/**
* Returns the charging control enabled status
*
* @return whether charging control has been enabled
*/
public boolean getEnabled() {
try {
return checkService() && sService.getChargingControlEnabled();
} catch (RemoteException e) {
return false;
}
}
/**
* Set charging control enable status
*
* @param enabled whether charging control should be enabled
* @return true if the enabled status was successfully set
*/
public boolean setEnabled(boolean enabled) {
try {
return checkService() && sService.setChargingControlEnabled(enabled);
} catch (RemoteException e) {
return false;
}
}
/**
* Returns the current charging control mode
*
* @return id of the charging control mode
*/
public int getMode() {
try {
return checkService() ? sService.getChargingControlMode() : MODE_NONE;
} catch (RemoteException e) {
return MODE_NONE;
}
}
/**
* Selects the new charging control mode
*
* @param mode the new charging control mode
* @return true if the mode was successfully set
*/
public boolean setMode(int mode) {
try {
return checkService() && sService.setChargingControlMode(mode);
} catch (RemoteException e) {
return false;
}
}
/**
* Gets the charging control start time
*
* @return the seconds of the day of the start time
*/
public int getStartTime() {
try {
return checkService() ? sService.getChargingControlStartTime() : 0;
} catch (RemoteException e) {
return 0;
}
}
/**
* Sets the charging control start time
*
* @param time the seconds of the day of the start time
* @return true if the start time was successfully set
*/
public boolean setStartTime(int time) {
try {
return checkService() && sService.setChargingControlStartTime(time);
} catch (RemoteException e) {
return false;
}
}
/**
* Gets the charging control target time
*
* @return the seconds of the day of the target time
*/
public int getTargetTime() {
try {
return checkService() ? sService.getChargingControlTargetTime() : 0;
} catch (RemoteException e) {
return 0;
}
}
/**
* Sets the charging control target time
*
* @param time the seconds of the day of the target time
* @return true if the target time was successfully set
*/
public boolean setTargetTime(int time) {
try {
return checkService() && sService.setChargingControlTargetTime(time);
} catch (RemoteException e) {
return false;
}
}
/**
* Gets the charging control limit
*
* @return the charging control limit
*/
public int getLimit() {
try {
return checkService() ? sService.getChargingControlLimit() : 100;
} catch (RemoteException e) {
return 0;
}
}
/**
* Sets the charging control limit
*
* @param limit the charging control limit
* @return true if the limit was successfully set
*/
public boolean setLimit(int limit) {
try {
return checkService() && sService.setChargingControlLimit(limit);
} catch (RemoteException e) {
return false;
}
}
/**
* Resets the charging control setting to default
*
* @return true if the setting was successfully reset
*/
public boolean reset() {
try {
return checkService() && sService.resetChargingControl();
} catch (RemoteException e) {
return false;
}
}
/**
* Returns whether the device's battery control bypasses battery
*
* @return true if the charging control bypasses battery
*/
public boolean allowFineGrainedSettings() {
try {
return checkService() && sService.allowFineGrainedSettings();
} catch (RemoteException e) {
return false;
}
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2023 The LineageOS 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 lineageos.health;
/** @hide */
interface IHealthInterface {
boolean isChargingControlSupported();
boolean getChargingControlEnabled();
boolean setChargingControlEnabled(boolean enabled);
int getChargingControlMode();
boolean setChargingControlMode(int mode);
int getChargingControlStartTime();
boolean setChargingControlStartTime(int time);
int getChargingControlTargetTime();
boolean setChargingControlTargetTime(int time);
int getChargingControlLimit();
boolean setChargingControlLimit(int limit);
boolean resetChargingControl();
boolean allowFineGrainedSettings();
}