OmniControl: Add charging control preferences

Change-Id: I936add4df464eb9818258de4955b651266c67ec3
Signed-off-by: micky387 <mickaelsaibi@free.fr>
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cd5a37c..0ea1f8d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -43,6 +43,18 @@
             android:exported="false"
             android:label="@string/color_activity_title"
             android:launchMode="singleTop" />
+
+        <!-- Charging control settings (Battery category) -->
+        <activity
+            android:name=".ChargingControlSettings"
+            android:exported="true"
+            android:label="@string/charging_control_title">
+            <intent-filter>
+                <action android:name="org.omnirom.control.CHARGING_CONTROL_SETTINGS" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
     </application>
 
 </manifest>
diff --git a/app/src/main/java/org/omnirom/control/ChargingControlSettings.java b/app/src/main/java/org/omnirom/control/ChargingControlSettings.java
new file mode 100644
index 0000000..821503a
--- /dev/null
+++ b/app/src/main/java/org/omnirom/control/ChargingControlSettings.java
@@ -0,0 +1,233 @@
+/*
+ * 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.omnirom.control;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+import org.omnirom.control.R;
+import org.omnirom.control.AbstractSettingsFragment;
+
+import org.omnirom.control.health.StartTimePreference;
+import org.omnirom.control.health.TargetTimePreference;
+import org.omnirom.control.health.ChargingLimitPreference;
+
+import omnirom.health.HealthInterface;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.XmlRes;
+import androidx.preference.ListPreference;
+import omnirom.preference.SystemSettingSwitchPreference;
+
+import static omnirom.health.HealthInterface.MODE_AUTO;
+import static omnirom.health.HealthInterface.MODE_MANUAL;
+import static omnirom.health.HealthInterface.MODE_LIMIT;
+
+import org.omnirom.omnilib.utils.OmniSettings;
+
+public class ChargingControlSettings extends AbstractSettingsFragment implements
+        Preference.OnPreferenceChangeListener {
+    private static final String TAG = ChargingControlSettings.class.getSimpleName();
+
+    public static final @DrawableRes int ICON_RES = R.drawable.ic_charging_control;
+    public static final @XmlRes int XML_RES = R.xml.charging_control_settings;
+    private static final String DEFAULT_VALUES = "reset_to_default_key";
+
+    private static final String CHARGING_CONTROL_ENABLED_PREF = "charging_control_enabled";
+    private static final String CHARGING_CONTROL_MODE_PREF = "charging_control_mode";
+    private static final String CHARGING_CONTROL_START_TIME_PREF = "charging_control_start_time";
+    private static final String CHARGING_CONTROL_TARGET_TIME_PREF = "charging_control_target_time";
+    private static final String CHARGING_CONTROL_LIMIT_PREF = "charging_control_charging_limit";
+
+    private Preference mDefaultValues;
+    private SystemSettingSwitchPreference mChargingControlEnabledPref;
+    private ListPreference mChargingControlModePref;
+    private StartTimePreference mChargingControlStartTimePref;
+    private TargetTimePreference mChargingControlTargetTimePref;
+    private ChargingLimitPreference mChargingControlLimitPref;
+
+    private HealthInterface mHealthInterface;
+
+    @Override
+    public String getFragmentTitle() {
+        return getString(R.string.charging_control_title);
+    }
+
+    @Override
+    public String getFragmentSummary() {
+        return getString(R.string.charging_control_summary);
+    }
+
+    @Override
+    public int getFragmentIcon() {
+        return ICON_RES;
+    }
+
+    @Override
+    public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+        setPreferencesFromResource(XML_RES, rootKey);
+        final Resources res = getResources();
+
+        mHealthInterface = HealthInterface.getInstance(getActivity());
+
+        final PreferenceScreen prefSet = getPreferenceScreen();
+
+        mChargingControlEnabledPref = prefSet.findPreference(CHARGING_CONTROL_ENABLED_PREF);
+        mChargingControlEnabledPref.setOnPreferenceChangeListener(this);
+        mChargingControlModePref = prefSet.findPreference(CHARGING_CONTROL_MODE_PREF);
+        mChargingControlModePref.setOnPreferenceChangeListener(this);
+        mChargingControlStartTimePref = prefSet.findPreference(CHARGING_CONTROL_START_TIME_PREF);
+        mChargingControlTargetTimePref = prefSet.findPreference(CHARGING_CONTROL_TARGET_TIME_PREF);
+        mChargingControlLimitPref = prefSet.findPreference(CHARGING_CONTROL_LIMIT_PREF);
+
+        if (mChargingControlLimitPref != null) {
+            if (mHealthInterface.allowFineGrainedSettings()) {
+                mChargingControlModePref.setEntries(concatStringArrays(
+                        mChargingControlModePref.getEntries(),
+                        res.getStringArray(
+                                R.array.charging_control_mode_entries_fine_grained_control)));
+                mChargingControlModePref.setEntryValues(concatStringArrays(
+                        mChargingControlModePref.getEntryValues(),
+                        res.getStringArray(
+                                R.array.charging_control_mode_values_fine_grained_control)));
+            }
+        }
+
+        refreshValues();
+
+        mDefaultValues = (Preference) findPreference(DEFAULT_VALUES);
+        mDefaultValues.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+            @Override
+            public boolean onPreferenceClick(Preference preference) {
+                resetToDefaults();
+                return true;
+            }
+        });
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        refreshUi();
+    }
+
+    private void refreshValues() {
+        if (mChargingControlEnabledPref != null) {
+            mChargingControlEnabledPref.setChecked(mHealthInterface.getEnabled());
+        }
+
+        if (mChargingControlModePref != null) {
+            final int chargingControlMode = mHealthInterface.getMode();
+            mChargingControlModePref.setValue(Integer.toString(chargingControlMode));
+            refreshUi();
+        }
+
+        if (mChargingControlStartTimePref != null) {
+            mChargingControlStartTimePref.setValue(
+                    mChargingControlStartTimePref.getTimeSetting());
+        }
+
+        if (mChargingControlTargetTimePref != null) {
+            mChargingControlTargetTimePref.setValue(
+                    mChargingControlTargetTimePref.getTimeSetting());
+        }
+
+        if (mChargingControlLimitPref != null) {
+            mChargingControlLimitPref.setValue(
+                    mChargingControlLimitPref.getSetting());
+        }
+    }
+
+    private void refreshUi() {
+        final int chargingControlMode = mHealthInterface.getMode();
+
+        refreshUi(chargingControlMode);
+    }
+
+    private void refreshUi(final int chargingControlMode) {
+        String summary = null;
+        boolean isChargingControlStartTimePrefVisible = false;
+        boolean isChargingControlTargetTimePrefVisible = false;
+        boolean isChargingControlLimitPrefVisible = false;
+
+        final Resources res = getResources();
+
+        switch (chargingControlMode) {
+            case MODE_AUTO:
+                summary = res.getString(R.string.charging_control_mode_auto_summary);
+                break;
+            case MODE_MANUAL:
+                summary = res.getString(R.string.charging_control_mode_custom_summary);
+                isChargingControlStartTimePrefVisible = true;
+                isChargingControlTargetTimePrefVisible = true;
+                break;
+            case MODE_LIMIT:
+                summary = res.getString(R.string.charging_control_mode_limit_summary);
+                isChargingControlLimitPrefVisible = true;
+                break;
+            default:
+                return;
+        }
+
+        mChargingControlModePref.setSummary(summary);
+
+        if (mChargingControlStartTimePref != null) {
+            mChargingControlStartTimePref.setVisible(isChargingControlStartTimePrefVisible);
+        }
+
+        if (mChargingControlTargetTimePref != null) {
+            mChargingControlTargetTimePref.setVisible(isChargingControlTargetTimePrefVisible);
+        }
+
+        if (mChargingControlLimitPref != null) {
+            mChargingControlLimitPref.setVisible(isChargingControlLimitPrefVisible);
+        }
+    }
+
+    @Override
+    public boolean onPreferenceChange(final Preference preference, final Object objValue) {
+        if (preference == mChargingControlEnabledPref) {
+            mHealthInterface.setEnabled((Boolean) objValue);
+        } else if (preference == mChargingControlModePref) {
+            final int chargingControlMode = Integer.parseInt((String) objValue);
+            mHealthInterface.setMode(chargingControlMode);
+            refreshUi(chargingControlMode);
+        }
+        return true;
+    }
+
+    private void resetToDefaults() {
+        mHealthInterface.reset();
+
+        refreshValues();
+    }
+
+    private CharSequence[] concatStringArrays(CharSequence[] array1, CharSequence[] array2) {
+        return Stream.concat(Arrays.stream(array1), Arrays.stream(array2)).toArray(size ->
+                (CharSequence[]) Array.newInstance(CharSequence.class, size));
+    }
+}
diff --git a/app/src/main/java/org/omnirom/control/GridViewFragment.kt b/app/src/main/java/org/omnirom/control/GridViewFragment.kt
index 5914e60..909cbb2 100644
--- a/app/src/main/java/org/omnirom/control/GridViewFragment.kt
+++ b/app/src/main/java/org/omnirom/control/GridViewFragment.kt
@@ -21,6 +21,8 @@
 import android.content.Context
 import android.content.Intent
 import android.os.Bundle
+import android.os.IBinder
+import android.os.ServiceManager
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -103,6 +105,16 @@
                 )
             )
         }
+        if (getLineageHealthStatus()) {
+            gridItems.add(
+                FragmentGridItem(
+                    R.string.charging_control_title,
+                    R.string.charging_control_summary,
+                    R.drawable.ic_charging_control,
+                    ChargingControlSettings()
+                )
+            )
+        }
         gridItems.add(
             FragmentGridItem(
                 R.string.bars_settings_title,
@@ -337,4 +349,19 @@
                     requireActivity().startActivity(gridItem.gridIntent)
             }
     }
+
+    private fun isNegated(key: String?): Boolean {
+        return key != null && key.startsWith("!")
+    }
+
+    private fun getLineageHealthStatus(): Boolean {
+        var rService = "lineagehealth"
+        val negated = isNegated(rService)
+        if (negated) {
+            rService = rService.substring(1)
+        }
+        val value: IBinder? = ServiceManager.getService(rService)
+        val available = value != null
+        return available != negated
+    }
 }
diff --git a/app/src/main/java/org/omnirom/control/health/ChargingLimitPreference.java b/app/src/main/java/org/omnirom/control/health/ChargingLimitPreference.java
new file mode 100644
index 0000000..dde90c2
--- /dev/null
+++ b/app/src/main/java/org/omnirom/control/health/ChargingLimitPreference.java
@@ -0,0 +1,97 @@
+/*
+ * 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.omnirom.control.health;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
+
+import omnirom.health.HealthInterface;
+
+import org.omnirom.control.R;
+
+public class ChargingLimitPreference extends Preference
+        implements SeekBar.OnSeekBarChangeListener {
+    private static final String TAG = ChargingLimitPreference.class.getSimpleName();
+
+    private TextView mChargingLimitValue;
+    private SeekBar mChargingLimitBar;
+
+    private final HealthInterface mHealthInterface;
+
+    public ChargingLimitPreference(final Context context, final AttributeSet attrs) {
+        super(context, attrs);
+
+        setLayoutResource(R.layout.preference_charging_limit);
+
+        mHealthInterface = HealthInterface.getInstance(context);
+    }
+
+    @Override
+    public void onBindViewHolder(final PreferenceViewHolder holder) {
+        super.onBindViewHolder(holder);
+
+        mChargingLimitValue = (TextView) holder.findViewById(R.id.value);
+
+        mChargingLimitBar = (SeekBar) holder.findViewById(R.id.seekbar_widget);
+        mChargingLimitBar.setOnSeekBarChangeListener(this);
+
+        int currLimit = getSetting();
+        mChargingLimitBar.setProgress(currLimit);
+        updateValue(currLimit);
+    }
+
+    @Override
+    public void onStartTrackingTouch(final SeekBar seekBar) {
+    }
+
+    @Override
+    public void onStopTrackingTouch(final SeekBar seekBar) {
+        setSetting(seekBar.getProgress());
+    }
+
+    @Override
+    public void onProgressChanged(final SeekBar seekBar, final int progress,
+            final boolean fromUser) {
+        updateValue(progress);
+    }
+
+    public void setValue(final int value) {
+        if (mChargingLimitBar != null) {
+            mChargingLimitBar.setProgress(value);
+        }
+        updateValue(value);
+    }
+
+    public int getSetting() {
+        return mHealthInterface.getLimit();
+    }
+
+    protected void setSetting(final int chargingLimit) {
+        mHealthInterface.setLimit(chargingLimit);
+    }
+
+    private void updateValue(final int value) {
+        if (mChargingLimitValue != null) {
+            mChargingLimitValue.setText(String.format("%d%%", value));
+        }
+    }
+}
diff --git a/app/src/main/java/org/omnirom/control/health/StartTimePreference.java b/app/src/main/java/org/omnirom/control/health/StartTimePreference.java
new file mode 100644
index 0000000..59dee18
--- /dev/null
+++ b/app/src/main/java/org/omnirom/control/health/StartTimePreference.java
@@ -0,0 +1,45 @@
+/*
+ * 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.omnirom.control.health;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import org.omnirom.control.R;
+
+public class StartTimePreference extends TimePreference {
+    private static final String TAG = StartTimePreference.class.getSimpleName();
+
+    public StartTimePreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected int getSummaryResourceId() {
+        return R.string.charging_control_start_time_summary;
+    }
+
+    @Override
+    public int getTimeSetting() {
+        return mHealthInterface.getStartTime();
+    }
+
+    @Override
+    protected void setTimeSetting(int secondOfDay) {
+        mHealthInterface.setStartTime(secondOfDay);
+    }
+}
diff --git a/app/src/main/java/org/omnirom/control/health/TargetTimePreference.java b/app/src/main/java/org/omnirom/control/health/TargetTimePreference.java
new file mode 100644
index 0000000..fbbd3a5
--- /dev/null
+++ b/app/src/main/java/org/omnirom/control/health/TargetTimePreference.java
@@ -0,0 +1,45 @@
+/*
+ * 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.omnirom.control.health;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import org.omnirom.control.R;
+
+public class TargetTimePreference extends TimePreference {
+    private static final String TAG = TargetTimePreference.class.getSimpleName();
+
+    public TargetTimePreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected int getSummaryResourceId() {
+        return R.string.charging_control_target_time_summary;
+    }
+
+    @Override
+    public int getTimeSetting() {
+        return mHealthInterface.getTargetTime();
+    }
+
+    @Override
+    protected void setTimeSetting(int secondOfDay) {
+        mHealthInterface.setTargetTime(secondOfDay);
+    }
+}
diff --git a/app/src/main/java/org/omnirom/control/health/TimePreference.java b/app/src/main/java/org/omnirom/control/health/TimePreference.java
new file mode 100644
index 0000000..cde677d
--- /dev/null
+++ b/app/src/main/java/org/omnirom/control/health/TimePreference.java
@@ -0,0 +1,106 @@
+/*
+ * 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.omnirom.control.health;
+
+import static java.time.format.FormatStyle.SHORT;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TimePicker;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.preference.PreferenceViewHolder;
+
+import org.omnirom.control.widget.CustomDialogPreference;
+import org.omnirom.control.R;
+
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+
+import omnirom.health.HealthInterface;
+
+public abstract class TimePreference extends CustomDialogPreference<AlertDialog> {
+    private static final String TAG = TimePreference.class.getSimpleName();
+    private static final DateTimeFormatter mFormatter = DateTimeFormatter.ofLocalizedTime(SHORT);
+
+    private TimePicker mTimePicker;
+    private LocalTime mLocalTime;
+
+    protected final HealthInterface mHealthInterface;
+
+    public TimePreference(final Context context, final AttributeSet attrs) {
+        super(context, attrs);
+
+        setDialogLayoutResource(R.layout.dialog_time);
+        mHealthInterface = HealthInterface.getInstance(context);
+    }
+
+    @Override
+    public void onBindViewHolder(final PreferenceViewHolder holder) {
+        mLocalTime = LocalTime.ofSecondOfDay(getTimeSetting());
+        super.onBindViewHolder(holder);
+    }
+
+    @Override
+    protected void onPrepareDialogBuilder(final AlertDialog.Builder builder,
+            final DialogInterface.OnClickListener listener) {
+        super.onPrepareDialogBuilder(builder, listener);
+
+        builder.setNegativeButton(R.string.cancel, null);
+        builder.setPositiveButton(R.string.dlg_ok, null);
+    }
+
+    @Override
+    protected void onDialogClosed(final boolean positiveResult) {
+        super.onDialogClosed(positiveResult);
+
+        if (positiveResult) {
+            mLocalTime = LocalTime.of(mTimePicker.getHour(),
+                    mTimePicker.getMinute());
+            setTimeSetting(mLocalTime.toSecondOfDay());
+            setSummary(getSummary());
+        }
+    }
+
+    @Override
+    protected void onBindDialogView(View view) {
+        super.onBindDialogView(view);
+
+        mTimePicker = view.findViewById(R.id.time_picker);
+        mTimePicker.setHour(mLocalTime.getHour());
+        mTimePicker.setMinute(mLocalTime.getMinute());
+    }
+
+    @Override
+    public CharSequence getSummary() {
+        return String.format(getContext().getString(getSummaryResourceId()),
+                mLocalTime.format(mFormatter));
+    }
+
+    public void setValue(final int value) {
+        mLocalTime = LocalTime.ofSecondOfDay(value);
+        setSummary(getSummary());
+    }
+
+    protected abstract int getSummaryResourceId();
+
+    protected abstract int getTimeSetting();
+
+    protected abstract void setTimeSetting(int secondOfDay);
+}
diff --git a/app/src/main/java/org/omnirom/control/widget/CustomDialogPreference.java b/app/src/main/java/org/omnirom/control/widget/CustomDialogPreference.java
new file mode 100644
index 0000000..ab9738b
--- /dev/null
+++ b/app/src/main/java/org/omnirom/control/widget/CustomDialogPreference.java
@@ -0,0 +1,205 @@
+/*
+ * SPDX-FileCopyrightText: 2015 The Android Open Source Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.omnirom.control.widget;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.preference.PreferenceDialogFragmentCompat;
+import androidx.preference.DialogPreference;
+
+public class CustomDialogPreference<T extends DialogInterface> extends DialogPreference {
+
+    private CustomPreferenceDialogFragment mFragment;
+
+    public CustomDialogPreference(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    public CustomDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    public CustomDialogPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public CustomDialogPreference(Context context) {
+        super(context);
+    }
+
+    public boolean isDialogOpen() {
+        return getDialog() != null && getDialog() instanceof Dialog && ((Dialog)getDialog()).isShowing();
+    }
+
+    public T getDialog() {
+        return (T) (mFragment != null ? mFragment.getDialog() : null);
+    }
+
+    protected void onPrepareDialogBuilder(AlertDialog.Builder builder,
+            DialogInterface.OnClickListener listener) {
+    }
+
+    protected void onDialogClosed(boolean positiveResult) {
+    }
+
+    protected void onClick(T dialog, int which) {
+    }
+
+    protected void onBindDialogView(View view) {
+    }
+
+    protected void onStart() {
+    }
+
+    protected void onStop() {
+    }
+
+    protected void onPause() {
+    }
+
+    protected void onResume() {
+    }
+
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        return null;
+    }
+
+    protected View onCreateDialogView(Context context) {
+        return null;
+    }
+
+    private void setFragment(CustomPreferenceDialogFragment fragment) {
+        mFragment = fragment;
+    }
+
+    protected boolean onDismissDialog(T dialog, int which) {
+        return true;
+    }
+
+    public static class CustomPreferenceDialogFragment extends PreferenceDialogFragmentCompat {
+
+        public static CustomPreferenceDialogFragment newInstance(String key) {
+            final CustomPreferenceDialogFragment fragment = new CustomPreferenceDialogFragment();
+            final Bundle b = new Bundle(1);
+            b.putString(ARG_KEY, key);
+            fragment.setArguments(b);
+            return fragment;
+        }
+
+        private CustomDialogPreference getCustomizablePreference() {
+            return (CustomDialogPreference) getPreference();
+        }
+
+        private class OnDismissListener implements View.OnClickListener {
+            private final int mWhich;
+            private final DialogInterface mDialog;
+
+            public OnDismissListener(DialogInterface dialog, int which) {
+                mWhich = which;
+                mDialog = dialog;
+            }
+
+            @Override
+            public void onClick(View view) {
+                CustomPreferenceDialogFragment.this.onClick(mDialog, mWhich);
+                if (getCustomizablePreference().onDismissDialog(mDialog, mWhich)) {
+                    mDialog.dismiss();
+                }
+            }
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            if (getDialog() instanceof AlertDialog) {
+                AlertDialog a = (AlertDialog)getDialog();
+                if (a.getButton(Dialog.BUTTON_NEUTRAL) != null) {
+                    a.getButton(Dialog.BUTTON_NEUTRAL).setOnClickListener(
+                            new OnDismissListener(a, Dialog.BUTTON_NEUTRAL));
+                }
+                if (a.getButton(Dialog.BUTTON_POSITIVE) != null) {
+                    a.getButton(Dialog.BUTTON_POSITIVE).setOnClickListener(
+                            new OnDismissListener(a, Dialog.BUTTON_POSITIVE));
+                }
+                if (a.getButton(Dialog.BUTTON_NEGATIVE) != null) {
+                    a.getButton(Dialog.BUTTON_NEGATIVE).setOnClickListener(
+                            new OnDismissListener(a, Dialog.BUTTON_NEGATIVE));
+                }
+            }
+            getCustomizablePreference().onStart();
+        }
+
+        @Override
+        public void onStop() {
+            super.onStop();
+            getCustomizablePreference().onStop();
+        }
+
+        @Override
+        public void onPause() {
+            super.onPause();
+            getCustomizablePreference().onPause();
+        }
+
+        @Override
+        public void onResume() {
+            super.onResume();
+            getCustomizablePreference().onResume();
+        }
+
+        @Override
+        protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+            super.onPrepareDialogBuilder(builder);
+            getCustomizablePreference().setFragment(this);
+            getCustomizablePreference().onPrepareDialogBuilder(builder, this);
+        }
+
+        @Override
+        public void onDialogClosed(boolean positiveResult) {
+            getCustomizablePreference().onDialogClosed(positiveResult);
+        }
+
+        @Override
+        protected void onBindDialogView(View view) {
+            super.onBindDialogView(view);
+            getCustomizablePreference().onBindDialogView(view);
+        }
+
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            super.onClick(dialog, which);
+            getCustomizablePreference().onClick(dialog, which);
+        }
+
+        @NonNull
+        @Override
+        public Dialog onCreateDialog(Bundle savedInstanceState) {
+            getCustomizablePreference().setFragment(this);
+            final Dialog sub = getCustomizablePreference().onCreateDialog(savedInstanceState);
+            if (sub == null) {
+                return super.onCreateDialog(savedInstanceState);
+            }
+            return sub;
+        }
+
+        @Override
+        protected View onCreateDialogView(Context context) {
+            final View v = getCustomizablePreference().onCreateDialogView(context);
+            if (v == null) {
+                return super.onCreateDialogView(context);
+            }
+            return v;
+        }
+    }
+}
diff --git a/app/src/main/res/drawable/ic_charging_control.xml b/app/src/main/res/drawable/ic_charging_control.xml
new file mode 100644
index 0000000..70f7f39
--- /dev/null
+++ b/app/src/main/res/drawable/ic_charging_control.xml
@@ -0,0 +1,17 @@
+<!--
+     SPDX-FileCopyrightText: 2018 The Android Open Source Project
+     SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:tint="?android:textColorPrimary"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="?android:attr/colorPrimary"
+        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="?android:attr/colorPrimary"
+        android:pathData="M11.17,18.42v-4.58H9.5l3.33,-6.25v4.58h1.67L11.17,18.42z"/>
+</vector>
diff --git a/app/src/main/res/layout/dialog_time.xml b/app/src/main/res/layout/dialog_time.xml
new file mode 100644
index 0000000..3572317
--- /dev/null
+++ b/app/src/main/res/layout/dialog_time.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:paddingLeft="16dp"
+    android:paddingRight="16dp"
+    android:orientation="vertical">
+
+    <TimePicker
+        android:id="@+id/time_picker"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:paddingTop="8dp" />
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/preference_charging_limit.xml b/app/src/main/res/layout/preference_charging_limit.xml
new file mode 100644
index 0000000..9c2b4fe
--- /dev/null
+++ b/app/src/main/res/layout/preference_charging_limit.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center_vertical"
+    android:paddingVertical="6dip"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:background="?android:attr/selectableItemBackground"
+    android:orientation="vertical">
+
+    <RelativeLayout
+        android:layout_width="wrap_content"
+        android:layout_height="0dp"
+        android:layout_weight="1">
+
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:textAppearance="?android:attr/textAppearanceLarge"
+            android:textColor="?android:attr/textColorPrimary"
+            android:ellipsize="marquee"
+            android:fadingEdge="horizontal" />
+
+        <TextView
+            android:id="@+id/value"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentEnd="true"
+            android:layout_centerVertical="true"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textColor="?android:attr/textColorSecondary"
+            android:singleLine="true" />
+
+    </RelativeLayout>
+
+    <SeekBar
+        android:id="@+id/seekbar_widget"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingVertical="15dip"
+        android:min="70"
+        android:max="100" />
+
+</LinearLayout>
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index bc76d8b..5823a9b 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -1,4 +1,21 @@
 <resources>
+    <!-- Values for the charging control modes -->
+    <string-array name="charging_control_mode_entries" translatable="false">
+        <item>@string/charging_control_mode_auto_title</item>
+        <item>@string/charging_control_mode_custom_title</item>
+    </string-array>
+    <string-array name="charging_control_mode_values" translatable="false">
+        <item>1</item>
+        <item>2</item>
+    </string-array>
+    <!-- Values for the additional charging control modes if fine-grained control is supported -->
+    <string-array name="charging_control_mode_entries_fine_grained_control" translatable="false">
+        <item>@string/charging_control_mode_limit_title</item>
+    </string-array>
+    <string-array name="charging_control_mode_values_fine_grained_control" translatable="false">
+        <item>3</item>
+    </string-array>
+
     <!-- LED behavior when battery is low -->
     <string-array name="led_battery_entries" translatable="false">
         <item>Solid when charging, flashing when not charging</item>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4cca5ed..9ba14ac 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -50,6 +50,26 @@
     <string name="device_settings_title">Device settings</string>
     <string name="device_settings_summary">Advanced device specific options</string>
 
+    <!-- Charging control settings -->
+    <string name="charging_control_title">Charging control</string>
+    <string name="charging_control_summary">Charging control settings</string>
+    <string name="charging_control_enable_title">Enable charging control</string>
+    <string name="charging_control_mode_title">Charging mode</string>
+    <string name="charging_control_mode_auto_title">Automatic schedule</string>
+    <string name="charging_control_mode_auto_summary">Automatically determine when to start charging based on alarms set</string>
+    <string name="charging_control_mode_custom_title">Custom schedule</string>
+    <string name="charging_control_mode_custom_summary">Set a target time to full charge</string>
+    <string name="charging_control_mode_limit_title">Limit charging</string>
+    <string name="charging_control_mode_limit_summary">Limit charging to a certain percentage</string>
+    <string name="charging_control_start_time_title">Start time</string>
+    <string name="charging_control_start_time_summary">Charging control activates when you start charging after %s</string>
+    <string name="charging_control_target_time_title">Target time to full charge</string>
+    <string name="charging_control_target_time_summary">Battery will be fully charged by %s</string>
+    <string name="charging_control_limit_title">Limit</string>
+    <string name="disabled">Disabled</string>
+    <string name="dlg_ok">OK</string>
+    <string name="enabled">Enabled</string>
+
     <!-- doze on charge -->
     <string name="doze_on_charge_title">Show ambient display when charging</string>
     <string name="doze_on_charge_summary">Wake screen when charging</string>
diff --git a/app/src/main/res/xml/charging_control_settings.xml b/app/src/main/res/xml/charging_control_settings.xml
new file mode 100644
index 0000000..bc0098f
--- /dev/null
+++ b/app/src/main/res/xml/charging_control_settings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    android:key="charging_control"
+    android:title="@string/charging_control_title">
+
+    <Preference
+        android:title="@string/reset_to_default"
+        android:widgetLayout="@layout/reset_default"
+        android:key="reset_to_default_key" />
+
+    <omnirom.preference.SystemSettingSwitchPreference
+        android:key="charging_control_enabled"
+        android:title="@string/charging_control_enable_title" />
+
+    <ListPreference
+        android:key="charging_control_mode"
+        android:title="@string/charging_control_mode_title"
+        android:entries="@array/charging_control_mode_entries"
+        android:entryValues="@array/charging_control_mode_values"
+        android:dependency="charging_control_enabled" />
+
+    <org.omnirom.control.health.StartTimePreference
+        android:key="charging_control_start_time"
+        android:title="@string/charging_control_start_time_title"
+        android:dependency="charging_control_enabled" />
+
+    <org.omnirom.control.health.TargetTimePreference
+        android:key="charging_control_target_time"
+        android:title="@string/charging_control_target_time_title"
+        android:dependency="charging_control_enabled" />
+
+    <org.omnirom.control.health.ChargingLimitPreference
+        android:key="charging_control_charging_limit"
+        android:title="@string/charging_control_limit_title"
+        android:dependency="charging_control_enabled" />
+
+</PreferenceScreen>