diff --git a/Android.bp b/Android.bp
index ed3ecd747194..c6f414a2ac8a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -251,6 +251,7 @@ java_library {
"vendor.lineage.livedisplay-V2.0-java",
"vendor.lineage.livedisplay-V2.1-java",
"vendor.lineage.touch-V1.0-java",
+ "vendor.lineage.health-V1-java",
],
sdk_version: "core_platform",
installable: false,
diff --git a/core/java/com/android/internal/lineage/app/LineageContextConstants.java b/core/java/com/android/internal/lineage/app/LineageContextConstants.java
index 5400ddf45ff2..f9e108e8e5ff 100644
--- a/core/java/com/android/internal/lineage/app/LineageContextConstants.java
+++ b/core/java/com/android/internal/lineage/app/LineageContextConstants.java
@@ -43,6 +43,17 @@ private LineageContextConstants() {
*/
public static final String LINEAGE_HARDWARE_SERVICE = "lineagehardware";
+ /**
+ * 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";
+
/**
* Manages display color adjustments
*
diff --git a/core/java/com/android/internal/lineage/health/HealthInterface.java b/core/java/com/android/internal/lineage/health/HealthInterface.java
new file mode 100644
index 000000000000..d854e0036894
--- /dev/null
+++ b/core/java/com/android/internal/lineage/health/HealthInterface.java
@@ -0,0 +1,269 @@
+/*
+ * 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 com.android.internal.lineage.health;
+
+import android.content.Context;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import com.android.internal.lineage.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();
+ }
+ /**
+ * 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;
+ }
+ }
+}
diff --git a/core/java/com/android/internal/lineage/health/IHealthInterface.aidl b/core/java/com/android/internal/lineage/health/IHealthInterface.aidl
new file mode 100644
index 000000000000..c2e9759ce6f1
--- /dev/null
+++ b/core/java/com/android/internal/lineage/health/IHealthInterface.aidl
@@ -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 com.android.internal.lineage.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();
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 7e51cc510a2c..ca50241341b7 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -6611,6 +6611,9 @@
+
+
+
diff --git a/core/res/res/drawable/ic_charging_control.xml b/core/res/res/drawable/ic_charging_control.xml
new file mode 100644
index 000000000000..830853230a47
--- /dev/null
+++ b/core/res/res/drawable/ic_charging_control.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/core/res/res/values/custom_config.xml b/core/res/res/values/custom_config.xml
index 9b3fa91561a2..1ba78ec74f6b 100644
--- a/core/res/res/values/custom_config.xml
+++ b/core/res/res/values/custom_config.xml
@@ -133,4 +133,33 @@
+
+
+ false
+
+ 1
+
+ 79200
+
+ 21600
+
+ 80
+
+ 30
+
+ 10
+
diff --git a/core/res/res/values/custom_strings.xml b/core/res/res/values/custom_strings.xml
index fca35af6f9d3..c16d828b6abf 100644
--- a/core/res/res/values/custom_strings.xml
+++ b/core/res/res/values/custom_strings.xml
@@ -61,4 +61,13 @@
1.\u0020
2.\u0020
+
+ Charging control
+ Charging control
+ Cancel
+ Battery will be charged to %1$d%%
+ Battery is charged to %1$d%%
+ Battery will be fully charged at %1$s
+ Battery is charged
+
diff --git a/core/res/res/values/custom_symbols.xml b/core/res/res/values/custom_symbols.xml
index 534369fec244..6d5dd58af436 100644
--- a/core/res/res/values/custom_symbols.xml
+++ b/core/res/res/values/custom_symbols.xml
@@ -160,4 +160,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/services/core/java/com/android/server/lineage/health/ChargingControlController.java b/services/core/java/com/android/server/lineage/health/ChargingControlController.java
new file mode 100644
index 000000000000..918eaac1cb51
--- /dev/null
+++ b/services/core/java/com/android/server/lineage/health/ChargingControlController.java
@@ -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 com.android.server.lineage.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 com.android.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 android.provider.Settings;
+
+import vendor.lineage.health.ChargingControlSupportedMode;
+import vendor.lineage.health.IChargingControl;
+
+import static com.android.internal.lineage.health.HealthInterface.MODE_NONE;
+import static com.android.internal.lineage.health.HealthInterface.MODE_AUTO;
+import static com.android.internal.lineage.health.HealthInterface.MODE_MANUAL;
+import static com.android.internal.lineage.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 = Settings.System.getUriFor(
+ Settings.System.CHARGING_CONTROL_MODE);
+ private final Uri LIMIT_URI = Settings.System.getUriFor(
+ Settings.System.CHARGING_CONTROL_LIMIT);
+ private final Uri ENABLED_URI = Settings.System.getUriFor(
+ Settings.System.CHARGING_CONTROL_ENABLED);
+ private final Uri START_TIME_URI = Settings.System.getUriFor(
+ Settings.System.CHARGING_CONTROL_START_TIME);
+ private final Uri TARGET_TIME_URI = Settings.System.getUriFor(
+ Settings.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 =
+ "com.android.settings.lineage.health.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(Settings.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(Settings.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(Settings.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(Settings.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(Settings.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 = Settings.System.getInt(mContentResolver,
+ Settings.System.CHARGING_CONTROL_ENABLED, 0)
+ != 0;
+ mConfigLimit = Settings.System.getInt(mContentResolver,
+ Settings.System.CHARGING_CONTROL_LIMIT,
+ mDefaultLimit);
+ mConfigMode = Settings.System.getInt(mContentResolver,
+ Settings.System.CHARGING_CONTROL_MODE,
+ mDefaultMode);
+ mConfigStartTime = Settings.System.getInt(mContentResolver,
+ Settings.System.CHARGING_CONTROL_START_TIME,
+ mDefaultStartTime);
+ mConfigTargetTime = Settings.System.getInt(mContentResolver,
+ Settings.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;
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/lineage/health/HealthInterfaceService.java b/services/core/java/com/android/server/lineage/health/HealthInterfaceService.java
new file mode 100644
index 000000000000..86745d1d5928
--- /dev/null
+++ b/services/core/java/com/android/server/lineage/health/HealthInterfaceService.java
@@ -0,0 +1,164 @@
+/*
+ * 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 com.android.server.lineage.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 com.android.server.SystemService;
+
+import com.android.internal.lineage.app.LineageContextConstants;
+import com.android.internal.lineage.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 SystemService {
+
+ private static final String TAG = "LineageHealth";
+ private final Context mContext;
+ private final Handler mHandler;
+ private final ServiceThread mHandlerThread;
+
+ private final List mFeatures = new ArrayList();
+
+ // 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 void onStart() {
+ 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);
+ }
+ }
+ };
+}
diff --git a/services/core/java/com/android/server/lineage/health/LineageHealthFeature.java b/services/core/java/com/android/server/lineage/health/LineageHealthFeature.java
new file mode 100644
index 000000000000..efc5df13132a
--- /dev/null
+++ b/services/core/java/com/android/server/lineage/health/LineageHealthFeature.java
@@ -0,0 +1,33 @@
+
+/*
+ * 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 com.android.server.lineage.health;
+
+import android.content.Context;
+import android.os.Handler;
+
+import com.android.server.lineage.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();
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 75b022293970..831302800f75 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -218,6 +218,7 @@
import com.android.server.wm.ActivityTaskManagerService;
import com.android.server.wm.WindowManagerGlobalLock;
import com.android.server.wm.WindowManagerService;
+import com.android.server.lineage.health.HealthInterfaceService;
import dalvik.system.VMRuntime;
@@ -2556,6 +2557,9 @@ private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
t.traceBegin("StartLiveDisplayService");
mSystemServiceManager.startService(LiveDisplayService.class);
t.traceEnd();
+ t.traceBegin("StartHealthService");
+ mSystemServiceManager.startService(HealthInterfaceService.class);
+ t.traceEnd();
}
}