diff --git a/BoardConfig.mk b/BoardConfig.mk new file mode 100644 index 0000000..081cb31 --- /dev/null +++ b/BoardConfig.mk @@ -0,0 +1,36 @@ +# +# Copyright (C) 2021-2025 The LineageOS Project +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Partitions +BOARD_SUPER_PARTITION_SIZE := 13329498112 + +# Include the common OEM chipset BoardConfig. +include device/oneplus/sm8750-common/BoardConfigCommon.mk + +DEVICE_PATH := device/oneplus/dodge + +# Assert +TARGET_OTA_ASSERT_DEVICE := OP5D0DL1,OP5D55L1 + +# Display +TARGET_SCREEN_DENSITY := 640 + +# Kernel +TARGET_KERNEL_ADDITIONAL_FLAGS += CONFIG_DODGE_DTB=y + +# Properties +TARGET_ODM_PROP += $(DEVICE_PATH)/odm.prop +TARGET_SYSTEM_EXT_PROP += $(DEVICE_PATH)/system_ext.prop +TARGET_VENDOR_PROP += $(DEVICE_PATH)/vendor.prop + +# Recovery +TARGET_RECOVERY_UI_MARGIN_HEIGHT := 103 + +# SEPolicy +BOARD_VENDOR_SEPOLICY_DIRS += device/oneplus/dodge/sepolicy/vendor + +# Include the proprietary files BoardConfig. +include vendor/oneplus/dodge/BoardConfigVendor.mk diff --git a/DeviceSettings/AndroidManifest.xml b/DeviceSettings/AndroidManifest.xml index 2976355..f9e7cb4 100644 --- a/DeviceSettings/AndroidManifest.xml +++ b/DeviceSettings/AndroidManifest.xml @@ -111,6 +111,12 @@ android:resource="@string/bypass_charging_summary" /> + + + + + + + + + + + diff --git a/DeviceSettings/res/drawable/ic_custom_seekbar_minus.xml b/DeviceSettings/res/drawable/ic_custom_seekbar_minus.xml new file mode 100644 index 0000000..a117dfe --- /dev/null +++ b/DeviceSettings/res/drawable/ic_custom_seekbar_minus.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/DeviceSettings/res/drawable/ic_custom_seekbar_plus.xml b/DeviceSettings/res/drawable/ic_custom_seekbar_plus.xml new file mode 100644 index 0000000..e52c915 --- /dev/null +++ b/DeviceSettings/res/drawable/ic_custom_seekbar_plus.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/DeviceSettings/res/drawable/ic_custom_seekbar_reset.xml b/DeviceSettings/res/drawable/ic_custom_seekbar_reset.xml new file mode 100644 index 0000000..b527b47 --- /dev/null +++ b/DeviceSettings/res/drawable/ic_custom_seekbar_reset.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/DeviceSettings/res/drawable/ic_waiting.xml b/DeviceSettings/res/drawable/ic_waiting.xml new file mode 100644 index 0000000..b8306f3 --- /dev/null +++ b/DeviceSettings/res/drawable/ic_waiting.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/DeviceSettings/res/layout/custom_seekbar_preference.xml b/DeviceSettings/res/layout/custom_seekbar_preference.xml new file mode 100644 index 0000000..a2dd9c9 --- /dev/null +++ b/DeviceSettings/res/layout/custom_seekbar_preference.xml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DeviceSettings/res/values/attrs.xml b/DeviceSettings/res/values/attrs.xml new file mode 100644 index 0000000..c68738e --- /dev/null +++ b/DeviceSettings/res/values/attrs.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/DeviceSettings/res/values/strings.xml b/DeviceSettings/res/values/strings.xml index 2bc06dc..208720d 100644 --- a/DeviceSettings/res/values/strings.xml +++ b/DeviceSettings/res/values/strings.xml @@ -81,7 +81,20 @@ Bypass charging is not supported on this device Enabled Disabled +<<<<<<< HEAD:DeviceSettings/res/values/strings.xml Coudn\'t enable Bypass Charging. +======= + It was not possible to change Bypass Charging status. Please, try again later. + Target level + Battery will charge until it reaches target level, then bypass charging will be activated. + Battery will charge until it reaches target level + + + Value: %s + by default + Default value: %s\nLong tap to set + Default value is set +>>>>>>> 96046be (DeviceSettings: rework bypass charging):device-settings/res/values/strings.xml GameBar diff --git a/DeviceSettings/res/xml/bypass_charging_settings.xml b/DeviceSettings/res/xml/bypass_charging_settings.xml index 1be278f..3989044 100644 --- a/DeviceSettings/res/xml/bypass_charging_settings.xml +++ b/DeviceSettings/res/xml/bypass_charging_settings.xml @@ -1,14 +1,35 @@ - - + - + + + + + + + + + diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/Constants.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/Constants.java index 3f1b031..4e97f2d 100644 --- a/DeviceSettings/src/org/lineageos/device/DeviceSettings/Constants.java +++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/Constants.java @@ -29,14 +29,14 @@ import android.media.AudioManager; public class Constants { - public static final String SLIDER_STATE - = "/proc/tristatekey/tri_state"; + /* Alert Slider */ + public static final String NODE_SLIDER_STATE = "/proc/tristatekey/tri_state"; - public static final String NOTIF_SLIDER_PANEL_KEY = "notification_slider"; - public static final String NOTIF_SLIDER_USAGE_KEY = "slider_usage"; - public static final String NOTIF_SLIDER_ACTION_TOP_KEY = "action_top_position"; - public static final String NOTIF_SLIDER_ACTION_MIDDLE_KEY = "action_middle_position"; - public static final String NOTIF_SLIDER_ACTION_BOTTOM_KEY = "action_bottom_position"; + public static final String KEY_NOTIF_SLIDER_PANEL = "notification_slider"; + public static final String KEY_NOTIF_SLIDER_USAGE = "slider_usage"; + public static final String KEY_NOTIF_SLIDER_ACTION_TOP = "action_top_position"; + public static final String KEY_NOTIF_SLIDER_ACTION_MIDDLE = "action_middle_position"; + public static final String KEY_NOTIF_SLIDER_ACTION_BOTTOM = "action_bottom_position"; public static final String EXTRA_SLIDER_USAGE = "usage"; public static final String EXTRA_SLIDER_ACTIONS = "actions"; @@ -76,4 +76,17 @@ public class Constants { // Holds -> mapping public static final Map sBooleanNodePreferenceMap = new HashMap<>(); public static final Map sStringNodePreferenceMap = new HashMap<>(); + + /* OnePulse PWM */ + public static final String NODE_ONEPULSE_PWM = "/sys/kernel/oplus_display/pwm_onepulse"; + public static final String KEY_ONEPULSE_PWM = "onepulse_pwm"; + + /* Bypass Charging */ + public static final String NODE_BYPASS_CHARGING = "/sys/class/oplus_chg/battery/mmi_charging_enable"; + public static final String KEY_BYPASS_CHARGING = "bypass_charging"; + public static final String KEY_BYPASS_CHARGING_TARGET = "bypass_charging_target"; + + public static final int BYPASS_OFF = 0; + public static final int BYPASS_WAITING = 1; + public static final int BYPASS_ON = 2; } diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/DeviceSettings.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/DeviceSettings.java index 73e0052..c76b2d4 100644 --- a/DeviceSettings/src/org/lineageos/device/DeviceSettings/DeviceSettings.java +++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/DeviceSettings.java @@ -98,17 +98,30 @@ public class DeviceSettings extends SettingsBasePreferenceFragment // mUSB2FastChargeModeSwitch.setEnabled(false); // } +<<<<<<< HEAD:DeviceSettings/src/org/lineageos/device/DeviceSettings/DeviceSettings.java +======= + mOnePulsePWMSwitch = (SwitchPreferenceCompat) findPreference(Constants.KEY_ONEPULSE_PWM); + if (Utils.fileWritable(Constants.NODE_ONEPULSE_PWM)) { + mOnePulsePWMSwitch.setEnabled(true); + mOnePulsePWMSwitch.setChecked(sharedPrefs.getBoolean(Constants.KEY_ONEPULSE_PWM, + Utils.getFileValueAsBoolean(Constants.NODE_ONEPULSE_PWM, false))); + mOnePulsePWMSwitch.setOnPreferenceChangeListener(this); + } else { + mOnePulsePWMSwitch.setEnabled(false); + } + +>>>>>>> 96046be (DeviceSettings: rework bypass charging):device-settings/src/org/lineageos/device/settings/DeviceSettings.java initNotificationSliderPreference(); } private void initNotificationSliderPreference() { - registerPreferenceListener(Constants.NOTIF_SLIDER_USAGE_KEY); - registerPreferenceListener(Constants.NOTIF_SLIDER_ACTION_TOP_KEY); - registerPreferenceListener(Constants.NOTIF_SLIDER_ACTION_MIDDLE_KEY); - registerPreferenceListener(Constants.NOTIF_SLIDER_ACTION_BOTTOM_KEY); + registerPreferenceListener(Constants.KEY_NOTIF_SLIDER_USAGE); + registerPreferenceListener(Constants.KEY_NOTIF_SLIDER_ACTION_TOP); + registerPreferenceListener(Constants.KEY_NOTIF_SLIDER_ACTION_MIDDLE); + registerPreferenceListener(Constants.KEY_NOTIF_SLIDER_ACTION_BOTTOM); ListPreference usagePref = (ListPreference) findPreference( - Constants.NOTIF_SLIDER_USAGE_KEY); + Constants.KEY_NOTIF_SLIDER_USAGE); handleSliderUsageChange(usagePref.getValue()); } @@ -138,18 +151,28 @@ public class DeviceSettings extends SettingsBasePreferenceFragment // Utils.writeValue(FILE_FAST_CHARGE, enabled ? "1" : "0"); // return true; // } +<<<<<<< HEAD:DeviceSettings/src/org/lineageos/device/DeviceSettings/DeviceSettings.java +======= + if (preference == mOnePulsePWMSwitch) { + boolean enabled = (Boolean) newValue; + SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + sharedPrefs.edit().putBoolean(Constants.KEY_ONEPULSE_PWM, enabled).commit(); + Utils.writeValue(Constants.NODE_ONEPULSE_PWM, enabled ? "1" : "0"); + return true; + } +>>>>>>> 96046be (DeviceSettings: rework bypass charging):device-settings/src/org/lineageos/device/settings/DeviceSettings.java String key = preference.getKey(); switch (key) { - case Constants.NOTIF_SLIDER_USAGE_KEY: + case Constants.KEY_NOTIF_SLIDER_USAGE: return handleSliderUsageChange((String) newValue) && handleSliderUsageDefaultsChange((String) newValue) && notifySliderUsageChange((String) newValue); - case Constants.NOTIF_SLIDER_ACTION_TOP_KEY: + case Constants.KEY_NOTIF_SLIDER_ACTION_TOP: return notifySliderActionChange(0, (String) newValue); - case Constants.NOTIF_SLIDER_ACTION_MIDDLE_KEY: + case Constants.KEY_NOTIF_SLIDER_ACTION_MIDDLE: return notifySliderActionChange(1, (String) newValue); - case Constants.NOTIF_SLIDER_ACTION_BOTTOM_KEY: + case Constants.KEY_NOTIF_SLIDER_ACTION_BOTTOM: return notifySliderActionChange(2, (String) newValue); default: break; @@ -252,11 +275,11 @@ public class DeviceSettings extends SettingsBasePreferenceFragment private boolean updateSliderActions(int entriesResId, int entryValuesResId) { String[] entries = getResources().getStringArray(entriesResId); String[] entryValues = getResources().getStringArray(entryValuesResId); - return updateSliderPreference(Constants.NOTIF_SLIDER_ACTION_TOP_KEY, + return updateSliderPreference(Constants.KEY_NOTIF_SLIDER_ACTION_TOP, entries, entryValues) && - updateSliderPreference(Constants.NOTIF_SLIDER_ACTION_MIDDLE_KEY, + updateSliderPreference(Constants.KEY_NOTIF_SLIDER_ACTION_MIDDLE, entries, entryValues) && - updateSliderPreference(Constants.NOTIF_SLIDER_ACTION_BOTTOM_KEY, + updateSliderPreference(Constants.KEY_NOTIF_SLIDER_ACTION_BOTTOM, entries, entryValues); } @@ -266,11 +289,11 @@ public class DeviceSettings extends SettingsBasePreferenceFragment return false; } - return updateSliderPreferenceValue(Constants.NOTIF_SLIDER_ACTION_TOP_KEY, + return updateSliderPreferenceValue(Constants.KEY_NOTIF_SLIDER_ACTION_TOP, defaults[0]) && - updateSliderPreferenceValue(Constants.NOTIF_SLIDER_ACTION_MIDDLE_KEY, + updateSliderPreferenceValue(Constants.KEY_NOTIF_SLIDER_ACTION_MIDDLE, defaults[1]) && - updateSliderPreferenceValue(Constants.NOTIF_SLIDER_ACTION_BOTTOM_KEY, + updateSliderPreferenceValue(Constants.KEY_NOTIF_SLIDER_ACTION_BOTTOM, defaults[2]); } @@ -300,15 +323,15 @@ public class DeviceSettings extends SettingsBasePreferenceFragment ListPreference p; p = (ListPreference) findPreference( - Constants.NOTIF_SLIDER_ACTION_TOP_KEY); + Constants.KEY_NOTIF_SLIDER_ACTION_TOP); actions[0] = Integer.parseInt(p.getValue()); p = (ListPreference) findPreference( - Constants.NOTIF_SLIDER_ACTION_MIDDLE_KEY); + Constants.KEY_NOTIF_SLIDER_ACTION_MIDDLE); actions[1] = Integer.parseInt(p.getValue()); p = (ListPreference) findPreference( - Constants.NOTIF_SLIDER_ACTION_BOTTOM_KEY); + Constants.KEY_NOTIF_SLIDER_ACTION_BOTTOM); actions[2] = Integer.parseInt(p.getValue()); return actions; @@ -322,7 +345,7 @@ public class DeviceSettings extends SettingsBasePreferenceFragment private boolean notifySliderActionChange(int index, String value) { ListPreference p = (ListPreference) findPreference( - Constants.NOTIF_SLIDER_USAGE_KEY); + Constants.KEY_NOTIF_SLIDER_USAGE); int usage = Integer.parseInt(p.getValue()); int[] actions = getCurrentSliderActions(); @@ -348,7 +371,7 @@ public class DeviceSettings extends SettingsBasePreferenceFragment SharedPreferences prefs = context.getSharedPreferences( context.getPackageName() + "_preferences", Context.MODE_PRIVATE); - String usage = prefs.getString(Constants.NOTIF_SLIDER_USAGE_KEY, + String usage = prefs.getString(Constants.KEY_NOTIF_SLIDER_USAGE, res.getString(R.string.config_defaultNotificationSliderUsage)); int defaultsResId = getDefaultResIdForUsage(usage); @@ -362,19 +385,19 @@ public class DeviceSettings extends SettingsBasePreferenceFragment } String actionTop = prefs.getString( - Constants.NOTIF_SLIDER_ACTION_TOP_KEY, defaults[0]); + Constants.KEY_NOTIF_SLIDER_ACTION_TOP, defaults[0]); String actionMiddle = prefs.getString( - Constants.NOTIF_SLIDER_ACTION_MIDDLE_KEY, defaults[1]); + Constants.KEY_NOTIF_SLIDER_ACTION_MIDDLE, defaults[1]); String actionBottom = prefs.getString( - Constants.NOTIF_SLIDER_ACTION_BOTTOM_KEY, defaults[2]); + Constants.KEY_NOTIF_SLIDER_ACTION_BOTTOM, defaults[2]); prefs.edit() - .putString(Constants.NOTIF_SLIDER_USAGE_KEY, usage) - .putString(Constants.NOTIF_SLIDER_ACTION_TOP_KEY, actionTop) - .putString(Constants.NOTIF_SLIDER_ACTION_MIDDLE_KEY, actionMiddle) - .putString(Constants.NOTIF_SLIDER_ACTION_BOTTOM_KEY, actionBottom) + .putString(Constants.KEY_NOTIF_SLIDER_USAGE, usage) + .putString(Constants.KEY_NOTIF_SLIDER_ACTION_TOP, actionTop) + .putString(Constants.KEY_NOTIF_SLIDER_ACTION_MIDDLE, actionMiddle) + .putString(Constants.KEY_NOTIF_SLIDER_ACTION_BOTTOM, actionBottom) .commit(); sendUpdateBroadcast(context, Integer.parseInt(usage), new int[] { @@ -393,6 +416,18 @@ public class DeviceSettings extends SettingsBasePreferenceFragment // } // } +<<<<<<< HEAD:DeviceSettings/src/org/lineageos/device/DeviceSettings/DeviceSettings.java +======= + public static void restoreOnePulsePwmSetting(Context context) { + if (Utils.fileWritable(Constants.NODE_ONEPULSE_PWM)) { + SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean value = sharedPrefs.getBoolean(Constants.KEY_ONEPULSE_PWM, + Utils.getFileValueAsBoolean(Constants.NODE_ONEPULSE_PWM, false)); + Utils.writeValue(Constants.NODE_ONEPULSE_PWM, value ? "1" : "0"); + } + } + +>>>>>>> 96046be (DeviceSettings: rework bypass charging):device-settings/src/org/lineageos/device/settings/DeviceSettings.java private static int getDefaultResIdForUsage(String usage) { switch (usage) { case Constants.NOTIF_SLIDER_FOR_NOTIFICATION: diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/SliderControllerBase.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/SliderControllerBase.java index 80da35b..d5ebdf1 100644 --- a/DeviceSettings/src/org/lineageos/device/DeviceSettings/SliderControllerBase.java +++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/SliderControllerBase.java @@ -80,7 +80,7 @@ public abstract class SliderControllerBase { } try { - int state = Integer.parseInt(FileUtils.readOneLine(Constants.SLIDER_STATE).trim()); + int state = Integer.parseInt(FileUtils.readOneLine(Constants.NODE_SLIDER_STATE).trim()); ret = processAction(mActions[state - 1]); if (ret > 0 && notify) { sendUpdateBroadcast(context, state - 1, ret); diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingController.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingController.java index af62ab6..ac197ea 100644 --- a/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingController.java +++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingController.java @@ -17,53 +17,30 @@ package org.lineageos.device.DeviceSettings.bypasschrg; -import static lineageos.health.HealthInterface.MODE_AUTO; -import static lineageos.health.HealthInterface.MODE_LIMIT; - import android.content.ContentResolver; import android.content.Context; -import android.content.SharedPreferences; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.Handler; -import android.provider.Settings; +import android.os.BatteryManager; import android.util.Log; import android.widget.Toast; import androidx.preference.PreferenceManager; +import org.lineageos.device.DeviceSettings.Constants; import org.lineageos.device.DeviceSettings.R; import org.lineageos.device.DeviceSettings.utils.FileUtils; -/** - * This class is implemented to coexist with Lineage Charging Control (CC). - * Bypass Charging will override (disable) CC, while it's enabled. - * CC status will be restored, when Bypass Charging is disabled. - * Any user changes to CC settings, while Bypass Charging is enabled, - * will override Bypass Charging settings. - */ public class BypassChargingController { - private static final boolean DEBUG = false; - + private static final boolean DEBUG = true; private static final String TAG = "BypassChargingController"; - private static final String BYPASS_CHARGING_NODE = "/sys/class/oplus_chg/battery/mmi_charging_enable"; - private static final String KEY_BYPASS_CHARGING_ENABLED = "bypass_charging_enabled"; - - // Bypass modes private static final String BYPASS_CHARGING_ENABLED = "0"; private static final String BYPASS_CHARGING_DISABLED = "1"; + private static final String KEY_BATTERY_LEVEL = "current_battery_level"; - private static final int CC_LIMIT_MIN = 10; - private static final int CC_LIMIT_MAX = 100; - private static final int CC_LIMIT_DEF = 80; - - // Charging Control settings - private static final String KEY_CHARGING_CONTROL_ENABLED = "charging_control_enabled"; - private static final String KEY_CHARGING_CONTROL_MODE = "charging_control_mode"; - private static final String KEY_CHARGING_CONTROL_LIMIT = "charging_control_charging_limit"; + private int mBatteryLevel; private Context mContext; private ContentResolver mContentResolver; + private final Object mLock = new Object(); private static BypassChargingController sInstance; public static synchronized BypassChargingController getInstance(Context context) { @@ -76,37 +53,45 @@ public class BypassChargingController { private BypassChargingController(Context context) { mContext = context.getApplicationContext(); mContentResolver = mContext.getContentResolver(); + mBatteryLevel = getLevelFromIntent(); + if (isValidLevel(mBatteryLevel)) { + saveCurrentBatteryLevel(mBatteryLevel); + } } - private final ContentObserver mSettingsObserver = new ContentObserver(new Handler()) { - @Override - public void onChange(boolean selfChange, Uri uri) { - switch(uri.getLastPathSegment()) { - case KEY_CHARGING_CONTROL_ENABLED: - case KEY_CHARGING_CONTROL_MODE: - case KEY_CHARGING_CONTROL_LIMIT: - break; + // Called from Service when battery level changes + public void onBatteryLevelChanged(int level) { + synchronized (mLock) { + if (isValidLevel(level) && (mBatteryLevel == -1 || mBatteryLevel != level)) { + mBatteryLevel = level; + saveCurrentBatteryLevel(level); + maybeEnableBypassCharging(); + if (DEBUG) Log.d(TAG, "Battery level changed (service): " + level + "%"); } } - }; - - public boolean isBypassChargingSupported() { - return isNodeAccessible(BYPASS_CHARGING_NODE); } - public boolean isBypassChargingEnabled() { - try { - String value = FileUtils.readOneLine(BYPASS_CHARGING_NODE); - return value != null && BYPASS_CHARGING_ENABLED.equals(value); - } catch (Exception e) { - Log.e(TAG, "Failed to read bypass sysnode", e); - return false; + // get battery level using a sticky intent + private int getLevelFromIntent() { + android.content.IntentFilter filter = + new android.content.IntentFilter(android.content.Intent.ACTION_BATTERY_CHANGED); + android.content.Intent intent = mContext.registerReceiver(null, filter); + + if (intent == null) { + if (DEBUG) Log.w(TAG, "Sticky battery intent was null"); + return -1; } + + int extraLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); + int extraScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); + + return (extraLevel >= 0 && extraScale > 0) ? + (int)((extraLevel / (float)extraScale) * 100) : -1; } private boolean isNodeAccessible(String node) { try { - String value = FileUtils.readOneLine(node); + String status = FileUtils.readOneLine(node); return true; } catch (Exception e) { Log.e(TAG, "Node " + node + " not accessible", e); @@ -114,102 +99,146 @@ public class BypassChargingController { } } - private boolean writeToNode(String value) { - try { - FileUtils.writeLine(BYPASS_CHARGING_NODE, value); - } catch (Exception e) { - Log.e(TAG, "Failed to write bypass sysnode", e); + private boolean writeToNode(String status) { + synchronized (mLock) { + try { + FileUtils.writeLine(Constants.NODE_BYPASS_CHARGING, status); + } catch (Exception e) { + Log.e(TAG, "Failed to write bypass sysnode", e); + return false; + } + return true; + } + } + + private int readFromNode() { + synchronized (mLock) { + try { + String value = FileUtils.readOneLine(Constants.NODE_BYPASS_CHARGING); + return Integer.parseInt(value); + } catch (Exception e) { + Log.e(TAG, "Failed to read bypass sysnode", e); + return -1; + } + } + } + + public boolean isBypassChargingSupported() { + return isNodeAccessible(Constants.NODE_BYPASS_CHARGING); + } + + private void maybeEnableBypassCharging() { + if (mBatteryLevel >= getBypassChargingTarget() + && getBypassChargingStatus() == Constants.BYPASS_WAITING) { + if (writeToNode(BYPASS_CHARGING_ENABLED)) { + saveBypassChargingStatus(Constants.BYPASS_ON); + BypassChargingService.stop(mContext); + } + } + } + + private void maybeDisableBypassCharging(int target) { + if (mBatteryLevel < target + && getBypassChargingStatus() == Constants.BYPASS_ON) { + if (writeToNode(BYPASS_CHARGING_DISABLED)) { + saveBypassChargingStatus(Constants.BYPASS_WAITING); + BypassChargingService.start(mContext); + } + } + } + + public boolean enableBypassCharging() { + int level = getCurrentBatteryLevel(); + + if (!isValidLevel(level)) { + if (DEBUG) Log.w(TAG, "Cannot enable bypass: invalid battery level " + level); return false; } - return true; - } - public void setBypassCharging(boolean enable) { - if (enable) { - enableBypassCharging(); + if (getBypassChargingTarget() > level) { + saveBypassChargingStatus(Constants.BYPASS_WAITING); + BypassChargingService.start(mContext); + return true; } - else { - disableBypassCharging(); + else if (writeToNode(BYPASS_CHARGING_ENABLED)) { + saveBypassChargingStatus(Constants.BYPASS_ON); + BypassChargingService.stop(mContext); + return true; } + return false; } - private void enableBypassCharging() { - setChargingControlEnabled(true); - setChargingControlMode(MODE_LIMIT); - setChargingControlLimit(CC_LIMIT_MIN); - writeToNode(BYPASS_CHARGING_ENABLED); + public boolean disableBypassCharging() { + if (writeToNode(BYPASS_CHARGING_DISABLED)) { + saveBypassChargingStatus(Constants.BYPASS_OFF); + BypassChargingService.stop(mContext); + return true; + } + return false; } - public void disableBypassCharging() { - writeToNode(BYPASS_CHARGING_DISABLED); - setChargingControlLimit(CC_LIMIT_DEF); - // setChargingControlMode(MODE_AUTO); - setChargingControlEnabled(false); - } - - private void saveBypassChargingEnabled(boolean enabled) { + private void saveBypassChargingStatus(int status) { PreferenceManager.getDefaultSharedPreferences(mContext) .edit() - .putBoolean(KEY_BYPASS_CHARGING_ENABLED, enabled) - .commit(); + .putInt(Constants.KEY_BYPASS_CHARGING, status) + .apply(); } - private boolean isSavedBypassChargingEnabled() { + public int getBypassChargingStatus() { return PreferenceManager.getDefaultSharedPreferences(mContext) - .getBoolean(KEY_BYPASS_CHARGING_ENABLED, false); + .getInt(Constants.KEY_BYPASS_CHARGING, Constants.BYPASS_OFF); } - private void backupChargingControlSettings() { + public void setBypassChargingTarget(int target) { + if (target >= 0 && target <= 100) { + saveBypassChargingTarget(target); + if (getBypassChargingStatus() == Constants.BYPASS_ON) { + maybeDisableBypassCharging(target); + } + else { + maybeEnableBypassCharging(); + } + } + } + + private void saveBypassChargingTarget(int target) { PreferenceManager.getDefaultSharedPreferences(mContext) .edit() - .putInt(KEY_CHARGING_CONTROL_MODE, getChargingControlMode()) - .putInt(KEY_CHARGING_CONTROL_LIMIT, getChargingControlLimit()) - .putBoolean(KEY_CHARGING_CONTROL_ENABLED, isChargingControlEnabled()) - .commit(); + .putInt(Constants.KEY_BYPASS_CHARGING_TARGET, target) + .apply(); } - private void restoreChargingControlSettings() { - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(mContext); - setChargingControlMode(sharedPreferences.getInt( - KEY_CHARGING_CONTROL_LIMIT, CC_LIMIT_DEF)); - setChargingControlMode(sharedPreferences.getInt( - KEY_CHARGING_CONTROL_MODE, MODE_AUTO)); - setChargingControlEnabled(sharedPreferences.getBoolean( - KEY_CHARGING_CONTROL_ENABLED, false)); + public int getBypassChargingTarget() { + return PreferenceManager.getDefaultSharedPreferences(mContext) + .getInt(Constants.KEY_BYPASS_CHARGING_TARGET, 0); } - private boolean isChargingControlEnabled() { - return Settings.System.getInt(mContentResolver, - KEY_CHARGING_CONTROL_ENABLED, 0) != 0; - } - - private void setChargingControlEnabled(boolean enabled) { - Settings.System.putInt(mContentResolver, - KEY_CHARGING_CONTROL_ENABLED, enabled ? 1 : 0); - } - - private int getChargingControlMode() { - return Settings.System.getInt(mContentResolver, - KEY_CHARGING_CONTROL_MODE, MODE_AUTO); - } - - private void setChargingControlMode(int mode) { - Settings.System.putInt(mContentResolver, - KEY_CHARGING_CONTROL_MODE, mode); - } - - private int getChargingControlLimit() { - return Settings.System.getInt(mContentResolver, - KEY_CHARGING_CONTROL_LIMIT, CC_LIMIT_DEF); - } - - private void setChargingControlLimit(int limit) { - if (limit < CC_LIMIT_MIN || limit > CC_LIMIT_MAX) { - return; + private void saveCurrentBatteryLevel(int level) { + synchronized (mLock) { + if (isValidLevel(level)) { + PreferenceManager.getDefaultSharedPreferences(mContext) + .edit() + .putInt(KEY_BATTERY_LEVEL, level) + .apply(); + } else { + if (DEBUG) Log.w(TAG, "Attempted to save invalid battery level: " + level); + } } - Settings.System.putInt(mContentResolver, - KEY_CHARGING_CONTROL_LIMIT, limit); + } + + public int getCurrentBatteryLevel() { + synchronized (mLock) { + int level = PreferenceManager.getDefaultSharedPreferences(mContext) + .getInt(KEY_BATTERY_LEVEL, -1); + if (!isValidLevel(level)) { + if (DEBUG) Log.w(TAG, "Battery level is invalid or not set: " + level); + } + return level; + } + } + + private boolean isValidLevel(int level) { + return level >= 0 && level <= 100; } private void showToast(int resId) { diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingFragment.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingFragment.java index 4c5f460..3c68296 100644 --- a/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingFragment.java +++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingFragment.java @@ -21,12 +21,12 @@ import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.TwoStatePreference; +import org.lineageos.device.DeviceSettings.Constants; import org.lineageos.device.DeviceSettings.R; +import org.lineageos.device.DeviceSettings.preferences.CustomSeekBarPreference; public class BypassChargingFragment extends PreferenceFragmentCompat { - private static final String KEY_BYPASS_CHARGING = "bypass_charging"; - @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.bypass_charging_settings, rootKey); @@ -35,16 +35,33 @@ public class BypassChargingFragment extends PreferenceFragmentCompat { BypassChargingController.getInstance(getContext()); boolean bypassSupported = bypassController.isBypassChargingSupported(); - TwoStatePreference bypassPreference = findPreference(KEY_BYPASS_CHARGING); - bypassPreference.setEnabled(bypassSupported); - if (bypassSupported) { - bypassPreference.setChecked(bypassController.isBypassChargingEnabled()); - bypassPreference.setOnPreferenceChangeListener((pref, newValue) -> { - bypassController.setBypassCharging((boolean) newValue); - return true; - }); - } else { - bypassPreference.setSummary(R.string.bypass_charging_unavailable); + TwoStatePreference bypassPreference = findPreference(Constants.KEY_BYPASS_CHARGING); + if (bypassPreference != null) { + bypassPreference.setEnabled(bypassSupported); + if (bypassSupported) { + bypassPreference.setChecked(bypassController.getBypassChargingStatus() != Constants.BYPASS_OFF); + bypassPreference.setOnPreferenceChangeListener((pref, newValue) -> { + boolean enable = (boolean) newValue; + if (enable) { + bypassController.enableBypassCharging(); + } + else { + bypassController.disableBypassCharging(); + } + return true; + }); + } else { + bypassPreference.setSummary(R.string.bypass_charging_unavailable); + } + } + + CustomSeekBarPreference targetPreference = findPreference(Constants.KEY_BYPASS_CHARGING_TARGET); + if (targetPreference != null) { + targetPreference.setValue(bypassController.getBypassChargingTarget()); + targetPreference.setOnPreferenceChangeListener((pref, newValue) -> { + bypassController.setBypassChargingTarget((int) newValue); + return true; + }); } } } diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingService.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingService.java new file mode 100644 index 0000000..4d387ac --- /dev/null +++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingService.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2025 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.device.DeviceSettings.bypasschrg; + +import static android.os.BatteryManager.EXTRA_LEVEL; +import static android.os.BatteryManager.EXTRA_SCALE; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.IBinder; +import android.os.UserHandle; +import android.util.Log; + +import androidx.core.app.NotificationCompat; + +import org.lineageos.device.DeviceSettings.Constants; +import org.lineageos.device.DeviceSettings.R; + +public class BypassChargingService extends Service { + public static final String CHANNEL_ID = "bypass_charging_channel"; + public static final int NOTIF_ID = 1337; + private static final String TAG = "BypassChargingService"; + + private BypassChargingController mController; + private BroadcastReceiver mBatteryReceiver; + + @Override + public void onCreate() { + super.onCreate(); + mController = BypassChargingController.getInstance(getApplicationContext()); + createNotificationChannel(); + startForeground(NOTIF_ID, buildNotification()); + registerBatteryReceiver(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + unregisterBatteryReceiver(); + stopForeground(true); + } + + @Override + public IBinder onBind(Intent intent) { + return null; // Not a bound service + } + + private void registerBatteryReceiver() { + if (mBatteryReceiver != null) return; + mBatteryReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int extraLevel = intent.getIntExtra(EXTRA_LEVEL, -1); + int extraScale = intent.getIntExtra(EXTRA_SCALE, -1); + int level = (extraLevel >= 0 && extraScale > 0) ? + (int) ((extraLevel / (float) extraScale) * 100) : -1; + + Log.d(TAG, "Battery level in service: " + level); + mController.onBatteryLevelChanged(level); + } + }; + IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + registerReceiver(mBatteryReceiver, filter); + } + + private void unregisterBatteryReceiver() { + if (mBatteryReceiver != null) { + try { + unregisterReceiver(mBatteryReceiver); + } catch (Exception e) {} + mBatteryReceiver = null; + } + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + getString(R.string.bypass_charging_title), + NotificationManager.IMPORTANCE_LOW + ); + channel.setDescription(getString(R.string.bypass_charging_waiting)); + NotificationManager manager = getSystemService(NotificationManager.class); + if (manager != null) manager.createNotificationChannel(channel); + } + } + + private Notification buildNotification() { + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.bypass_charging_title)) + .setContentText(getString(R.string.bypass_charging_waiting)) + .setSmallIcon(R.drawable.ic_bypass_charging) + .setOngoing(true) + .setPriority(NotificationCompat.PRIORITY_LOW); + return builder.build(); + } + + // Static method to start/stop service from anywhere + public static void start(Context context) { + Intent intent = new Intent(context, BypassChargingService.class); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundServiceAsUser(intent, UserHandle.CURRENT); + } else { + context.startService(intent); + } + } + + public static void stop(Context context) { + Intent intent = new Intent(context, BypassChargingService.class); + context.stopService(intent); + } +} diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingTile.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingTile.java index 6dce7b7..43c85dc 100644 --- a/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingTile.java +++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingTile.java @@ -16,9 +16,12 @@ package org.lineageos.device.DeviceSettings.bypasschrg; +import android.graphics.drawable.Icon; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; + +import org.lineageos.device.DeviceSettings.Constants; import org.lineageos.device.DeviceSettings.R; public class BypassChargingTile extends TileService { @@ -35,26 +38,42 @@ public class BypassChargingTile extends TileService { @Override public void onStartListening() { - mEnabled = mBypassController.isBypassChargingEnabled(); - updateTileState(); + int status = mBypassController.getBypassChargingStatus(); + mEnabled = status != Constants.BYPASS_OFF; + updateTileState(status); } @Override public void onClick() { - if (mEnabled == mBypassController.isBypassChargingEnabled()) { - mEnabled = !mEnabled; - updateTileState(); - mBypassController.setBypassCharging(mEnabled); + boolean enabled = mBypassController.getBypassChargingStatus() != Constants.BYPASS_OFF; + if (mEnabled == enabled) { + boolean success; + if (mEnabled) { + success = mBypassController.disableBypassCharging() ? true : false; + } + else { + success = mBypassController.enableBypassCharging() ? true : false; + } + if (success) { + mEnabled = !mEnabled; + updateTileState(mBypassController.getBypassChargingStatus()); + } } } - private void updateTileState() { + private void updateTileState(int status) { Tile tile = getQsTile(); if (tile == null) return; - tile.setState(mEnabled ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE); + tile.setState(status==Constants.BYPASS_OFF ? Tile.STATE_INACTIVE : Tile.STATE_ACTIVE); tile.setLabel(getString(R.string.bypass_charging_title)); tile.setContentDescription(getString(R.string.bypass_charging_summary)); + if (status==Constants.BYPASS_WAITING) { + tile.setIcon(Icon.createWithResource(this, R.drawable.ic_bypass_waiting)); + } + else { + tile.setIcon(Icon.createWithResource(this, R.drawable.ic_bypass_charging)); + } tile.updateTile(); } } diff --git a/DeviceSettings/src/org/lineageos/device/settings/preferences/CustomSeekBarPreference.java b/DeviceSettings/src/org/lineageos/device/settings/preferences/CustomSeekBarPreference.java new file mode 100644 index 0000000..26fa30c --- /dev/null +++ b/DeviceSettings/src/org/lineageos/device/settings/preferences/CustomSeekBarPreference.java @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2016-2023 crDroid Android 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.device.settings.preferences; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.PorterDuff; +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.*; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.ImageView; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.Toast; + +import org.lineageos.device.DeviceSettings.R; +import com.android.settingslib.Utils; + +public class CustomSeekBarPreference extends Preference implements SeekBar.OnSeekBarChangeListener { + protected final String TAG = getClass().getName(); + private static final String SETTINGS_NS = "http://schemas.android.com/apk/res/com.android.settings"; + protected static final String ANDROIDNS = "http://schemas.android.com/apk/res/android"; + + protected int mInterval = 1; + protected boolean mShowSign = false; + protected String mUnits = ""; + protected boolean mContinuousUpdates = false; + + protected int mMinValue = 0; + protected int mMaxValue = 100; + protected boolean mDefaultValueExists = false; + protected int mDefaultValue; + protected boolean mDefaultValueTextExists = false; + protected String mDefaultValueText; + + protected int mValue; + + protected TextView mValueTextView; + protected ImageView mResetImageView; + protected ImageView mMinusImageView; + protected ImageView mPlusImageView; + protected SeekBar mSeekBar; + + protected boolean mTrackingTouch = false; + protected int mTrackingValue; + + public CustomSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomSeekBarPreference); + try { + mShowSign = a.getBoolean(R.styleable.CustomSeekBarPreference_showSign, mShowSign); + String units = a.getString(R.styleable.CustomSeekBarPreference_units); + if (units != null) + mUnits = " " + units; + mContinuousUpdates = a.getBoolean(R.styleable.CustomSeekBarPreference_continuousUpdates, mContinuousUpdates); + String defaultValueText = a.getString(R.styleable.CustomSeekBarPreference_defaultValueText); + mDefaultValueTextExists = defaultValueText != null && !defaultValueText.isEmpty(); + if (mDefaultValueTextExists) { + mDefaultValueText = defaultValueText; + } + } finally { + a.recycle(); + } + + try { + String newInterval = attrs.getAttributeValue(SETTINGS_NS, "interval"); + if (newInterval != null) + mInterval = Integer.parseInt(newInterval); + } catch (Exception e) { + Log.e(TAG, "Invalid interval value", e); + } + mMinValue = attrs.getAttributeIntValue(SETTINGS_NS, "min", mMinValue); + mMaxValue = attrs.getAttributeIntValue(ANDROIDNS, "max", mMaxValue); + if (mMaxValue < mMinValue) + mMaxValue = mMinValue; + String defaultValue = attrs.getAttributeValue(ANDROIDNS, "defaultValue"); + mDefaultValueExists = defaultValue != null && !defaultValue.isEmpty(); + if (mDefaultValueExists) { + mDefaultValue = getLimitedValue(Integer.parseInt(defaultValue)); + mValue = mDefaultValue; + } else { + mValue = mMinValue; + } + + mSeekBar = new SeekBar(context, attrs); + setLayoutResource(R.layout.custom_seekbar_preference); + } + + public CustomSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CustomSeekBarPreference(Context context, AttributeSet attrs) { + this(context, attrs, TypedArrayUtils.getAttr(context, + androidx.preference.R.attr.seekBarPreferenceStyle, + com.android.internal.R.attr.seekBarPreferenceStyle)); + } + + public CustomSeekBarPreference(Context context) { + this(context, null); + } + + @Override + public void onDependencyChanged(Preference dependency, boolean disableDependent) { + super.onDependencyChanged(dependency, disableDependent); + this.setShouldDisableView(true); + if (mSeekBar != null) + mSeekBar.setEnabled(!disableDependent); + if (mResetImageView != null) + mResetImageView.setEnabled(!disableDependent); + if (mPlusImageView != null) + mPlusImageView.setEnabled(!disableDependent); + if (mMinusImageView != null) + mMinusImageView.setEnabled(!disableDependent); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + try + { + // move our seekbar to the new view we've been given + ViewParent oldContainer = mSeekBar.getParent(); + ViewGroup newContainer = (ViewGroup) holder.findViewById(R.id.seekbar); + if (oldContainer != newContainer) { + // remove the seekbar from the old view + if (oldContainer != null) { + ((ViewGroup) oldContainer).removeView(mSeekBar); + } + // remove the existing seekbar (there may not be one) and add ours + newContainer.removeAllViews(); + newContainer.addView(mSeekBar, ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + } catch (Exception ex) { + Log.e(TAG, "Error binding view: " + ex.toString()); + } + + mSeekBar.setMax(getSeekValue(mMaxValue)); + mSeekBar.setProgress(getSeekValue(mValue)); + mSeekBar.setEnabled(isEnabled()); + + mValueTextView = (TextView) holder.findViewById(R.id.value); + mResetImageView = (ImageView) holder.findViewById(R.id.reset); + mMinusImageView = (ImageView) holder.findViewById(R.id.minus); + mPlusImageView = (ImageView) holder.findViewById(R.id.plus); + + updateValueViews(); + + mSeekBar.setOnSeekBarChangeListener(this); + mResetImageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Toast.makeText(getContext(), getContext().getString(R.string.custom_seekbar_default_value_to_set, getTextValue(mDefaultValue)), + Toast.LENGTH_LONG).show(); + } + }); + mResetImageView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + setValue(mDefaultValue, true); + return true; + } + }); + mMinusImageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + setValue(mValue - mInterval, true); + } + }); + mMinusImageView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + setValue(mMaxValue - mMinValue > mInterval * 2 && mMaxValue + mMinValue < mValue * 2 ? Math.floorDiv(mMaxValue + mMinValue, 2) : mMinValue, true); + return true; + } + }); + mPlusImageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + setValue(mValue + mInterval, true); + } + }); + mPlusImageView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + setValue(mMaxValue - mMinValue > mInterval * 2 && mMaxValue + mMinValue > mValue * 2 ? -1 * Math.floorDiv(-1 * (mMaxValue + mMinValue), 2) : mMaxValue, true); + return true; + } + }); + } + + protected int getLimitedValue(int v) { + return v < mMinValue ? mMinValue : (v > mMaxValue ? mMaxValue : v); + } + + protected int getSeekValue(int v) { + return 0 - Math.floorDiv(mMinValue - v, mInterval); + } + + protected String getTextValue(int v) { + if (mDefaultValueTextExists && mDefaultValueExists && v == mDefaultValue) { + return mDefaultValueText; + } + return (mShowSign && v > 0 ? "+" : "") + String.valueOf(v) + mUnits; + } + + protected void updateValueViews() { + if (mValueTextView != null) { + if (!mTrackingTouch || mContinuousUpdates) { + if (mDefaultValueTextExists && mDefaultValueExists && mValue == mDefaultValue) { + mValueTextView.setText(mDefaultValueText + " (" + + getContext().getString(R.string.custom_seekbar_default_value) + ")"); + } else { + mValueTextView.setText(getContext().getString(R.string.custom_seekbar_value, getTextValue(mValue)) + + (mDefaultValueExists && mValue == mDefaultValue ? " (" + + getContext().getString(R.string.custom_seekbar_default_value) + ")" : "")); + } + } else { + if (mDefaultValueTextExists && mDefaultValueExists && mTrackingValue == mDefaultValue) { + mValueTextView.setText("[" + mDefaultValueText + "]"); + } else { + mValueTextView.setText(getContext().getString(R.string.custom_seekbar_value, "[" + getTextValue(mTrackingValue) + "]")); + } + } + } + if (mResetImageView != null) { + if (!mDefaultValueExists || mValue == mDefaultValue || mTrackingTouch) + mResetImageView.setVisibility(View.INVISIBLE); + else + mResetImageView.setVisibility(View.VISIBLE); + } + if (mMinusImageView != null) { + if (mValue == mMinValue || mTrackingTouch) { + mMinusImageView.setClickable(false); + mMinusImageView.setColorFilter(Utils.getColorAttrDefaultColor(getContext(), android.R.attr.textColorTertiary), + PorterDuff.Mode.SRC_IN); + } else { + mMinusImageView.setClickable(true); + mMinusImageView.clearColorFilter(); + } + } + if (mPlusImageView != null) { + if (mValue == mMaxValue || mTrackingTouch) { + mPlusImageView.setClickable(false); + mPlusImageView.setColorFilter(Utils.getColorAttrDefaultColor(getContext(), android.R.attr.textColorTertiary), + PorterDuff.Mode.SRC_IN); + } else { + mPlusImageView.setClickable(true); + mPlusImageView.clearColorFilter(); + } + } + } + + protected void changeValue(int newValue) { + // for subclasses + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + int newValue = getLimitedValue(mMinValue + (progress * mInterval)); + if (mTrackingTouch && !mContinuousUpdates) { + mTrackingValue = newValue; + updateValueViews(); + } else if (mValue != newValue) { + // change rejected, revert to the previous value + if (!callChangeListener(newValue)) { + mSeekBar.setProgress(getSeekValue(mValue)); + return; + } + // change accepted, store it + changeValue(newValue); + persistInt(newValue); + + mValue = newValue; + updateValueViews(); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + mTrackingValue = mValue; + mTrackingTouch = true; + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mTrackingTouch = false; + if (!mContinuousUpdates) + onProgressChanged(mSeekBar, getSeekValue(mTrackingValue), false); + notifyChanged(); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + if (restoreValue) + mValue = getPersistedInt(mValue); + } + + @Override + public void setDefaultValue(Object defaultValue) { + if (defaultValue instanceof Integer) + setDefaultValue((Integer) defaultValue, mSeekBar != null); + else + setDefaultValue(defaultValue == null ? (String) null : defaultValue.toString(), mSeekBar != null); + } + + public void setDefaultValue(int newValue, boolean update) { + newValue = getLimitedValue(newValue); + if (!mDefaultValueExists || mDefaultValue != newValue) { + mDefaultValueExists = true; + mDefaultValue = newValue; + if (update) + updateValueViews(); + } + } + + public void setDefaultValue(String newValue, boolean update) { + if (mDefaultValueExists && (newValue == null || newValue.isEmpty())) { + mDefaultValueExists = false; + if (update) + updateValueViews(); + } else if (newValue != null && !newValue.isEmpty()) { + setDefaultValue(Integer.parseInt(newValue), update); + } + } + + public void setValue(int newValue) { + mValue = getLimitedValue(newValue); + if (mSeekBar != null) mSeekBar.setProgress(getSeekValue(mValue)); + } + + public void setValue(int newValue, boolean update) { + newValue = getLimitedValue(newValue); + if (mValue != newValue) { + if (update) + mSeekBar.setProgress(getSeekValue(newValue)); + else + mValue = newValue; + } + } + + public int getValue() { + return mValue; + } + + public void setMax(int max) { + mMaxValue = max < mMinValue ? mMinValue : max; + mSeekBar.setMax(mMaxValue); + } + + public void setMin(int min) { + mMinValue = min > mMaxValue ? mMaxValue : min; + mSeekBar.setMin(mMinValue); + } + + public void refresh(int newValue) { + setValue(newValue, mSeekBar != null); + } +} diff --git a/device-settings/res/xml/bypass_charging_settings.xml b/device-settings/res/xml/bypass_charging_settings.xml new file mode 100644 index 0000000..9fbce17 --- /dev/null +++ b/device-settings/res/xml/bypass_charging_settings.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + diff --git a/device-settings/src/org/lineageos/device/settings/preferences/CustomSeekBarPreference.java b/device-settings/src/org/lineageos/device/settings/preferences/CustomSeekBarPreference.java new file mode 100644 index 0000000..be63103 --- /dev/null +++ b/device-settings/src/org/lineageos/device/settings/preferences/CustomSeekBarPreference.java @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2016-2023 crDroid Android 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.device.settings.preferences; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.PorterDuff; +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.*; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.ImageView; +import android.widget.SeekBar; +import android.widget.TextView; +import android.widget.Toast; + +import org.lineageos.device.settings.R; +import com.android.settingslib.Utils; + +public class CustomSeekBarPreference extends Preference implements SeekBar.OnSeekBarChangeListener { + protected final String TAG = getClass().getName(); + private static final String SETTINGS_NS = "http://schemas.android.com/apk/res/com.android.settings"; + protected static final String ANDROIDNS = "http://schemas.android.com/apk/res/android"; + + protected int mInterval = 1; + protected boolean mShowSign = false; + protected String mUnits = ""; + protected boolean mContinuousUpdates = false; + + protected int mMinValue = 0; + protected int mMaxValue = 100; + protected boolean mDefaultValueExists = false; + protected int mDefaultValue; + protected boolean mDefaultValueTextExists = false; + protected String mDefaultValueText; + + protected int mValue; + + protected TextView mValueTextView; + protected ImageView mResetImageView; + protected ImageView mMinusImageView; + protected ImageView mPlusImageView; + protected SeekBar mSeekBar; + + protected boolean mTrackingTouch = false; + protected int mTrackingValue; + + public CustomSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomSeekBarPreference); + try { + mShowSign = a.getBoolean(R.styleable.CustomSeekBarPreference_showSign, mShowSign); + String units = a.getString(R.styleable.CustomSeekBarPreference_units); + if (units != null) + mUnits = " " + units; + mContinuousUpdates = a.getBoolean(R.styleable.CustomSeekBarPreference_continuousUpdates, mContinuousUpdates); + String defaultValueText = a.getString(R.styleable.CustomSeekBarPreference_defaultValueText); + mDefaultValueTextExists = defaultValueText != null && !defaultValueText.isEmpty(); + if (mDefaultValueTextExists) { + mDefaultValueText = defaultValueText; + } + } finally { + a.recycle(); + } + + try { + String newInterval = attrs.getAttributeValue(SETTINGS_NS, "interval"); + if (newInterval != null) + mInterval = Integer.parseInt(newInterval); + } catch (Exception e) { + Log.e(TAG, "Invalid interval value", e); + } + mMinValue = attrs.getAttributeIntValue(SETTINGS_NS, "min", mMinValue); + mMaxValue = attrs.getAttributeIntValue(ANDROIDNS, "max", mMaxValue); + if (mMaxValue < mMinValue) + mMaxValue = mMinValue; + String defaultValue = attrs.getAttributeValue(ANDROIDNS, "defaultValue"); + mDefaultValueExists = defaultValue != null && !defaultValue.isEmpty(); + if (mDefaultValueExists) { + mDefaultValue = getLimitedValue(Integer.parseInt(defaultValue)); + mValue = mDefaultValue; + } else { + mValue = mMinValue; + } + + mSeekBar = new SeekBar(context, attrs); + setLayoutResource(R.layout.custom_seekbar_preference); + } + + public CustomSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public CustomSeekBarPreference(Context context, AttributeSet attrs) { + this(context, attrs, TypedArrayUtils.getAttr(context, + androidx.preference.R.attr.seekBarPreferenceStyle, + com.android.internal.R.attr.seekBarPreferenceStyle)); + } + + public CustomSeekBarPreference(Context context) { + this(context, null); + } + + @Override + public void onDependencyChanged(Preference dependency, boolean disableDependent) { + super.onDependencyChanged(dependency, disableDependent); + this.setShouldDisableView(true); + if (mSeekBar != null) + mSeekBar.setEnabled(!disableDependent); + if (mResetImageView != null) + mResetImageView.setEnabled(!disableDependent); + if (mPlusImageView != null) + mPlusImageView.setEnabled(!disableDependent); + if (mMinusImageView != null) + mMinusImageView.setEnabled(!disableDependent); + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + try + { + // move our seekbar to the new view we've been given + ViewParent oldContainer = mSeekBar.getParent(); + ViewGroup newContainer = (ViewGroup) holder.findViewById(R.id.seekbar); + if (oldContainer != newContainer) { + // remove the seekbar from the old view + if (oldContainer != null) { + ((ViewGroup) oldContainer).removeView(mSeekBar); + } + // remove the existing seekbar (there may not be one) and add ours + newContainer.removeAllViews(); + newContainer.addView(mSeekBar, ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + } catch (Exception ex) { + Log.e(TAG, "Error binding view: " + ex.toString()); + } + + mSeekBar.setMax(getSeekValue(mMaxValue)); + mSeekBar.setProgress(getSeekValue(mValue)); + mSeekBar.setEnabled(isEnabled()); + + mValueTextView = (TextView) holder.findViewById(R.id.value); + mResetImageView = (ImageView) holder.findViewById(R.id.reset); + mMinusImageView = (ImageView) holder.findViewById(R.id.minus); + mPlusImageView = (ImageView) holder.findViewById(R.id.plus); + + updateValueViews(); + + mSeekBar.setOnSeekBarChangeListener(this); + mResetImageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Toast.makeText(getContext(), getContext().getString(R.string.custom_seekbar_default_value_to_set, getTextValue(mDefaultValue)), + Toast.LENGTH_LONG).show(); + } + }); + mResetImageView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + setValue(mDefaultValue, true); + return true; + } + }); + mMinusImageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + setValue(mValue - mInterval, true); + } + }); + mMinusImageView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + setValue(mMaxValue - mMinValue > mInterval * 2 && mMaxValue + mMinValue < mValue * 2 ? Math.floorDiv(mMaxValue + mMinValue, 2) : mMinValue, true); + return true; + } + }); + mPlusImageView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + setValue(mValue + mInterval, true); + } + }); + mPlusImageView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + setValue(mMaxValue - mMinValue > mInterval * 2 && mMaxValue + mMinValue > mValue * 2 ? -1 * Math.floorDiv(-1 * (mMaxValue + mMinValue), 2) : mMaxValue, true); + return true; + } + }); + } + + protected int getLimitedValue(int v) { + return v < mMinValue ? mMinValue : (v > mMaxValue ? mMaxValue : v); + } + + protected int getSeekValue(int v) { + return 0 - Math.floorDiv(mMinValue - v, mInterval); + } + + protected String getTextValue(int v) { + if (mDefaultValueTextExists && mDefaultValueExists && v == mDefaultValue) { + return mDefaultValueText; + } + return (mShowSign && v > 0 ? "+" : "") + String.valueOf(v) + mUnits; + } + + protected void updateValueViews() { + if (mValueTextView != null) { + if (!mTrackingTouch || mContinuousUpdates) { + if (mDefaultValueTextExists && mDefaultValueExists && mValue == mDefaultValue) { + mValueTextView.setText(mDefaultValueText + " (" + + getContext().getString(R.string.custom_seekbar_default_value) + ")"); + } else { + mValueTextView.setText(getContext().getString(R.string.custom_seekbar_value, getTextValue(mValue)) + + (mDefaultValueExists && mValue == mDefaultValue ? " (" + + getContext().getString(R.string.custom_seekbar_default_value) + ")" : "")); + } + } else { + if (mDefaultValueTextExists && mDefaultValueExists && mTrackingValue == mDefaultValue) { + mValueTextView.setText("[" + mDefaultValueText + "]"); + } else { + mValueTextView.setText(getContext().getString(R.string.custom_seekbar_value, "[" + getTextValue(mTrackingValue) + "]")); + } + } + } + if (mResetImageView != null) { + if (!mDefaultValueExists || mValue == mDefaultValue || mTrackingTouch) + mResetImageView.setVisibility(View.INVISIBLE); + else + mResetImageView.setVisibility(View.VISIBLE); + } + if (mMinusImageView != null) { + if (mValue == mMinValue || mTrackingTouch) { + mMinusImageView.setClickable(false); + mMinusImageView.setColorFilter(Utils.getColorAttrDefaultColor(getContext(), android.R.attr.textColorTertiary), + PorterDuff.Mode.SRC_IN); + } else { + mMinusImageView.setClickable(true); + mMinusImageView.clearColorFilter(); + } + } + if (mPlusImageView != null) { + if (mValue == mMaxValue || mTrackingTouch) { + mPlusImageView.setClickable(false); + mPlusImageView.setColorFilter(Utils.getColorAttrDefaultColor(getContext(), android.R.attr.textColorTertiary), + PorterDuff.Mode.SRC_IN); + } else { + mPlusImageView.setClickable(true); + mPlusImageView.clearColorFilter(); + } + } + } + + protected void changeValue(int newValue) { + // for subclasses + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + int newValue = getLimitedValue(mMinValue + (progress * mInterval)); + if (mTrackingTouch && !mContinuousUpdates) { + mTrackingValue = newValue; + updateValueViews(); + } else if (mValue != newValue) { + // change rejected, revert to the previous value + if (!callChangeListener(newValue)) { + mSeekBar.setProgress(getSeekValue(mValue)); + return; + } + // change accepted, store it + changeValue(newValue); + persistInt(newValue); + + mValue = newValue; + updateValueViews(); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + mTrackingValue = mValue; + mTrackingTouch = true; + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mTrackingTouch = false; + if (!mContinuousUpdates) + onProgressChanged(mSeekBar, getSeekValue(mTrackingValue), false); + notifyChanged(); + } + + @Override + protected void onSetInitialValue(boolean restoreValue, Object defaultValue) { + if (restoreValue) + mValue = getPersistedInt(mValue); + } + + @Override + public void setDefaultValue(Object defaultValue) { + if (defaultValue instanceof Integer) + setDefaultValue((Integer) defaultValue, mSeekBar != null); + else + setDefaultValue(defaultValue == null ? (String) null : defaultValue.toString(), mSeekBar != null); + } + + public void setDefaultValue(int newValue, boolean update) { + newValue = getLimitedValue(newValue); + if (!mDefaultValueExists || mDefaultValue != newValue) { + mDefaultValueExists = true; + mDefaultValue = newValue; + if (update) + updateValueViews(); + } + } + + public void setDefaultValue(String newValue, boolean update) { + if (mDefaultValueExists && (newValue == null || newValue.isEmpty())) { + mDefaultValueExists = false; + if (update) + updateValueViews(); + } else if (newValue != null && !newValue.isEmpty()) { + setDefaultValue(Integer.parseInt(newValue), update); + } + } + + public void setValue(int newValue) { + mValue = getLimitedValue(newValue); + if (mSeekBar != null) mSeekBar.setProgress(getSeekValue(mValue)); + } + + public void setValue(int newValue, boolean update) { + newValue = getLimitedValue(newValue); + if (mValue != newValue) { + if (update) + mSeekBar.setProgress(getSeekValue(newValue)); + else + mValue = newValue; + } + } + + public int getValue() { + return mValue; + } + + public void setMax(int max) { + mMaxValue = max < mMinValue ? mMinValue : max; + mSeekBar.setMax(mMaxValue); + } + + public void setMin(int min) { + mMinValue = min > mMaxValue ? mMaxValue : min; + mSeekBar.setMin(mMinValue); + } + + public void refresh(int newValue) { + setValue(newValue, mSeekBar != null); + } +} diff --git a/device.mk b/device.mk new file mode 100644 index 0000000..6a09733 --- /dev/null +++ b/device.mk @@ -0,0 +1,70 @@ +# +# Copyright (C) 2021-2025 The LineageOS Project +# +# SPDX-License-Identifier: Apache-2.0 +# + +# AAPT +PRODUCT_AAPT_CONFIG := normal +PRODUCT_AAPT_PREF_CONFIG := xxxhdpi + +# Audio +PRODUCT_COPY_FILES += \ + $(LOCAL_PATH)/configs/audio/audio_policy_volumes.xml:$(TARGET_COPY_OUT_VENDOR)/etc/audio_policy_volumes.xml \ + $(LOCAL_PATH)/configs/audio/default_volume_tables.xml:$(TARGET_COPY_OUT_VENDOR)/etc/default_volume_tables.xml + +# Boot animation +TARGET_SCREEN_HEIGHT := 3168 +TARGET_SCREEN_WIDTH := 1440 + +# Display +PRODUCT_COPY_FILES += \ + $(LOCAL_PATH)/configs/display/displayconfig.xml:$(TARGET_COPY_OUT_VENDOR)/etc/displayconfig/display_id_4630946756802996883.xml + +# LiveDisplay +$(call soong_config_set,OPLUS_LINEAGE_LIVEDISPLAY_HAL,ENABLE_AF,false) +$(call soong_config_set,OPLUS_LINEAGE_LIVEDISPLAY_HAL,ENABLE_SE,false) + + +# Overlays +DEVICE_PACKAGE_OVERLAYS += \ + $(LOCAL_PATH)/overlay-lineage + +PRODUCT_PACKAGES += \ + OPlusFrameworksResTarget \ + OPlusSettingsProviderResTarget \ + OPlusSettingsResTarget \ + OPlusSystemUIResTarget + +# Power +$(call soong_config_set,qtipower,mode_ext_lib,power-ext-oplus) + +# PowerShare +PRODUCT_PACKAGES += \ + vendor.lineage.powershare-service.oplus + +# Regional properties +PRODUCT_COPY_FILES += \ + $(LOCAL_PATH)/recovery/root/vendor/odm/etc/23821/build.default.prop:$(TARGET_COPY_OUT_ODM)/etc/23821/build.default.prop \ + $(LOCAL_PATH)/recovery/root/vendor/odm/etc/23893/build.EU.prop:$(TARGET_COPY_OUT_ODM)/etc/23893/build.EU.prop \ + $(LOCAL_PATH)/recovery/root/vendor/odm/etc/23893/build.IN.prop:$(TARGET_COPY_OUT_ODM)/etc/23893/build.IN.prop \ + $(LOCAL_PATH)/recovery/root/vendor/odm/etc/23893/build.NA.prop:$(TARGET_COPY_OUT_ODM)/etc/23893/build.NA.prop \ + $(LOCAL_PATH)/recovery/root/vendor/odm/etc/23893/build.default.prop:$(TARGET_COPY_OUT_ODM)/etc/23893/build.default.prop + +# Soong namespaces +PRODUCT_SOONG_NAMESPACES += \ + $(LOCAL_PATH) + +# Touch features +$(call soong_config_set,OPLUS_LINEAGE_TOUCH_HAL,ENABLE_GM,true) +$(call soong_config_set,OPLUS_LINEAGE_TOUCH_HAL,ENABLE_HTPR,false) + +# Vibrator +$(call soong_config_set,OPLUS_LINEAGE_VIBRATOR_HAL,USE_EFFECT_STREAM,true) +$(call soong_config_set,OPLUS_LINEAGE_VIBRATOR_HAL,INCLUDE_DIR,$(LOCAL_PATH)/vibrator/include) + +# Inherit from the common OEM chipset makefile. +$(call inherit-product, device/oneplus/sm8750-common/common.mk) + +# Inherit from the proprietary files makefile. +$(call inherit-product, vendor/oneplus/dodge/dodge-vendor.mk) diff --git a/overlay-lineage/frameworks/base/core/res/res/values/config.xml b/overlay-lineage/frameworks/base/core/res/res/values/config.xml index 2e28aea..35322f9 100644 --- a/overlay-lineage/frameworks/base/core/res/res/values/config.xml +++ b/overlay-lineage/frameworks/base/core/res/res/values/config.xml @@ -16,4 +16,5 @@ true true + diff --git a/sepolicy/vendor/system_app.te b/sepolicy/vendor/system_app.te new file mode 100644 index 0000000..c47019d --- /dev/null +++ b/sepolicy/vendor/system_app.te @@ -0,0 +1,8 @@ +# Bypass Charging +allow system_app vendor_sysfs_usb_supply:file w_file_perms; + +# Gamebar +allow system_app { proc_stat vendor_sysfs_kgsl vendor_sysfs_kgsl_gpuclk }:file r_file_perms; + +# OnePulse PWM +rw_dir_file(system_app, vendor_sysfs_graphics);