diff --git a/DeviceSettings/Android.bp b/DeviceSettings/Android.bp
index 123c01c..005c551 100644
--- a/DeviceSettings/Android.bp
+++ b/DeviceSettings/Android.bp
@@ -19,7 +19,18 @@ android_app {
"org.lineageos.platform.internal",
],
+ required: [
+ "privapp-permissions-device_settings.xml",
+ ],
+
optimize: {
proguard_flags_files: ["proguard.flags"],
},
}
+
+prebuilt_etc {
+ name: "privapp-permissions-device_settings.xml",
+ src: "privapp-permissions-device_settings.xml",
+ sub_dir: "permissions",
+ system_ext_specific: true,
+}
diff --git a/DeviceSettings/AndroidManifest.xml b/DeviceSettings/AndroidManifest.xml
index b2bb563..2976355 100644
--- a/DeviceSettings/AndroidManifest.xml
+++ b/DeviceSettings/AndroidManifest.xml
@@ -26,6 +26,9 @@
+
+
+
@@ -35,13 +38,23 @@
android:label="@string/device_title"
android:defaultToDeviceProtectedStorage="true"
android:directBootAware="true"
- android:theme="@style/Theme.SubSettingsBase.Expressive"
+ android:theme="@style/Theme.DeviceSettings"
android:exported="true">
+
+
+
+
+
+
+
+
@@ -88,5 +101,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DeviceSettings/privapp-permissions-device_settings.xml b/DeviceSettings/privapp-permissions-device_settings.xml
new file mode 100644
index 0000000..3147def
--- /dev/null
+++ b/DeviceSettings/privapp-permissions-device_settings.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/DeviceSettings/res/drawable/banner_fade_gradient.xml b/DeviceSettings/res/drawable/banner_fade_gradient.xml
new file mode 100644
index 0000000..4567557
--- /dev/null
+++ b/DeviceSettings/res/drawable/banner_fade_gradient.xml
@@ -0,0 +1,6 @@
+
+
+
diff --git a/DeviceSettings/res/drawable/ic_bypass_charging.xml b/DeviceSettings/res/drawable/ic_bypass_charging.xml
new file mode 100644
index 0000000..68749f9
--- /dev/null
+++ b/DeviceSettings/res/drawable/ic_bypass_charging.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/DeviceSettings/res/drawable/ic_game_bar.xml b/DeviceSettings/res/drawable/ic_game_bar.xml
new file mode 100644
index 0000000..54f24ae
--- /dev/null
+++ b/DeviceSettings/res/drawable/ic_game_bar.xml
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/DeviceSettings/res/drawable/oplus_banner.png b/DeviceSettings/res/drawable/oplus_banner.png
new file mode 100644
index 0000000..b84bbac
Binary files /dev/null and b/DeviceSettings/res/drawable/oplus_banner.png differ
diff --git a/DeviceSettings/res/layout/activity_bypass_charging.xml b/DeviceSettings/res/layout/activity_bypass_charging.xml
new file mode 100644
index 0000000..9ee55f5
--- /dev/null
+++ b/DeviceSettings/res/layout/activity_bypass_charging.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/DeviceSettings/res/layout/activity_game_bar.xml b/DeviceSettings/res/layout/activity_game_bar.xml
new file mode 100644
index 0000000..c4964ba
--- /dev/null
+++ b/DeviceSettings/res/layout/activity_game_bar.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/DeviceSettings/res/layout/activity_game_bar_app_selector.xml b/DeviceSettings/res/layout/activity_game_bar_app_selector.xml
new file mode 100644
index 0000000..eec564c
--- /dev/null
+++ b/DeviceSettings/res/layout/activity_game_bar_app_selector.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/DeviceSettings/res/layout/banner_collapsing_toolbar.xml b/DeviceSettings/res/layout/banner_collapsing_toolbar.xml
new file mode 100644
index 0000000..b97f8e8
--- /dev/null
+++ b/DeviceSettings/res/layout/banner_collapsing_toolbar.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/DeviceSettings/res/layout/device_settings_activity.xml b/DeviceSettings/res/layout/device_settings_activity.xml
new file mode 100644
index 0000000..0ef57c0
--- /dev/null
+++ b/DeviceSettings/res/layout/device_settings_activity.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DeviceSettings/res/layout/game_bar.xml b/DeviceSettings/res/layout/game_bar.xml
new file mode 100644
index 0000000..e8948d9
--- /dev/null
+++ b/DeviceSettings/res/layout/game_bar.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/DeviceSettings/res/layout/game_bar_app_selector.xml b/DeviceSettings/res/layout/game_bar_app_selector.xml
new file mode 100644
index 0000000..6ce63b5
--- /dev/null
+++ b/DeviceSettings/res/layout/game_bar_app_selector.xml
@@ -0,0 +1,6 @@
+
+
diff --git a/DeviceSettings/res/layout/game_bar_app_selector_item.xml b/DeviceSettings/res/layout/game_bar_app_selector_item.xml
new file mode 100644
index 0000000..38c45e5
--- /dev/null
+++ b/DeviceSettings/res/layout/game_bar_app_selector_item.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DeviceSettings/res/values-ru-rRU/strings.xml b/DeviceSettings/res/values-ru-rRU/strings.xml
index 7005fad..79e361a 100644
--- a/DeviceSettings/res/values-ru-rRU/strings.xml
+++ b/DeviceSettings/res/values-ru-rRU/strings.xml
@@ -16,8 +16,8 @@
-->
- Настройки OnePlus
- Изменение особых настроек устройства OnePlus
+ Aston Settings
+ Extra settings for OnePlus Ace3 | 12R
Переключатель режима
@@ -69,4 +69,26 @@
Снять ограничения касаний
Улучшение в играх нескольких касаний
Может увеличить вероятность случайного прикосновения
+
+
+ Gaming
+ Extra settings for gamers
+
+
+ Bypass charging
+ Supply power directly to the phone
+ Bypass charging supplies power directly to the board, instead of the battery, helping to reduce device heat, when running demanding applications like games.
+ Bypass charging is not supported on this device
+ Enabled
+ Disabled
+ Coudn\'t enable Bypass Charging.
+
+
+ GameBar
+ Enable the system overlay (FPS, Temp, etc.)
+ Overlay permission is required
+ Overlay permission granted
+ Overlay permission denied
+ GameBar
+ Toggle the game overlay
diff --git a/DeviceSettings/res/values/arrays.xml b/DeviceSettings/res/values/arrays.xml
index 1ce60e9..bb5a5dc 100644
--- a/DeviceSettings/res/values/arrays.xml
+++ b/DeviceSettings/res/values/arrays.xml
@@ -111,4 +111,104 @@
- 64
- 63
+
+
+
+ - New API (Default)
+ - Legacy Sysfs
+
+
+ - new
+ - legacy
+
+
+
+
+ - Every 500ms
+ - Every second
+ - Every 2 seconds
+ - Every 5 seconds
+
+
+ - 500
+ - 1000
+ - 2000
+ - 5000
+
+
+
+
+ - Top Left
+ - Top Center
+ - Top Right
+ - Bottom Left
+ - Bottom Center
+ - Bottom Right
+ - Custom Draggable
+
+
+ - top_left
+ - top_center
+ - top_right
+ - bottom_left
+ - bottom_center
+ - bottom_right
+ - draggable
+
+
+
+
+ - White
+ - Crimson
+ - Fruit Salad
+ - Royal Blue
+ - Amber
+ - Teal
+ - Electric Violet
+ - Magenta
+
+
+ - #FFFFFF
+ - #DC143C
+ - #4CAF50
+ - #4169E1
+ - #FFBF00
+ - #008080
+ - #8A2BE2
+ - #FF1493
+
+
+
+
+ - Full
+ - Minimal
+
+
+ - full
+ - minimal
+
+
+
+
+ - Side-by-Side
+ - Stacked
+
+
+ - side_by_side
+ - stacked
+
+
+
+
+ - 1 second
+ - 3 seconds
+ - 5 seconds
+ - 10 seconds
+
+
+ - 1000
+ - 3000
+ - 5000
+ - 10000
+
diff --git a/DeviceSettings/res/values/strings.xml b/DeviceSettings/res/values/strings.xml
index dc98e72..b643744 100644
--- a/DeviceSettings/res/values/strings.xml
+++ b/DeviceSettings/res/values/strings.xml
@@ -16,8 +16,8 @@
-->
- OnePlus Settings
- Adjust OnePlus specific device settings
+ Aston Settings
+ Extra settings for OnePlus Ace3 | 12R
Alert slider
@@ -69,4 +69,26 @@
Unlimit edge touch
Improve your multi-touch experience in games
May increase the possibility of accidental touch
+
+
+ Gaming
+ Extra settings for gamers
+
+
+ Bypass charging
+ Supply power directly to the phone
+ Bypass charging supplies power directly to the board, instead of the battery, helping to reduce device heat, when running demanding applications like games.
+ Bypass charging is not supported on this device
+ Enabled
+ Disabled
+ Coudn\'t enable Bypass Charging.
+
+
+ GameBar
+ Enable the system overlay (FPS, Temp, etc.)
+ Overlay permission is required
+ Overlay permission granted
+ Overlay permission denied
+ GameBar
+ Toggle the game overlay
diff --git a/DeviceSettings/res/values/themes.xml b/DeviceSettings/res/values/themes.xml
new file mode 100644
index 0000000..dd06f5d
--- /dev/null
+++ b/DeviceSettings/res/values/themes.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/DeviceSettings/res/xml/bypass_charging_settings.xml b/DeviceSettings/res/xml/bypass_charging_settings.xml
new file mode 100644
index 0000000..1be278f
--- /dev/null
+++ b/DeviceSettings/res/xml/bypass_charging_settings.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/DeviceSettings/res/xml/game_bar_preferences.xml b/DeviceSettings/res/xml/game_bar_preferences.xml
new file mode 100644
index 0000000..6fea1af
--- /dev/null
+++ b/DeviceSettings/res/xml/game_bar_preferences.xml
@@ -0,0 +1,252 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DeviceSettings/res/xml/main.xml b/DeviceSettings/res/xml/main.xml
index 14dddeb..422cb86 100644
--- a/DeviceSettings/res/xml/main.xml
+++ b/DeviceSettings/res/xml/main.xml
@@ -93,4 +93,22 @@
android:title="@string/usb2_fc_title" />
-->
+
+
+
+
+
+
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/BootCompletedReceiver.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/BootCompletedReceiver.java
new file mode 100644
index 0000000..71787be
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/BootCompletedReceiver.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2015 The CyanogenMod Project
+ * 2017-2020 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;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.preference.PreferenceManager;
+
+import org.lineageos.device.DeviceSettings.gamebar.GameBar;
+import org.lineageos.device.DeviceSettings.gamebar.GameBarMonitorService;
+
+public class BootCompletedReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
+ restoreGameBarOverlayState(context);
+ }
+ }
+
+ private void restoreGameBarOverlayState(Context context) {
+ var prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean mainEnabled = prefs.getBoolean("game_bar_enable", false);
+ boolean autoEnabled = prefs.getBoolean("game_bar_auto_enable", false);
+ if (mainEnabled) {
+ // Restore game bar preferences
+ GameBar.getInstance(context).applyPreferences();
+ GameBar.getInstance(context).show();
+ }
+ if (autoEnabled) {
+ // Start GameBarMonitorService
+ Intent monitorIntent = new Intent(context, GameBarMonitorService.class);
+ context.startService(monitorIntent);
+ }
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/DeviceSettings.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/DeviceSettings.java
index cb598bf..73e0052 100644
--- a/DeviceSettings/src/org/lineageos/device/DeviceSettings/DeviceSettings.java
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/DeviceSettings.java
@@ -384,14 +384,14 @@ public class DeviceSettings extends SettingsBasePreferenceFragment
});
}
- public static void restoreFastChargeSetting(Context context) {
- if (Utils.fileWritable(FILE_FAST_CHARGE)) {
- SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
- boolean value = sharedPrefs.getBoolean(KEY_USB2_SWITCH,
- Utils.getFileValueAsBoolean(FILE_FAST_CHARGE, false));
- Utils.writeValue(FILE_FAST_CHARGE, value ? "1" : "0");
- }
- }
+ // public static void restoreFastChargeSetting(Context context) {
+ // if (Utils.fileWritable(FILE_FAST_CHARGE)) {
+ // SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+ // boolean value = sharedPrefs.getBoolean(KEY_USB2_SWITCH,
+ // Utils.getFileValueAsBoolean(FILE_FAST_CHARGE, false));
+ // Utils.writeValue(FILE_FAST_CHARGE, value ? "1" : "0");
+ // }
+ // }
private static int getDefaultResIdForUsage(String usage) {
switch (usage) {
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/DeviceSettingsActivity.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/DeviceSettingsActivity.java
index 8967fe9..ae09c83 100644
--- a/DeviceSettings/src/org/lineageos/device/DeviceSettings/DeviceSettingsActivity.java
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/DeviceSettingsActivity.java
@@ -16,29 +16,51 @@
package org.lineageos.device.DeviceSettings;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.DialogFragment;
-import android.app.Fragment;
-import android.content.DialogInterface;
import android.os.Bundle;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import androidx.preference.PreferenceFragment;
-import androidx.preference.PreferenceManager;
+import android.view.View;
import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity;
+import com.google.android.material.appbar.AppBarLayout;
+import com.google.android.material.appbar.CollapsingToolbarLayout;
+
public class DeviceSettingsActivity extends CollapsingToolbarBaseActivity {
+ private View bannerFadeOverlay;
+ private boolean pinned = false; // Set true to pin the fade overlay
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ setTitle(" ");
+ // Load your fragment as usual
getSupportFragmentManager().beginTransaction().replace(
- com.android.settingslib.collapsingtoolbar.R.id.content_frame,
- new DeviceSettings()).commit();
+ R.id.content_frame,
+ new DeviceSettings()).commit();
+
+ // Inject banner dynamically into CollapsingToolbarLayout
+ CollapsingToolbarLayout collapsingToolbar =
+ findViewById(R.id.collapsing_toolbar);
+ if (collapsingToolbar != null) {
+ View banner = getLayoutInflater().inflate(R.layout.banner_collapsing_toolbar, collapsingToolbar, false);
+
+ // You may want to insert at position 0 to ensure it's on top
+ collapsingToolbar.addView(banner, 0);
+
+ bannerFadeOverlay = banner.findViewById(R.id.bannerFadeOverlay);
+
+ // Animate fade overlay on scroll
+ AppBarLayout appBar = findViewById(R.id.app_bar);
+ if (appBar != null) {
+ appBar.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> {
+ if (bannerFadeOverlay == null) return;
+ int totalScrollRange = appBarLayout.getTotalScrollRange();
+ float offsetFraction = Math.abs(verticalOffset) / (float) totalScrollRange;
+ float maxAlpha = 0.8f;
+ float alpha = pinned ? maxAlpha : maxAlpha * (1 - offsetFraction);
+ bannerFadeOverlay.setAlpha(alpha);
+ });
+ }
+ }
}
}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/TileHandlerActivity.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/TileHandlerActivity.java
new file mode 100644
index 0000000..c3769cd
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/TileHandlerActivity.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2025 kenway215
+ *
+ * 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;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.service.quicksettings.TileService;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.lineageos.device.DeviceSettings.bypasschrg.BypassChargingActivity;
+import org.lineageos.device.DeviceSettings.bypasschrg.BypassChargingTile;
+import org.lineageos.device.DeviceSettings.gamebar.GameBarSettingsActivity;
+import org.lineageos.device.DeviceSettings.gamebar.GameBarTileService;
+
+public final class TileHandlerActivity extends Activity {
+ private static final String TAG = "TileHandlerActivity";
+
+ // Map QS Tile services to their corresponding activity
+ private static final Map> TILE_ACTIVITY_MAP = new HashMap<>();
+
+ static {
+ TILE_ACTIVITY_MAP.put(GameBarTileService.class.getName(), GameBarSettingsActivity.class);
+ TILE_ACTIVITY_MAP.put(BypassChargingTile.class.getName(), BypassChargingActivity.class);
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ final Intent intent = getIntent();
+ if (intent == null || !TileService.ACTION_QS_TILE_PREFERENCES.equals(intent.getAction())) {
+ Log.e(TAG, "Invalid or null intent received");
+ finish();
+ return;
+ }
+
+ final ComponentName qsTile = intent.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME);
+ if (qsTile == null) {
+ Log.e(TAG, "No QS tile component found in intent");
+ finish();
+ return;
+ }
+
+ final String qsName = qsTile.getClassName();
+ final Intent targetIntent = new Intent();
+
+ // Check if the tile is mapped to an activity
+ if (TILE_ACTIVITY_MAP.containsKey(qsName)) {
+ targetIntent.setClass(this, TILE_ACTIVITY_MAP.get(qsName));
+ Log.d(TAG, "Launching settings activity for QS tile: " + qsName);
+ } else {
+ // Default: Open app settings for the QS tile's package
+ final String packageName = qsTile.getPackageName();
+ if (packageName == null) {
+ Log.e(TAG, "QS tile package name is null");
+ finish();
+ return;
+ }
+ targetIntent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ targetIntent.setData(Uri.fromParts("package", packageName, null));
+ Log.d(TAG, "Opening app info for package: " + packageName);
+ }
+
+ // Ensure proper navigation behavior
+ targetIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
+ Intent.FLAG_ACTIVITY_CLEAR_TASK |
+ Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ startActivity(targetIntent);
+ finish();
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingActivity.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingActivity.java
new file mode 100644
index 0000000..8b9eeca
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingActivity.java
@@ -0,0 +1,33 @@
+/*
+ * 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 android.os.Bundle;
+
+import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity;
+
+import org.lineageos.device.DeviceSettings.R;
+
+public class BypassChargingActivity extends CollapsingToolbarBaseActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_bypass_charging);
+ setTitle(getString(R.string.bypass_charging_title));
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingController.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingController.java
new file mode 100644
index 0000000..af62ab6
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingController.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2025 The LineageOS Project
+ * Copyright (C) 2025 AlphaDroid
+ *
+ * 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 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.util.Log;
+import android.widget.Toast;
+import androidx.preference.PreferenceManager;
+
+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 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 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 Context mContext;
+ private ContentResolver mContentResolver;
+
+ private static BypassChargingController sInstance;
+ public static synchronized BypassChargingController getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new BypassChargingController(context);
+ }
+ return sInstance;
+ }
+
+ private BypassChargingController(Context context) {
+ mContext = context.getApplicationContext();
+ mContentResolver = mContext.getContentResolver();
+ }
+
+ 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;
+ }
+ }
+ };
+
+ 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;
+ }
+ }
+
+ private boolean isNodeAccessible(String node) {
+ try {
+ String value = FileUtils.readOneLine(node);
+ return true;
+ } catch (Exception e) {
+ Log.e(TAG, "Node " + node + " not accessible", e);
+ return false;
+ }
+ }
+
+ private boolean writeToNode(String value) {
+ try {
+ FileUtils.writeLine(BYPASS_CHARGING_NODE, value);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to write bypass sysnode", e);
+ return false;
+ }
+ return true;
+ }
+
+ public void setBypassCharging(boolean enable) {
+ if (enable) {
+ enableBypassCharging();
+ }
+ else {
+ disableBypassCharging();
+ }
+ }
+
+ private void enableBypassCharging() {
+ setChargingControlEnabled(true);
+ setChargingControlMode(MODE_LIMIT);
+ setChargingControlLimit(CC_LIMIT_MIN);
+ writeToNode(BYPASS_CHARGING_ENABLED);
+ }
+
+ public void disableBypassCharging() {
+ writeToNode(BYPASS_CHARGING_DISABLED);
+ setChargingControlLimit(CC_LIMIT_DEF);
+ // setChargingControlMode(MODE_AUTO);
+ setChargingControlEnabled(false);
+ }
+
+ private void saveBypassChargingEnabled(boolean enabled) {
+ PreferenceManager.getDefaultSharedPreferences(mContext)
+ .edit()
+ .putBoolean(KEY_BYPASS_CHARGING_ENABLED, enabled)
+ .commit();
+ }
+
+ private boolean isSavedBypassChargingEnabled() {
+ return PreferenceManager.getDefaultSharedPreferences(mContext)
+ .getBoolean(KEY_BYPASS_CHARGING_ENABLED, false);
+ }
+
+ private void backupChargingControlSettings() {
+ PreferenceManager.getDefaultSharedPreferences(mContext)
+ .edit()
+ .putInt(KEY_CHARGING_CONTROL_MODE, getChargingControlMode())
+ .putInt(KEY_CHARGING_CONTROL_LIMIT, getChargingControlLimit())
+ .putBoolean(KEY_CHARGING_CONTROL_ENABLED, isChargingControlEnabled())
+ .commit();
+ }
+
+ 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));
+ }
+
+ 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;
+ }
+ Settings.System.putInt(mContentResolver,
+ KEY_CHARGING_CONTROL_LIMIT, limit);
+ }
+
+ private void showToast(int resId) {
+ Toast.makeText(mContext, mContext.getString(resId),
+ Toast.LENGTH_LONG).show();
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingFragment.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingFragment.java
new file mode 100644
index 0000000..4c5f460
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingFragment.java
@@ -0,0 +1,50 @@
+/*
+ * 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 android.os.Bundle;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.TwoStatePreference;
+
+import org.lineageos.device.DeviceSettings.R;
+
+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);
+
+ BypassChargingController bypassController =
+ 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);
+ }
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingTile.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingTile.java
new file mode 100644
index 0000000..6dce7b7
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/bypasschrg/BypassChargingTile.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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 android.service.quicksettings.Tile;
+import android.service.quicksettings.TileService;
+
+import org.lineageos.device.DeviceSettings.R;
+
+public class BypassChargingTile extends TileService {
+
+
+ private BypassChargingController mBypassController;
+ private boolean mEnabled;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mBypassController = BypassChargingController.getInstance(this);
+ }
+
+ @Override
+ public void onStartListening() {
+ mEnabled = mBypassController.isBypassChargingEnabled();
+ updateTileState();
+ }
+
+ @Override
+ public void onClick() {
+ if (mEnabled == mBypassController.isBypassChargingEnabled()) {
+ mEnabled = !mEnabled;
+ updateTileState();
+ mBypassController.setBypassCharging(mEnabled);
+ }
+ }
+
+ private void updateTileState() {
+ Tile tile = getQsTile();
+ if (tile == null) return;
+
+ tile.setState(mEnabled ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE);
+ tile.setLabel(getString(R.string.bypass_charging_title));
+ tile.setContentDescription(getString(R.string.bypass_charging_summary));
+ tile.updateTile();
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/ForegroundAppDetector.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/ForegroundAppDetector.java
new file mode 100644
index 0000000..23d890d
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/ForegroundAppDetector.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.util.Log;
+
+import java.lang.reflect.Method;
+import java.util.List;
+
+public class ForegroundAppDetector {
+
+ private static final String TAG = "ForegroundAppDetector";
+
+ public static String getForegroundPackageName(Context context) {
+
+ String pkg = tryGetRunningTasks(context);
+ if (pkg != null) {
+ return pkg;
+ }
+ pkg = tryReflectActivityTaskManager();
+ if (pkg != null) {
+ return pkg;
+ }
+ return "Unknown";
+ }
+
+ private static String tryGetRunningTasks(Context context) {
+ try {
+ if (context.checkSelfPermission("android.permission.GET_TASKS")
+ == PackageManager.PERMISSION_GRANTED) {
+
+ ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ List tasks = am.getRunningTasks(1);
+ if (tasks != null && !tasks.isEmpty()) {
+ ActivityManager.RunningTaskInfo top = tasks.get(0);
+ if (top.topActivity != null) {
+ return top.topActivity.getPackageName();
+ }
+ }
+ } else {
+ Log.w(TAG, "GET_TASKS permission not granted to this system app?");
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "tryGetRunningTasks error: ", e);
+ }
+ return null;
+ }
+
+ private static String tryReflectActivityTaskManager() {
+ try {
+ Class> atmClass = Class.forName("android.app.ActivityTaskManager");
+ Method getServiceMethod = atmClass.getDeclaredMethod("getService");
+ getServiceMethod.setAccessible(true);
+ Object atmService = getServiceMethod.invoke(null);
+ Method getTasksMethod = atmService.getClass().getMethod("getTasks", int.class);
+ @SuppressWarnings("unchecked")
+ List> taskList = (List>) getTasksMethod.invoke(atmService, 1);
+ if (taskList != null && !taskList.isEmpty()) {
+
+ Object firstTask = taskList.get(0);
+
+ Class> rtiClass = firstTask.getClass();
+ Method getTopActivityMethod = rtiClass.getDeclaredMethod("getTopActivity");
+ Object compName = getTopActivityMethod.invoke(firstTask);
+ if (compName != null) {
+
+ Method getPackageNameMethod = compName.getClass().getMethod("getPackageName");
+ String pkgName = (String) getPackageNameMethod.invoke(compName);
+ return pkgName;
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "tryReflectActivityTaskManager error: ", e);
+ }
+ return null;
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBar.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBar.java
new file mode 100644
index 0000000..0a4aa94
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBar.java
@@ -0,0 +1,774 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import android.app.usage.UsageStatsManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.GradientDrawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.util.TypedValue;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.preference.PreferenceManager;
+
+import org.lineageos.device.DeviceSettings.R;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+public class GameBar {
+
+ private static GameBar sInstance;
+ public static synchronized GameBar getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new GameBar(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ private static final String FPS_PATH = "/sys/class/drm/sde-crtc-0/measured_fps";
+ private static final String BATTERY_TEMP_PATH = "/sys/class/power_supply/battery/temp";
+
+ private static final String PREF_KEY_X = "game_bar_x";
+ private static final String PREF_KEY_Y = "game_bar_y";
+
+ private final Context mContext;
+ private final WindowManager mWindowManager;
+ private final Handler mHandler;
+
+ private View mOverlayView;
+ private LinearLayout mRootLayout;
+ private WindowManager.LayoutParams mLayoutParams;
+ private boolean mIsShowing = false;
+
+ private int mTextSizeSp = 16;
+ private int mBackgroundAlpha = 128;
+ private int mCornerRadius = 16;
+ private int mPaddingDp = 12;
+ private String mTitleColorHex = "#FFFFFF";
+ private String mValueColorHex = "#FFFFFF";
+ private String mOverlayFormat = "full";
+ private String mPosition = "top_left";
+ private String mSplitMode = "stacked";
+ private int mUpdateIntervalMs = 1000;
+ private boolean mDraggable = false;
+
+ private boolean mShowBatteryTemp = false;
+ private boolean mShowCpuUsage = false;
+ private boolean mShowCpuClock = false;
+ private boolean mShowCpuTemp = false;
+ private boolean mShowRam = false;
+ private boolean mShowFps = false;
+
+ private boolean mShowGpuUsage = false;
+ private boolean mShowGpuClock = false;
+ private boolean mShowGpuTemp = false;
+
+ private boolean mLongPressEnabled = false;
+ private long mLongPressThresholdMs = 1000;
+ private boolean mPressActive = false;
+ private float mDownX, mDownY;
+ private static final float TOUCH_SLOP = 20f;
+
+ private GestureDetector mGestureDetector;
+ private boolean mDoubleTapCaptureEnabled = false;
+ private boolean mSingleTapToggleEnabled = false;
+ private GradientDrawable mBgDrawable;
+
+ private int mItemSpacingDp = 8;
+
+ private final Runnable mLongPressRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mPressActive) {
+ openOverlaySettings();
+ mPressActive = false;
+ }
+ }
+ };
+
+ private final Runnable mUpdateRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mIsShowing) {
+ updateStats();
+ mHandler.postDelayed(this, mUpdateIntervalMs);
+ }
+ }
+ };
+
+ private GameBar(Context context) {
+ mContext = context;
+ mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ mHandler = new Handler(Looper.getMainLooper());
+
+ mBgDrawable = new GradientDrawable();
+ applyBackgroundStyle();
+
+ mGestureDetector = new GestureDetector(mContext, new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ if (mDoubleTapCaptureEnabled) {
+ if (GameDataExport.getInstance().isCapturing()) {
+ GameDataExport.getInstance().stopCapture();
+ Toast.makeText(mContext, "Capture Stopped", Toast.LENGTH_SHORT).show();
+ } else {
+ GameDataExport.getInstance().startCapture();
+ Toast.makeText(mContext, "Capture Started", Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ }
+ return super.onDoubleTap(e);
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ if (mSingleTapToggleEnabled) {
+ mOverlayFormat = "full".equals(mOverlayFormat) ? "minimal" : "full";
+ PreferenceManager.getDefaultSharedPreferences(mContext)
+ .edit()
+ .putString("game_bar_format", mOverlayFormat)
+ .apply();
+ Toast.makeText(mContext, "Overlay Format: " + mOverlayFormat, Toast.LENGTH_SHORT).show();
+ updateStats();
+ return true;
+ }
+ return super.onSingleTapConfirmed(e);
+ }
+ });
+ }
+
+ public void applyPreferences() {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
+
+ mShowFps = prefs.getBoolean("game_bar_fps_enable", false);
+ mShowBatteryTemp = prefs.getBoolean("game_bar_temp_enable", false);
+ mShowCpuUsage = prefs.getBoolean("game_bar_cpu_usage_enable", false);
+ mShowCpuClock = prefs.getBoolean("game_bar_cpu_clock_enable", false);
+ mShowCpuTemp = prefs.getBoolean("game_bar_cpu_temp_enable", false);
+ mShowRam = prefs.getBoolean("game_bar_ram_enable", false);
+
+ mShowGpuUsage = prefs.getBoolean("game_bar_gpu_usage_enable", false);
+ mShowGpuClock = prefs.getBoolean("game_bar_gpu_clock_enable", false);
+ mShowGpuTemp = prefs.getBoolean("game_bar_gpu_temp_enable", false);
+
+ mDoubleTapCaptureEnabled = prefs.getBoolean("game_bar_doubletap_capture", false);
+ mSingleTapToggleEnabled = prefs.getBoolean("game_bar_single_tap_toggle", false);
+
+ updateSplitMode(prefs.getString("game_bar_split_mode", "stacked"));
+ updateTextSize(prefs.getInt("game_bar_text_size", 16));
+ updateBackgroundAlpha(prefs.getInt("game_bar_background_alpha", 128));
+ updateCornerRadius(prefs.getInt("game_bar_corner_radius", 16));
+ updatePadding(prefs.getInt("game_bar_padding", 12));
+ updateTitleColor(prefs.getString("game_bar_title_color", "#FFFFFF"));
+ updateValueColor(prefs.getString("game_bar_value_color", "#4CAF50"));
+ updateOverlayFormat(prefs.getString("game_bar_format", "full"));
+ updateUpdateInterval(prefs.getString("game_bar_update_interval", "1000"));
+ updatePosition(prefs.getString("game_bar_position", "top_left"));
+
+ int spacing = prefs.getInt("game_bar_item_spacing", 8);
+ updateItemSpacing(spacing);
+
+ mLongPressEnabled = prefs.getBoolean("game_bar_longpress_enable", false);
+ String lpTimeoutStr = prefs.getString("game_bar_longpress_timeout", "1000");
+ try {
+ long lpt = Long.parseLong(lpTimeoutStr);
+ setLongPressThresholdMs(lpt);
+ } catch (NumberFormatException ignored) {}
+ }
+
+ public void show() {
+ if (mIsShowing) return;
+
+ applyPreferences();
+
+ mLayoutParams = new WindowManager.LayoutParams(
+ WindowManager.LayoutParams.WRAP_CONTENT,
+ WindowManager.LayoutParams.WRAP_CONTENT,
+ WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSLUCENT
+ );
+
+ if ("draggable".equals(mPosition)) {
+ mDraggable = true;
+ loadSavedPosition(mLayoutParams);
+ if (mLayoutParams.x == 0 && mLayoutParams.y == 0) {
+ mLayoutParams.gravity = Gravity.TOP | Gravity.START;
+ mLayoutParams.x = 0;
+ mLayoutParams.y = 100;
+ }
+ } else {
+ mDraggable = false;
+ applyPosition(mLayoutParams, mPosition);
+ }
+
+ mOverlayView = new LinearLayout(mContext);
+ mOverlayView.setLayoutParams(new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ ));
+ mRootLayout = (LinearLayout) mOverlayView;
+ applySplitMode();
+ applyBackgroundStyle();
+ applyPadding();
+
+ mOverlayView.setOnTouchListener((v, event) -> {
+ if (mGestureDetector != null && mGestureDetector.onTouchEvent(event)) {
+ return true;
+ }
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (mDraggable) {
+ initialX = mLayoutParams.x;
+ initialY = mLayoutParams.y;
+ initialTouchX = event.getRawX();
+ initialTouchY = event.getRawY();
+ }
+ if (mLongPressEnabled) {
+ mPressActive = true;
+ mDownX = event.getRawX();
+ mDownY = event.getRawY();
+ mHandler.postDelayed(mLongPressRunnable, mLongPressThresholdMs);
+ }
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ if (mLongPressEnabled && mPressActive) {
+ float dx = Math.abs(event.getRawX() - mDownX);
+ float dy = Math.abs(event.getRawY() - mDownY);
+ if (dx > TOUCH_SLOP || dy > TOUCH_SLOP) {
+ mPressActive = false;
+ mHandler.removeCallbacks(mLongPressRunnable);
+ }
+ }
+ if (mDraggable) {
+ int deltaX = (int) (event.getRawX() - initialTouchX);
+ int deltaY = (int) (event.getRawY() - initialTouchY);
+ mLayoutParams.x = initialX + deltaX;
+ mLayoutParams.y = initialY + deltaY;
+ mWindowManager.updateViewLayout(mOverlayView, mLayoutParams);
+ }
+ return true;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ if (mLongPressEnabled && mPressActive) {
+ mPressActive = false;
+ mHandler.removeCallbacks(mLongPressRunnable);
+ }
+ if (mDraggable) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
+ prefs.edit()
+ .putInt(PREF_KEY_X, mLayoutParams.x)
+ .putInt(PREF_KEY_Y, mLayoutParams.y)
+ .apply();
+ }
+ return true;
+ }
+ return false;
+ });
+
+ mWindowManager.addView(mOverlayView, mLayoutParams);
+ mIsShowing = true;
+ startUpdates();
+
+ // Start the FPS meter if using the new API method.
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+ GameBarFpsMeter.getInstance(mContext).start();
+ }
+ }
+
+ private int initialX, initialY;
+ private float initialTouchX, initialTouchY;
+
+ public void hide() {
+ if (!mIsShowing) return;
+ mHandler.removeCallbacksAndMessages(null);
+ if (mOverlayView != null) {
+ mWindowManager.removeView(mOverlayView);
+ mOverlayView = null;
+ }
+ mIsShowing = false;
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
+ GameBarFpsMeter.getInstance(mContext).stop();
+ }
+ }
+
+ private void updateStats() {
+ if (!mIsShowing || mRootLayout == null) return;
+
+ mRootLayout.removeAllViews();
+
+ List statViews = new ArrayList<>();
+
+ // 1) FPS
+ float fpsVal = GameBarFpsMeter.getInstance(mContext).getFps();
+ String fpsStr = fpsVal >= 0 ? String.format(Locale.getDefault(), "%.0f", fpsVal) : "N/A";
+ if (mShowFps) {
+ statViews.add(createStatLine("FPS", fpsStr));
+ }
+
+ // 2) Battery temp
+ String batteryTempStr = "N/A";
+ if (mShowBatteryTemp) {
+ String tmp = readLine(BATTERY_TEMP_PATH);
+ if (tmp != null && !tmp.isEmpty()) {
+ try {
+ int raw = Integer.parseInt(tmp.trim());
+ float c = raw / 10f;
+ batteryTempStr = String.format(Locale.getDefault(), "%.1f", c);
+ } catch (NumberFormatException ignored) {}
+ }
+ statViews.add(createStatLine("Temp", batteryTempStr + "°C"));
+ }
+
+ // 3) CPU usage
+ String cpuUsageStr = "N/A";
+ if (mShowCpuUsage) {
+ cpuUsageStr = GameBarCpuInfo.getCpuUsage();
+ String display = "N/A".equals(cpuUsageStr) ? "N/A" : cpuUsageStr + "%";
+ statViews.add(createStatLine("CPU", display));
+ }
+
+ // 4) CPU freq
+ if (mShowCpuClock) {
+ List freqs = GameBarCpuInfo.getCpuFrequencies();
+ if (!freqs.isEmpty()) {
+ statViews.add(buildCpuFreqView(freqs));
+ }
+ }
+
+ // 5) CPU temp
+ String cpuTempStr = "N/A";
+ if (mShowCpuTemp) {
+ cpuTempStr = GameBarCpuInfo.getCpuTemp();
+ statViews.add(createStatLine("CPU Temp", "N/A".equals(cpuTempStr) ? "N/A" : cpuTempStr + "°C"));
+ }
+
+ // 6) RAM usage
+ String ramStr = "N/A";
+ if (mShowRam) {
+ ramStr = GameBarMemInfo.getRamUsage();
+ statViews.add(createStatLine("RAM", "N/A".equals(ramStr) ? "N/A" : ramStr + " MB"));
+ }
+
+ // 7) GPU usage
+ String gpuUsageStr = "N/A";
+ if (mShowGpuUsage) {
+ gpuUsageStr = GameBarGpuInfo.getGpuUsage();
+ statViews.add(createStatLine("GPU", "N/A".equals(gpuUsageStr) ? "N/A" : gpuUsageStr + "%"));
+ }
+
+ // 8) GPU clock
+ String gpuClockStr = "N/A";
+ if (mShowGpuClock) {
+ gpuClockStr = GameBarGpuInfo.getGpuClock();
+ statViews.add(createStatLine("GPU Freq", "N/A".equals(gpuClockStr) ? "N/A" : gpuClockStr + "MHz"));
+ }
+
+ // 9) GPU temp
+ String gpuTempStr = "N/A";
+ if (mShowGpuTemp) {
+ gpuTempStr = GameBarGpuInfo.getGpuTemp();
+ statViews.add(createStatLine("GPU Temp", "N/A".equals(gpuTempStr) ? "N/A" : gpuTempStr + "°C"));
+ }
+
+ if ("side_by_side".equals(mSplitMode)) {
+ mRootLayout.setOrientation(LinearLayout.HORIZONTAL);
+ if ("minimal".equals(mOverlayFormat)) {
+ for (int i = 0; i < statViews.size(); i++) {
+ mRootLayout.addView(statViews.get(i));
+ if (i < statViews.size() - 1) {
+ mRootLayout.addView(createDotView());
+ }
+ }
+ } else {
+ for (View view : statViews) {
+ mRootLayout.addView(view);
+ }
+ }
+ } else {
+ mRootLayout.setOrientation(LinearLayout.VERTICAL);
+ for (View view : statViews) {
+ mRootLayout.addView(view);
+ }
+ }
+
+ if (GameDataExport.getInstance().isCapturing()) {
+ String dateTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date());
+ String pkgName = ForegroundAppDetector.getForegroundPackageName(mContext);
+
+ GameDataExport.getInstance().addOverlayData(
+ dateTime,
+ pkgName,
+ fpsStr,
+ batteryTempStr,
+ cpuUsageStr,
+ cpuTempStr,
+ gpuUsageStr,
+ gpuClockStr,
+ gpuTempStr
+ );
+ }
+
+ if (mLayoutParams != null) {
+ mWindowManager.updateViewLayout(mOverlayView, mLayoutParams);
+ }
+ }
+
+ private View buildCpuFreqView(List freqs) {
+ LinearLayout freqContainer = new LinearLayout(mContext);
+ freqContainer.setOrientation(LinearLayout.HORIZONTAL);
+
+ int spacingPx = dpToPx(mContext, mItemSpacingDp);
+ LinearLayout.LayoutParams outerLp = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ );
+ outerLp.setMargins(spacingPx, spacingPx / 2, spacingPx, spacingPx / 2);
+ freqContainer.setLayoutParams(outerLp);
+
+ if ("full".equals(mOverlayFormat)) {
+ TextView labelTv = new TextView(mContext);
+ labelTv.setTextSize(TypedValue.COMPLEX_UNIT_SP, mTextSizeSp);
+ try {
+ labelTv.setTextColor(Color.parseColor(mTitleColorHex));
+ } catch (Exception e) {
+ labelTv.setTextColor(Color.WHITE);
+ }
+ labelTv.setText("CPU Freq ");
+ freqContainer.addView(labelTv);
+ }
+
+ LinearLayout verticalFreqs = new LinearLayout(mContext);
+ verticalFreqs.setOrientation(LinearLayout.VERTICAL);
+
+ for (String freqLine : freqs) {
+ LinearLayout lineLayout = new LinearLayout(mContext);
+ lineLayout.setOrientation(LinearLayout.HORIZONTAL);
+
+ TextView freqTv = new TextView(mContext);
+ freqTv.setTextSize(TypedValue.COMPLEX_UNIT_SP, mTextSizeSp);
+ try {
+ freqTv.setTextColor(Color.parseColor(mValueColorHex));
+ } catch (Exception e) {
+ freqTv.setTextColor(Color.WHITE);
+ }
+ freqTv.setText(freqLine);
+
+ lineLayout.addView(freqTv);
+
+ LinearLayout.LayoutParams lineLp = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ );
+ lineLp.setMargins(spacingPx, spacingPx / 4, spacingPx, spacingPx / 4);
+ lineLayout.setLayoutParams(lineLp);
+
+ verticalFreqs.addView(lineLayout);
+ }
+
+ freqContainer.addView(verticalFreqs);
+ return freqContainer;
+ }
+
+ private LinearLayout createStatLine(String title, String rawValue) {
+ LinearLayout lineLayout = new LinearLayout(mContext);
+ lineLayout.setOrientation(LinearLayout.HORIZONTAL);
+
+ if ("full".equals(mOverlayFormat)) {
+ TextView tvTitle = new TextView(mContext);
+ tvTitle.setTextSize(TypedValue.COMPLEX_UNIT_SP, mTextSizeSp);
+ try {
+ tvTitle.setTextColor(Color.parseColor(mTitleColorHex));
+ } catch (Exception e) {
+ tvTitle.setTextColor(Color.WHITE);
+ }
+ tvTitle.setText(title.isEmpty() ? "" : title + " ");
+
+ TextView tvValue = new TextView(mContext);
+ tvValue.setTextSize(TypedValue.COMPLEX_UNIT_SP, mTextSizeSp);
+ try {
+ tvValue.setTextColor(Color.parseColor(mValueColorHex));
+ } catch (Exception e) {
+ tvValue.setTextColor(Color.WHITE);
+ }
+ tvValue.setText(rawValue);
+
+ lineLayout.addView(tvTitle);
+ lineLayout.addView(tvValue);
+ } else {
+ TextView tvMinimal = new TextView(mContext);
+ tvMinimal.setTextSize(TypedValue.COMPLEX_UNIT_SP, mTextSizeSp);
+ try {
+ tvMinimal.setTextColor(Color.parseColor(mValueColorHex));
+ } catch (Exception e) {
+ tvMinimal.setTextColor(Color.WHITE);
+ }
+ tvMinimal.setText(rawValue);
+ lineLayout.addView(tvMinimal);
+ }
+
+ int spacingPx = dpToPx(mContext, mItemSpacingDp);
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ );
+ lp.setMargins(spacingPx, spacingPx / 2, spacingPx, spacingPx / 2);
+ lineLayout.setLayoutParams(lp);
+
+ return lineLayout;
+ }
+
+ private View createDotView() {
+ TextView dotView = new TextView(mContext);
+ dotView.setTextSize(TypedValue.COMPLEX_UNIT_SP, mTextSizeSp);
+ try {
+ dotView.setTextColor(Color.parseColor(mValueColorHex));
+ } catch (Exception e) {
+ dotView.setTextColor(Color.WHITE);
+ }
+ dotView.setText(" . ");
+ return dotView;
+ }
+
+ public void setShowBatteryTemp(boolean show) { mShowBatteryTemp = show; }
+ public void setShowCpuUsage(boolean show) { mShowCpuUsage = show; }
+ public void setShowCpuClock(boolean show) { mShowCpuClock = show; }
+ public void setShowCpuTemp(boolean show) { mShowCpuTemp = show; }
+ public void setShowRam(boolean show) { mShowRam = show; }
+ public void setShowFps(boolean show) { mShowFps = show; }
+
+ public void setShowGpuUsage(boolean show) { mShowGpuUsage = show; }
+ public void setShowGpuClock(boolean show) { mShowGpuClock = show; }
+ public void setShowGpuTemp(boolean show) { mShowGpuTemp = show; }
+
+ public void updateTextSize(int sp) {
+ mTextSizeSp = sp;
+ }
+
+ public void updateCornerRadius(int radius) {
+ mCornerRadius = radius;
+ applyBackgroundStyle();
+ }
+
+ public void updateBackgroundAlpha(int alpha) {
+ mBackgroundAlpha = alpha;
+ applyBackgroundStyle();
+ }
+
+ public void updatePadding(int dp) {
+ mPaddingDp = dp;
+ applyPadding();
+ }
+
+ public void updateTitleColor(String hex) {
+ mTitleColorHex = hex;
+ }
+
+ public void updateValueColor(String hex) {
+ mValueColorHex = hex;
+ }
+
+ public void updateOverlayFormat(String format) {
+ mOverlayFormat = format;
+ if (mIsShowing) {
+ updateStats();
+ }
+ }
+
+ public void updateItemSpacing(int dp) {
+ mItemSpacingDp = dp;
+ if (mIsShowing) {
+ updateStats();
+ }
+ }
+
+ private void applyBackgroundStyle() {
+ int color = Color.argb(mBackgroundAlpha, 0, 0, 0);
+ mBgDrawable.setColor(color);
+ mBgDrawable.setCornerRadius(mCornerRadius);
+
+ if (mOverlayView != null) {
+ mOverlayView.setBackground(mBgDrawable);
+ }
+ }
+
+ private void applyPadding() {
+ if (mRootLayout != null) {
+ int px = dpToPx(mContext, mPaddingDp);
+ mRootLayout.setPadding(px, px, px, px);
+ }
+ }
+
+ public void updatePosition(String pos) {
+ mPosition = pos;
+ if (mIsShowing && mOverlayView != null && mLayoutParams != null) {
+ if ("draggable".equals(mPosition)) {
+ mDraggable = true;
+ loadSavedPosition(mLayoutParams);
+ if (mLayoutParams.x == 0 && mLayoutParams.y == 0) {
+ mLayoutParams.gravity = Gravity.TOP | Gravity.START;
+ mLayoutParams.x = 0;
+ mLayoutParams.y = 100;
+ }
+ } else {
+ mDraggable = false;
+ applyPosition(mLayoutParams, mPosition);
+ }
+ mWindowManager.updateViewLayout(mOverlayView, mLayoutParams);
+ }
+ }
+
+ public void updateSplitMode(String mode) {
+ mSplitMode = mode;
+ if (mIsShowing && mOverlayView != null) {
+ applySplitMode();
+ updateStats();
+ }
+ }
+
+ public void updateUpdateInterval(String intervalStr) {
+ try {
+ mUpdateIntervalMs = Integer.parseInt(intervalStr);
+ } catch (NumberFormatException e) {
+ mUpdateIntervalMs = 1000;
+ }
+ if (mIsShowing) {
+ startUpdates();
+ }
+ }
+
+ public void setLongPressEnabled(boolean enabled) {
+ mLongPressEnabled = enabled;
+ }
+ public void setLongPressThresholdMs(long ms) {
+ mLongPressThresholdMs = ms;
+ }
+
+ public void setDoubleTapCaptureEnabled(boolean enabled) {
+ mDoubleTapCaptureEnabled = enabled;
+ }
+
+ public void setSingleTapToggleEnabled(boolean enabled) {
+ mSingleTapToggleEnabled = enabled;
+ }
+
+ private void startUpdates() {
+ mHandler.removeCallbacksAndMessages(null);
+ mHandler.post(mUpdateRunnable);
+ }
+
+ private void applySplitMode() {
+ if (mRootLayout == null) return;
+ if ("side_by_side".equals(mSplitMode)) {
+ mRootLayout.setOrientation(LinearLayout.HORIZONTAL);
+ } else {
+ mRootLayout.setOrientation(LinearLayout.VERTICAL);
+ }
+ }
+
+ private void loadSavedPosition(WindowManager.LayoutParams lp) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
+ int savedX = prefs.getInt(PREF_KEY_X, Integer.MIN_VALUE);
+ int savedY = prefs.getInt(PREF_KEY_Y, Integer.MIN_VALUE);
+ if (savedX != Integer.MIN_VALUE && savedY != Integer.MIN_VALUE) {
+ lp.gravity = Gravity.TOP | Gravity.START;
+ lp.x = savedX;
+ lp.y = savedY;
+ }
+ }
+
+ private void applyPosition(WindowManager.LayoutParams lp, String pos) {
+ switch (pos) {
+ case "top_left":
+ lp.gravity = Gravity.TOP | Gravity.START;
+ lp.x = 0;
+ lp.y = 100;
+ break;
+ case "top_center":
+ lp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
+ lp.y = 100;
+ break;
+ case "top_right":
+ lp.gravity = Gravity.TOP | Gravity.END;
+ lp.x = 0;
+ lp.y = 100;
+ break;
+ case "bottom_left":
+ lp.gravity = Gravity.BOTTOM | Gravity.START;
+ lp.x = 0;
+ lp.y = 100;
+ break;
+ case "bottom_center":
+ lp.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
+ lp.y = 100;
+ break;
+ case "bottom_right":
+ lp.gravity = Gravity.BOTTOM | Gravity.END;
+ lp.x = 0;
+ lp.y = 100;
+ break;
+ default:
+ lp.gravity = Gravity.TOP | Gravity.START;
+ lp.x = 0;
+ lp.y = 100;
+ break;
+ }
+ }
+
+ private String readLine(String path) {
+ try (BufferedReader br = new BufferedReader(new FileReader(path))) {
+ return br.readLine();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private void openOverlaySettings() {
+ try {
+ Intent intent = new Intent(mContext, GameBarSettingsActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mContext.startActivity(intent);
+ } catch (Exception e) {
+ // Exception ignored
+ }
+ }
+
+ private static int dpToPx(Context context, int dp) {
+ float scale = context.getResources().getDisplayMetrics().density;
+ return Math.round(dp * scale);
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAppRemoverActivity.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAppRemoverActivity.java
new file mode 100644
index 0000000..1ee1409
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAppRemoverActivity.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import android.os.Bundle;
+import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity;
+import org.lineageos.device.DeviceSettings.R;
+
+public class GameBarAppRemoverActivity extends CollapsingToolbarBaseActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_game_bar_app_selector);
+ setTitle("Remove Auto-Enable Apps");
+
+ if (savedInstanceState == null) {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.content_frame, new GameBarAppRemoverFragment())
+ .commit();
+ }
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAppRemoverFragment.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAppRemoverFragment.java
new file mode 100644
index 0000000..f794b60
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAppRemoverFragment.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.preference.PreferenceManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+import org.lineageos.device.DeviceSettings.R;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class GameBarAppRemoverFragment extends Fragment {
+
+ private RecyclerView recyclerView;
+ private GameBarAutoAppsAdapter adapter;
+ private PackageManager packageManager;
+ private List autoAppsList;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.game_bar_app_selector, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ recyclerView = view.findViewById(R.id.app_list);
+ packageManager = getContext().getPackageManager();
+ recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ loadAutoApps();
+ }
+
+ private void loadAutoApps() {
+ Set autoAppsSet = getSavedAutoApps();
+ autoAppsList = new ArrayList<>();
+ for (String pkg : autoAppsSet) {
+ try {
+ ApplicationInfo info = packageManager.getApplicationInfo(pkg, 0);
+ autoAppsList.add(info);
+ } catch (PackageManager.NameNotFoundException e) {
+ }
+ }
+ adapter = new GameBarAutoAppsAdapter(packageManager, autoAppsList, new GameBarAutoAppsAdapter.OnAppRemoveListener() {
+ @Override
+ public void onAppRemove(ApplicationInfo appInfo) {
+ removeAppFromAutoList(appInfo.packageName);
+ Toast.makeText(getContext(), appInfo.loadLabel(packageManager) + " removed.", Toast.LENGTH_SHORT).show();
+ autoAppsList.remove(appInfo);
+ adapter.notifyDataSetChanged();
+ }
+ });
+ recyclerView.setAdapter(adapter);
+ }
+
+ private Set getSavedAutoApps() {
+ return PreferenceManager.getDefaultSharedPreferences(getContext())
+ .getStringSet(GameBarAppSelectorFragment.PREF_AUTO_APPS, new HashSet<>());
+ }
+
+ private void removeAppFromAutoList(String packageName) {
+ Set autoApps = new HashSet<>(getSavedAutoApps());
+ autoApps.remove(packageName);
+ PreferenceManager.getDefaultSharedPreferences(getContext())
+ .edit().putStringSet(GameBarAppSelectorFragment.PREF_AUTO_APPS, autoApps).apply();
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAppSelectorActivity.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAppSelectorActivity.java
new file mode 100644
index 0000000..f486be7
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAppSelectorActivity.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import android.os.Bundle;
+import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity;
+import org.lineageos.device.DeviceSettings.R;
+
+public class GameBarAppSelectorActivity extends CollapsingToolbarBaseActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_game_bar_app_selector);
+ setTitle("Select Apps for GameBar");
+
+ if (savedInstanceState == null) {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.content_frame, new GameBarAppSelectorFragment())
+ .commit();
+ }
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAppSelectorFragment.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAppSelectorFragment.java
new file mode 100644
index 0000000..e390190
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAppSelectorFragment.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.preference.PreferenceManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+import org.lineageos.device.DeviceSettings.R;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class GameBarAppSelectorFragment extends Fragment {
+
+ public static final String PREF_AUTO_APPS = "game_bar_auto_apps";
+
+ private RecyclerView recyclerView;
+ private GameBarAppsAdapter adapter;
+ private PackageManager packageManager;
+ private List allApps;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.game_bar_app_selector, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ recyclerView = view.findViewById(R.id.app_list);
+ packageManager = getContext().getPackageManager();
+ recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ loadApps();
+ }
+
+ private void loadApps() {
+ allApps = new ArrayList<>();
+ List installedApps = packageManager.getInstalledApplications(PackageManager.GET_META_DATA);
+ Set autoApps = getSavedAutoApps();
+ for (ApplicationInfo appInfo : installedApps) {
+ if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0 &&
+ !appInfo.packageName.equals(getContext().getPackageName()) &&
+ !autoApps.contains(appInfo.packageName)) {
+ allApps.add(appInfo);
+ }
+ }
+ adapter = new GameBarAppsAdapter(packageManager, allApps, new GameBarAppsAdapter.OnAppClickListener() {
+ @Override
+ public void onAppClick(ApplicationInfo appInfo) {
+ addAppToAutoList(appInfo.packageName);
+ Toast.makeText(getContext(), appInfo.loadLabel(packageManager) + " added.", Toast.LENGTH_SHORT).show();
+ allApps.remove(appInfo);
+ adapter.notifyDataSetChanged();
+ }
+ });
+ recyclerView.setAdapter(adapter);
+ }
+
+ private Set getSavedAutoApps() {
+ return PreferenceManager.getDefaultSharedPreferences(getContext())
+ .getStringSet(PREF_AUTO_APPS, new HashSet<>());
+ }
+
+ private void addAppToAutoList(String packageName) {
+ Set autoApps = new HashSet<>(getSavedAutoApps());
+ autoApps.add(packageName);
+ PreferenceManager.getDefaultSharedPreferences(getContext())
+ .edit().putStringSet(PREF_AUTO_APPS, autoApps).apply();
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAppsAdapter.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAppsAdapter.java
new file mode 100644
index 0000000..405aba2
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAppsAdapter.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import org.lineageos.device.DeviceSettings.R;
+import java.util.List;
+
+public class GameBarAppsAdapter extends RecyclerView.Adapter {
+
+ public interface OnAppClickListener {
+ void onAppClick(ApplicationInfo appInfo);
+ }
+
+ private PackageManager packageManager;
+ private List apps;
+ private OnAppClickListener listener;
+
+ public GameBarAppsAdapter(PackageManager packageManager, List apps, OnAppClickListener listener) {
+ this.packageManager = packageManager;
+ this.apps = apps;
+ this.listener = listener;
+ }
+
+ @NonNull
+ @Override
+ public GameBarAppsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.game_bar_app_selector_item, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull GameBarAppsAdapter.ViewHolder holder, int position) {
+ final ApplicationInfo appInfo = apps.get(position);
+ holder.appName.setText(appInfo.loadLabel(packageManager));
+ holder.appPackage.setText(appInfo.packageName);
+ holder.appIcon.setImageDrawable(appInfo.loadIcon(packageManager));
+ holder.itemView.setOnClickListener(v -> {
+ if (listener != null) {
+ listener.onAppClick(appInfo);
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return apps.size();
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ TextView appName;
+ TextView appPackage;
+ ImageView appIcon;
+
+ public ViewHolder(@NonNull View itemView) {
+ super(itemView);
+ appName = itemView.findViewById(R.id.app_name);
+ appPackage = itemView.findViewById(R.id.app_package);
+ appIcon = itemView.findViewById(R.id.app_icon);
+ }
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAutoAppsAdapter.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAutoAppsAdapter.java
new file mode 100644
index 0000000..c197c2a
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarAutoAppsAdapter.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import org.lineageos.device.DeviceSettings.R;
+import java.util.List;
+
+public class GameBarAutoAppsAdapter extends RecyclerView.Adapter {
+
+ public interface OnAppRemoveListener {
+ void onAppRemove(ApplicationInfo appInfo);
+ }
+
+ private PackageManager packageManager;
+ private List apps;
+ private OnAppRemoveListener listener;
+
+ public GameBarAutoAppsAdapter(PackageManager packageManager, List apps, OnAppRemoveListener listener) {
+ this.packageManager = packageManager;
+ this.apps = apps;
+ this.listener = listener;
+ }
+
+ @NonNull
+ @Override
+ public GameBarAutoAppsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.game_bar_app_selector_item, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull GameBarAutoAppsAdapter.ViewHolder holder, int position) {
+ final ApplicationInfo appInfo = apps.get(position);
+ holder.appName.setText(appInfo.loadLabel(packageManager));
+ holder.appPackage.setText(appInfo.packageName);
+ holder.appIcon.setImageDrawable(appInfo.loadIcon(packageManager));
+ holder.itemView.setOnClickListener(v -> {
+ if (listener != null) {
+ listener.onAppRemove(appInfo);
+ }
+ });
+ }
+
+ @Override
+ public int getItemCount() {
+ return apps.size();
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ TextView appName;
+ TextView appPackage;
+ ImageView appIcon;
+
+ public ViewHolder(@NonNull View itemView) {
+ super(itemView);
+ appName = itemView.findViewById(R.id.app_name);
+ appPackage = itemView.findViewById(R.id.app_package);
+ appIcon = itemView.findViewById(R.id.app_icon);
+ }
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarBootReceiver.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarBootReceiver.java
new file mode 100644
index 0000000..8131ef3
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarBootReceiver.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import androidx.preference.PreferenceManager;
+
+public class GameBarBootReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_BOOT_COMPLETED.equals(action)
+ || Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action)) {
+ restoreOverlayState(context);
+ }
+ }
+
+ private void restoreOverlayState(Context context) {
+ var prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean mainEnabled = prefs.getBoolean("game_bar_enable", false);
+ boolean autoEnabled = prefs.getBoolean("game_bar_auto_enable", false);
+ if (mainEnabled) {
+ GameBar.getInstance(context).applyPreferences();
+ GameBar.getInstance(context).show();
+ }
+ if (autoEnabled) {
+ Intent monitorIntent = new Intent(context, GameBarMonitorService.class);
+ context.startService(monitorIntent);
+ }
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarCpuInfo.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarCpuInfo.java
new file mode 100644
index 0000000..3f4000f
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarCpuInfo.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class GameBarCpuInfo {
+
+ private static long sPrevIdle = -1;
+ private static long sPrevTotal = -1;
+
+ private static final String CPU_TEMP_PATH = "/sys/class/thermal/thermal_zone0/temp";
+
+ public static String getCpuUsage() {
+ String line = readLine("/proc/stat");
+ if (line == null || !line.startsWith("cpu ")) return "N/A";
+ String[] parts = line.split("\\s+");
+ if (parts.length < 8) return "N/A";
+
+ try {
+ long user = Long.parseLong(parts[1]);
+ long nice = Long.parseLong(parts[2]);
+ long system = Long.parseLong(parts[3]);
+ long idle = Long.parseLong(parts[4]);
+ long iowait = Long.parseLong(parts[5]);
+ long irq = Long.parseLong(parts[6]);
+ long softirq = Long.parseLong(parts[7]);
+ long steal = parts.length > 8 ? Long.parseLong(parts[8]) : 0;
+
+ long total = user + nice + system + idle + iowait + irq + softirq + steal;
+
+ if (sPrevTotal != -1 && total != sPrevTotal) {
+ long diffTotal = total - sPrevTotal;
+ long diffIdle = idle - sPrevIdle;
+ long usage = 100 * (diffTotal - diffIdle) / diffTotal;
+ sPrevTotal = total;
+ sPrevIdle = idle;
+ return String.valueOf(usage);
+ } else {
+
+ sPrevTotal = total;
+ sPrevIdle = idle;
+ return "N/A";
+ }
+ } catch (NumberFormatException e) {
+ return "N/A";
+ }
+ }
+
+ public static List getCpuFrequencies() {
+ List result = new ArrayList<>();
+ String cpuDirPath = "/sys/devices/system/cpu/";
+ java.io.File cpuDir = new java.io.File(cpuDirPath);
+ java.io.File[] files = cpuDir.listFiles((dir, name) -> name.matches("cpu\\d+"));
+ if (files == null || files.length == 0) {
+ return result;
+ }
+
+ List cpuFolders = new ArrayList<>();
+ Collections.addAll(cpuFolders, files);
+ cpuFolders.sort(Comparator.comparingInt(GameBarCpuInfo::extractCpuNumber));
+
+ for (java.io.File cpu : cpuFolders) {
+ String freqPath = cpu.getAbsolutePath() + "/cpufreq/scaling_cur_freq";
+ String freqStr = readLine(freqPath);
+ if (freqStr != null && !freqStr.isEmpty()) {
+ try {
+ int khz = Integer.parseInt(freqStr.trim());
+ int mhz = khz / 1000;
+ result.add(cpu.getName() + ": " + mhz + " MHz");
+ } catch (NumberFormatException e) {
+ result.add(cpu.getName() + ": N/A");
+ }
+ } else {
+ result.add(cpu.getName() + ": offline or frequency not available");
+ }
+ }
+ return result;
+ }
+
+ public static String getCpuTemp() {
+ String line = readLine(CPU_TEMP_PATH);
+ if (line == null) return "N/A";
+ line = line.trim();
+ try {
+ float raw = Float.parseFloat(line);
+ float c = raw / 1000f;
+ return String.format("%.1f", c);
+ } catch (NumberFormatException e) {
+ return "N/A";
+ }
+ }
+
+ private static int extractCpuNumber(java.io.File cpuFolder) {
+ String name = cpuFolder.getName().replace("cpu", "");
+ try {
+ return Integer.parseInt(name);
+ } catch (NumberFormatException e) {
+ return -1;
+ }
+ }
+
+ private static String readLine(String path) {
+ try (BufferedReader br = new BufferedReader(new FileReader(path))) {
+ return br.readLine();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarFpsMeter.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarFpsMeter.java
new file mode 100644
index 0000000..5947203
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarFpsMeter.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.view.WindowManager;
+import android.window.TaskFpsCallback;
+
+import androidx.preference.PreferenceManager;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+public class GameBarFpsMeter {
+
+ private static final float TOLERANCE = 0.1f;
+ private static final long STALENESS_THRESHOLD_MS = 2000;
+ private static final long TASK_CHECK_INTERVAL_MS = 1000;
+
+ private static GameBarFpsMeter sInstance;
+ private final Context mContext;
+ private final WindowManager mWindowManager;
+ private final SharedPreferences mPrefs;
+ private float mCurrentFps = 0f;
+ private TaskFpsCallback mTaskFpsCallback;
+ private boolean mCallbackRegistered = false;
+ private int mCurrentTaskId = -1;
+ private long mLastFpsUpdateTime = System.currentTimeMillis();
+ private final android.os.Handler mHandler = new android.os.Handler();
+
+ public static synchronized GameBarFpsMeter getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new GameBarFpsMeter(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ private GameBarFpsMeter(Context context) {
+ mContext = context;
+ mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ mTaskFpsCallback = new TaskFpsCallback() {
+ @Override
+ public void onFpsReported(float fps) {
+ if (fps > 0) {
+ mCurrentFps = fps;
+ mLastFpsUpdateTime = System.currentTimeMillis();
+ }
+ }
+ };
+ }
+ }
+
+ public void start() {
+ String method = mPrefs.getString("game_bar_fps_method", "new");
+ if (!"new".equals(method)) return;
+
+ stop();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ int taskId = getFocusedTaskId();
+ if (taskId <= 0) {
+ return;
+ }
+ mCurrentTaskId = taskId;
+ try {
+ mWindowManager.registerTaskFpsCallback(mCurrentTaskId, Runnable::run, mTaskFpsCallback);
+ mCallbackRegistered = true;
+ } catch (Exception e) {
+ }
+ mLastFpsUpdateTime = System.currentTimeMillis();
+ mHandler.postDelayed(mTaskCheckRunnable, TASK_CHECK_INTERVAL_MS);
+ }
+ }
+
+ public void stop() {
+ String method = mPrefs.getString("game_bar_fps_method", "new");
+ if ("new".equals(method) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (mCallbackRegistered) {
+ try {
+ mWindowManager.unregisterTaskFpsCallback(mTaskFpsCallback);
+ } catch (Exception e) {
+ }
+ mCallbackRegistered = false;
+ }
+ mHandler.removeCallbacks(mTaskCheckRunnable);
+ }
+ }
+
+ public float getFps() {
+ String method = mPrefs.getString("game_bar_fps_method", "new");
+ if ("legacy".equals(method)) {
+ return readLegacyFps();
+ } else {
+ return mCurrentFps;
+ }
+ }
+
+ private float readLegacyFps() {
+ try (BufferedReader br = new BufferedReader(new FileReader("/sys/class/drm/sde-crtc-0/measured_fps"))) {
+ String line = br.readLine();
+ if (line != null && line.startsWith("fps:")) {
+ String[] parts = line.split("\\s+");
+ if (parts.length >= 2) {
+ return Float.parseFloat(parts[1].trim());
+ }
+ }
+ } catch (IOException | NumberFormatException e) {
+ }
+ return -1f;
+ }
+
+ private final Runnable mTaskCheckRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ int newTaskId = getFocusedTaskId();
+ if (newTaskId > 0 && newTaskId != mCurrentTaskId) {
+ reinitCallback();
+ } else {
+ long now = System.currentTimeMillis();
+ if (now - mLastFpsUpdateTime > STALENESS_THRESHOLD_MS) {
+ reinitCallback();
+ }
+ }
+ mHandler.postDelayed(this, TASK_CHECK_INTERVAL_MS);
+ }
+ }
+ };
+
+ private int getFocusedTaskId() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ return -1;
+ }
+ try {
+ Class> atmClass = Class.forName("android.app.ActivityTaskManager");
+ Method getServiceMethod = atmClass.getDeclaredMethod("getService");
+ Object atmService = getServiceMethod.invoke(null);
+ Method getFocusedRootTaskInfoMethod = atmService.getClass().getMethod("getFocusedRootTaskInfo");
+ Object taskInfo = getFocusedRootTaskInfoMethod.invoke(atmService);
+ if (taskInfo != null) {
+ try {
+ Field taskIdField = taskInfo.getClass().getField("taskId");
+ return taskIdField.getInt(taskInfo);
+ } catch (NoSuchFieldException nsfe) {
+ try {
+ Field taskIdField = taskInfo.getClass().getField("mTaskId");
+ return taskIdField.getInt(taskInfo);
+ } catch (NoSuchFieldException nsfe2) {
+ }
+ }
+ }
+ } catch (Exception e) {
+ }
+ return -1;
+ }
+
+ private void reinitCallback() {
+ stop();
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ start();
+ }
+ }, 500);
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarFragment.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarFragment.java
new file mode 100644
index 0000000..5638572
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarFragment.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.widget.Toast;
+
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.SeekBarPreference;
+import androidx.preference.SwitchPreference;
+import androidx.preference.SwitchPreferenceCompat;
+
+import com.android.settingslib.widget.MainSwitchPreference;
+
+import org.lineageos.device.DeviceSettings.R;
+
+public class GameBarFragment extends PreferenceFragmentCompat {
+
+ private GameBar mGameBar;
+ private MainSwitchPreference mMasterSwitch;
+ private SwitchPreferenceCompat mAutoEnableSwitch;
+ private SwitchPreferenceCompat mFpsSwitch;
+ private SwitchPreferenceCompat mBatteryTempSwitch;
+ private SwitchPreferenceCompat mCpuUsageSwitch;
+ private SwitchPreferenceCompat mCpuClockSwitch;
+ private SwitchPreferenceCompat mCpuTempSwitch;
+ private SwitchPreferenceCompat mRamSwitch;
+ private SwitchPreferenceCompat mGpuUsageSwitch;
+ private SwitchPreferenceCompat mGpuClockSwitch;
+ private SwitchPreferenceCompat mGpuTempSwitch;
+ private Preference mCaptureStartPref;
+ private Preference mCaptureStopPref;
+ private Preference mCaptureExportPref;
+ private SwitchPreferenceCompat mDoubleTapCapturePref;
+ private SwitchPreferenceCompat mSingleTapTogglePref;
+ private SwitchPreferenceCompat mLongPressEnablePref;
+ private ListPreference mLongPressTimeoutPref;
+ private SeekBarPreference mTextSizePref;
+ private SeekBarPreference mBgAlphaPref;
+ private SeekBarPreference mCornerRadiusPref;
+ private SeekBarPreference mPaddingPref;
+ private SeekBarPreference mItemSpacingPref;
+ private ListPreference mUpdateIntervalPref;
+ private ListPreference mTextColorPref;
+ private ListPreference mTitleColorPref;
+ private ListPreference mValueColorPref;
+ private ListPreference mPositionPref;
+ private ListPreference mSplitModePref;
+ private ListPreference mOverlayFormatPref;
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ setPreferencesFromResource(R.xml.game_bar_preferences, rootKey);
+
+ mGameBar = GameBar.getInstance(getContext());
+
+ // Initialize all preferences.
+ mMasterSwitch = findPreference("game_bar_enable");
+ mAutoEnableSwitch = findPreference("game_bar_auto_enable");
+ mFpsSwitch = findPreference("game_bar_fps_enable");
+ mBatteryTempSwitch = findPreference("game_bar_temp_enable");
+ mCpuUsageSwitch = findPreference("game_bar_cpu_usage_enable");
+ mCpuClockSwitch = findPreference("game_bar_cpu_clock_enable");
+ mCpuTempSwitch = findPreference("game_bar_cpu_temp_enable");
+ mRamSwitch = findPreference("game_bar_ram_enable");
+ mGpuUsageSwitch = findPreference("game_bar_gpu_usage_enable");
+ mGpuClockSwitch = findPreference("game_bar_gpu_clock_enable");
+ mGpuTempSwitch = findPreference("game_bar_gpu_temp_enable");
+
+ mCaptureStartPref = findPreference("game_bar_capture_start");
+ mCaptureStopPref = findPreference("game_bar_capture_stop");
+ mCaptureExportPref = findPreference("game_bar_capture_export");
+
+ mDoubleTapCapturePref = findPreference("game_bar_doubletap_capture");
+ mSingleTapTogglePref = findPreference("game_bar_single_tap_toggle");
+ mLongPressEnablePref = findPreference("game_bar_longpress_enable");
+ mLongPressTimeoutPref = findPreference("game_bar_longpress_timeout");
+
+ mTextSizePref = findPreference("game_bar_text_size");
+ mBgAlphaPref = findPreference("game_bar_background_alpha");
+ mCornerRadiusPref = findPreference("game_bar_corner_radius");
+ mPaddingPref = findPreference("game_bar_padding");
+ mItemSpacingPref = findPreference("game_bar_item_spacing");
+
+ mUpdateIntervalPref = findPreference("game_bar_update_interval");
+ mTextColorPref = findPreference("game_bar_text_color");
+ mTitleColorPref = findPreference("game_bar_title_color");
+ mValueColorPref = findPreference("game_bar_value_color");
+ mPositionPref = findPreference("game_bar_position");
+ mSplitModePref = findPreference("game_bar_split_mode");
+ mOverlayFormatPref = findPreference("game_bar_format");
+
+ Preference appSelectorPref = findPreference("game_bar_app_selector");
+ if (appSelectorPref != null) {
+ appSelectorPref.setOnPreferenceClickListener(pref -> {
+ Intent intent = new Intent(getContext(), GameBarAppSelectorActivity.class);
+ startActivity(intent);
+ return true;
+ });
+ }
+ Preference appRemoverPref = findPreference("game_bar_app_remover");
+ if (appRemoverPref != null) {
+ appRemoverPref.setOnPreferenceClickListener(pref -> {
+ Intent intent = new Intent(getContext(), GameBarAppRemoverActivity.class);
+ startActivity(intent);
+ return true;
+ });
+ }
+
+ if (mMasterSwitch != null) {
+ mMasterSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ boolean enabled = (boolean) newValue;
+ if (enabled) {
+ if (Settings.canDrawOverlays(getContext())) {
+ mGameBar.applyPreferences();
+ mGameBar.show();
+ getContext().startService(new Intent(getContext(), GameBarMonitorService.class));
+ } else {
+ Toast.makeText(getContext(), R.string.overlay_permission_required, Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ } else {
+ mGameBar.hide();
+ if (mAutoEnableSwitch == null || !mAutoEnableSwitch.isChecked()) {
+ getContext().stopService(new Intent(getContext(), GameBarMonitorService.class));
+ }
+ }
+ return true;
+ });
+ }
+
+ if (mAutoEnableSwitch != null) {
+ mAutoEnableSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ boolean autoEnabled = (boolean) newValue;
+ if (autoEnabled) {
+ getContext().startService(new Intent(getContext(), GameBarMonitorService.class));
+ } else {
+ if (mMasterSwitch == null || !mMasterSwitch.isChecked()) {
+ getContext().stopService(new Intent(getContext(), GameBarMonitorService.class));
+ }
+ }
+ return true;
+ });
+ }
+
+ if (mFpsSwitch != null) {
+ mFpsSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mGameBar.setShowFps((boolean) newValue);
+ return true;
+ });
+ }
+ if (mBatteryTempSwitch != null) {
+ mBatteryTempSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mGameBar.setShowBatteryTemp((boolean) newValue);
+ return true;
+ });
+ }
+ if (mCpuUsageSwitch != null) {
+ mCpuUsageSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mGameBar.setShowCpuUsage((boolean) newValue);
+ return true;
+ });
+ }
+ if (mCpuClockSwitch != null) {
+ mCpuClockSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mGameBar.setShowCpuClock((boolean) newValue);
+ return true;
+ });
+ }
+ if (mCpuTempSwitch != null) {
+ mCpuTempSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mGameBar.setShowCpuTemp((boolean) newValue);
+ return true;
+ });
+ }
+ if (mRamSwitch != null) {
+ mRamSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mGameBar.setShowRam((boolean) newValue);
+ return true;
+ });
+ }
+ if (mGpuUsageSwitch != null) {
+ mGpuUsageSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mGameBar.setShowGpuUsage((boolean) newValue);
+ return true;
+ });
+ }
+ if (mGpuClockSwitch != null) {
+ mGpuClockSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mGameBar.setShowGpuClock((boolean) newValue);
+ return true;
+ });
+ }
+ if (mGpuTempSwitch != null) {
+ mGpuTempSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mGameBar.setShowGpuTemp((boolean) newValue);
+ return true;
+ });
+ }
+ if (mCaptureStartPref != null) {
+ mCaptureStartPref.setOnPreferenceClickListener(pref -> {
+ GameDataExport.getInstance().startCapture();
+ Toast.makeText(getContext(), "Started logging Data", Toast.LENGTH_SHORT).show();
+ return true;
+ });
+ }
+ if (mCaptureStopPref != null) {
+ mCaptureStopPref.setOnPreferenceClickListener(pref -> {
+ GameDataExport.getInstance().stopCapture();
+ Toast.makeText(getContext(), "Stopped logging Data", Toast.LENGTH_SHORT).show();
+ return true;
+ });
+ }
+ if (mCaptureExportPref != null) {
+ mCaptureExportPref.setOnPreferenceClickListener(pref -> {
+ GameDataExport.getInstance().exportDataToCsv();
+ Toast.makeText(getContext(), "Exported log data to file", Toast.LENGTH_SHORT).show();
+ return true;
+ });
+ }
+ if (mDoubleTapCapturePref != null) {
+ mDoubleTapCapturePref.setOnPreferenceChangeListener((pref, newValue) -> {
+ mGameBar.setDoubleTapCaptureEnabled((boolean) newValue);
+ return true;
+ });
+ }
+ if (mSingleTapTogglePref != null) {
+ mSingleTapTogglePref.setOnPreferenceChangeListener((pref, newValue) -> {
+ mGameBar.setSingleTapToggleEnabled((boolean) newValue);
+ return true;
+ });
+ }
+ if (mLongPressEnablePref != null) {
+ mLongPressEnablePref.setOnPreferenceChangeListener((pref, newValue) -> {
+ mGameBar.setLongPressEnabled((boolean) newValue);
+ return true;
+ });
+ }
+ if (mLongPressTimeoutPref != null) {
+ mLongPressTimeoutPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof String) {
+ long ms = Long.parseLong((String) newValue);
+ mGameBar.setLongPressThresholdMs(ms);
+ }
+ return true;
+ });
+ }
+ if (mTextSizePref != null) {
+ mTextSizePref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof Integer) {
+ mGameBar.updateTextSize((Integer) newValue);
+ }
+ return true;
+ });
+ }
+ if (mBgAlphaPref != null) {
+ mBgAlphaPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof Integer) {
+ mGameBar.updateBackgroundAlpha((Integer) newValue);
+ }
+ return true;
+ });
+ }
+ if (mCornerRadiusPref != null) {
+ mCornerRadiusPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof Integer) {
+ mGameBar.updateCornerRadius((Integer) newValue);
+ }
+ return true;
+ });
+ }
+ if (mPaddingPref != null) {
+ mPaddingPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof Integer) {
+ mGameBar.updatePadding((Integer) newValue);
+ }
+ return true;
+ });
+ }
+ if (mItemSpacingPref != null) {
+ mItemSpacingPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof Integer) {
+ mGameBar.updateItemSpacing((Integer) newValue);
+ }
+ return true;
+ });
+ }
+ if (mUpdateIntervalPref != null) {
+ mUpdateIntervalPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof String) {
+ mGameBar.updateUpdateInterval((String) newValue);
+ }
+ return true;
+ });
+ }
+ if (mTextColorPref != null) {
+ mTextColorPref.setOnPreferenceChangeListener((pref, newValue) -> true);
+ }
+ if (mTitleColorPref != null) {
+ mTitleColorPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof String) {
+ mGameBar.updateTitleColor((String) newValue);
+ }
+ return true;
+ });
+ }
+ if (mValueColorPref != null) {
+ mValueColorPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof String) {
+ mGameBar.updateValueColor((String) newValue);
+ }
+ return true;
+ });
+ }
+ if (mPositionPref != null) {
+ mPositionPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof String) {
+ mGameBar.updatePosition((String) newValue);
+ }
+ return true;
+ });
+ }
+ if (mSplitModePref != null) {
+ mSplitModePref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof String) {
+ mGameBar.updateSplitMode((String) newValue);
+ }
+ return true;
+ });
+ }
+ if (mOverlayFormatPref != null) {
+ mOverlayFormatPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof String) {
+ mGameBar.updateOverlayFormat((String) newValue);
+ }
+ return true;
+ });
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (!hasUsageStatsPermission(requireContext())) {
+ requestUsageStatsPermission();
+ }
+ Context context = getContext();
+ if (context != null) {
+ if ((mMasterSwitch != null && mMasterSwitch.isChecked()) ||
+ (mAutoEnableSwitch != null && mAutoEnableSwitch.isChecked())) {
+ context.startService(new Intent(context, GameBarMonitorService.class));
+ } else {
+ context.stopService(new Intent(context, GameBarMonitorService.class));
+ }
+ }
+ }
+
+ private boolean hasUsageStatsPermission(Context context) {
+ android.app.AppOpsManager appOps = (android.app.AppOpsManager)
+ context.getSystemService(Context.APP_OPS_SERVICE);
+ if (appOps == null) return false;
+ int mode = appOps.checkOpNoThrow(
+ android.app.AppOpsManager.OPSTR_GET_USAGE_STATS,
+ android.os.Process.myUid(),
+ context.getPackageName()
+ );
+ return (mode == android.app.AppOpsManager.MODE_ALLOWED);
+ }
+
+ private void requestUsageStatsPermission() {
+ Intent intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
+ startActivity(intent);
+ }
+}
+
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarGpuInfo.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarGpuInfo.java
new file mode 100644
index 0000000..afee061
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarGpuInfo.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+
+public class GameBarGpuInfo {
+
+ private static final String GPU_USAGE_PATH = "/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage";
+ private static final String GPU_CLOCK_PATH = "/sys/class/kgsl/kgsl-3d0/gpuclk";
+ private static final String GPU_TEMP_PATH = "/sys/class/kgsl/kgsl-3d0/temp";
+
+ public static String getGpuUsage() {
+ String line = readLine(GPU_USAGE_PATH);
+ if (line == null) {
+ return "N/A";
+ }
+ line = line.replace("%", "").trim();
+ try {
+ int val = Integer.parseInt(line);
+ return String.valueOf(val);
+ } catch (NumberFormatException e) {
+ return "N/A";
+ }
+ }
+
+ public static String getGpuClock() {
+ String line = readLine(GPU_CLOCK_PATH);
+ if (line == null) {
+ return "N/A";
+ }
+ line = line.trim();
+ try {
+ long hz = Long.parseLong(line);
+ long mhz = hz / 1_000_000;
+ return String.valueOf(mhz);
+ } catch (NumberFormatException e) {
+ return "N/A";
+ }
+ }
+
+ public static String getGpuTemp() {
+ String line = readLine(GPU_TEMP_PATH);
+ if (line == null) {
+ return "N/A";
+ }
+ line = line.trim();
+ try {
+ float raw = Float.parseFloat(line);
+ float c = raw / 1000f;
+ return String.format("%.1f", c);
+ } catch (NumberFormatException e) {
+ return "N/A";
+ }
+ }
+
+ private static String readLine(String path) {
+ try (BufferedReader br = new BufferedReader(new FileReader(path))) {
+ return br.readLine();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarMemInfo.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarMemInfo.java
new file mode 100644
index 0000000..a2fcf9f
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarMemInfo.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+
+public class GameBarMemInfo {
+
+ public static String getRamUsage() {
+ long memTotal = 0;
+ long memAvailable = 0;
+
+ try (BufferedReader br = new BufferedReader(new FileReader("/proc/meminfo"))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ if (line.startsWith("MemTotal:")) {
+ memTotal = parseMemValue(line);
+ } else if (line.startsWith("MemAvailable:")) {
+ memAvailable = parseMemValue(line);
+ }
+ if (memTotal > 0 && memAvailable > 0) {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ return "N/A";
+ }
+
+ if (memTotal == 0) {
+ return "N/A";
+ }
+
+ long usedKb = (memTotal - memAvailable);
+ long usedMb = usedKb / 1024;
+ return String.valueOf(usedMb);
+ }
+
+ private static long parseMemValue(String line) {
+ String[] parts = line.split("\\s+");
+ if (parts.length < 3) {
+ return 0;
+ }
+ try {
+ return Long.parseLong(parts[1]);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarMonitorService.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarMonitorService.java
new file mode 100644
index 0000000..c00e9c4
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarMonitorService.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import androidx.preference.PreferenceManager;
+import java.util.HashSet;
+import java.util.Set;
+
+public class GameBarMonitorService extends Service {
+
+ private Handler mHandler;
+ private Runnable mMonitorRunnable;
+ private static final long MONITOR_INTERVAL = 2000; // 2 seconds
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mHandler = new Handler();
+ mMonitorRunnable = new Runnable() {
+ @Override
+ public void run() {
+ monitorForegroundApp();
+ mHandler.postDelayed(this, MONITOR_INTERVAL);
+ }
+ };
+ mHandler.post(mMonitorRunnable);
+ }
+
+ private void monitorForegroundApp() {
+ var prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean masterEnabled = prefs.getBoolean("game_bar_enable", false);
+ if (masterEnabled) {
+ GameBar.getInstance(this).applyPreferences();
+ GameBar.getInstance(this).show();
+ return;
+ }
+
+ boolean autoEnabled = prefs.getBoolean("game_bar_auto_enable", false);
+ if (!autoEnabled) {
+ GameBar.getInstance(this).hide();
+ return;
+ }
+
+ String foreground = ForegroundAppDetector.getForegroundPackageName(this);
+ Set autoApps = prefs.getStringSet(GameBarAppSelectorFragment.PREF_AUTO_APPS, new HashSet<>());
+ if (autoApps.contains(foreground)) {
+ GameBar.getInstance(this).applyPreferences();
+ GameBar.getInstance(this).show();
+ } else {
+ GameBar.getInstance(this).hide();
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mHandler.removeCallbacks(mMonitorRunnable);
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarSettingsActivity.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarSettingsActivity.java
new file mode 100644
index 0000000..8972306
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarSettingsActivity.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.widget.Toast;
+
+import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity;
+
+import org.lineageos.device.DeviceSettings.R;
+
+public class GameBarSettingsActivity extends CollapsingToolbarBaseActivity {
+ private static final int OVERLAY_PERMISSION_REQUEST_CODE = 1234;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_game_bar);
+ setTitle(getString(R.string.game_bar_title));
+
+ if (!Settings.canDrawOverlays(this)) {
+ Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
+ Uri.parse("package:" + getPackageName()));
+ startActivityForResult(intent, OVERLAY_PERMISSION_REQUEST_CODE);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == OVERLAY_PERMISSION_REQUEST_CODE) {
+ if (Settings.canDrawOverlays(this)) {
+ Toast.makeText(this, R.string.overlay_permission_granted, Toast.LENGTH_SHORT).show();
+ } else {
+ Toast.makeText(this, R.string.overlay_permission_denied, Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarTileService.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarTileService.java
new file mode 100644
index 0000000..e3db7b8
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameBarTileService.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import android.service.quicksettings.Tile;
+import android.service.quicksettings.TileService;
+
+import androidx.preference.PreferenceManager;
+
+import org.lineageos.device.DeviceSettings.R;
+
+public class GameBarTileService extends TileService {
+ private GameBar mGameBar;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mGameBar = GameBar.getInstance(this);
+ }
+
+ @Override
+ public void onStartListening() {
+ boolean enabled = PreferenceManager.getDefaultSharedPreferences(this)
+ .getBoolean("game_bar_enable", false);
+ updateTileState(enabled);
+ }
+
+ @Override
+ public void onClick() {
+ boolean currentlyEnabled = PreferenceManager.getDefaultSharedPreferences(this)
+ .getBoolean("game_bar_enable", false);
+ boolean newState = !currentlyEnabled;
+
+ PreferenceManager.getDefaultSharedPreferences(this)
+ .edit()
+ .putBoolean("game_bar_enable", newState)
+ .commit();
+
+ updateTileState(newState);
+
+ if (newState) {
+ mGameBar.applyPreferences();
+ mGameBar.show();
+ } else {
+ mGameBar.hide();
+ }
+ }
+
+ private void updateTileState(boolean enabled) {
+ Tile tile = getQsTile();
+ if (tile == null) return;
+
+ tile.setState(enabled ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE);
+ tile.setLabel(getString(R.string.game_bar_tile_label));
+ tile.setContentDescription(getString(R.string.game_bar_tile_description));
+ tile.updateTile();
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameDataExport.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameDataExport.java
new file mode 100644
index 0000000..e604f5b
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/gamebar/GameDataExport.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2025 kenway214
+ *
+ * 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.gamebar;
+
+import android.os.Environment;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+public class GameDataExport {
+
+ private static GameDataExport sInstance;
+ public static synchronized GameDataExport getInstance() {
+ if (sInstance == null) {
+ sInstance = new GameDataExport();
+ }
+ return sInstance;
+ }
+
+ private boolean mCapturing = false;
+
+ private final List mStatsRows = new ArrayList<>();
+
+ private static final String[] CSV_HEADER = {
+ "DateTime",
+ "PackageName",
+ "FPS",
+ "Battery_Temp",
+ "CPU_Usage",
+ "CPU_Temp",
+ "GPU_Usage",
+ "GPU_Clock",
+ "GPU_Temp"
+ };
+
+ private GameDataExport() {
+ }
+
+ public void startCapture() {
+ mCapturing = true;
+ mStatsRows.clear();
+ mStatsRows.add(CSV_HEADER);
+ }
+
+ public void stopCapture() {
+ mCapturing = false;
+ }
+
+ public boolean isCapturing() {
+ return mCapturing;
+ }
+
+ public void addOverlayData(String dateTime,
+ String packageName,
+ String fps,
+ String batteryTemp,
+ String cpuUsage,
+ String cpuTemp,
+ String gpuUsage,
+ String gpuClock,
+ String gpuTemp) {
+ if (!mCapturing) return;
+
+ String[] row = {
+ dateTime,
+ packageName,
+ fps,
+ batteryTemp,
+ cpuUsage,
+ cpuTemp,
+ gpuUsage,
+ gpuClock,
+ gpuTemp
+ };
+ mStatsRows.add(row);
+ }
+
+ public void exportDataToCsv() {
+ if (mStatsRows.size() <= 1) {
+ return;
+ }
+ String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
+ File outFile = new File(Environment.getExternalStorageDirectory(), "GameBar_log_" + timeStamp + ".csv");
+
+ BufferedWriter bw = null;
+ try {
+ bw = new BufferedWriter(new FileWriter(outFile, true));
+ for (String[] row : mStatsRows) {
+ bw.write(toCsvLine(row));
+ bw.newLine();
+ }
+ bw.flush();
+ } catch (IOException ignored) {
+ } finally {
+ if (bw != null) {
+ try { bw.close(); } catch (IOException ignored) {}
+ }
+ }
+ }
+
+ private String toCsvLine(String[] columns) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < columns.length; i++) {
+ sb.append(columns[i]);
+ if (i < columns.length - 1) {
+ sb.append(",");
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/DeviceSettings/src/org/lineageos/device/DeviceSettings/utils/FileUtils.java b/DeviceSettings/src/org/lineageos/device/DeviceSettings/utils/FileUtils.java
new file mode 100644
index 0000000..2024f42
--- /dev/null
+++ b/DeviceSettings/src/org/lineageos/device/DeviceSettings/utils/FileUtils.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2016 The CyanogenMod 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.utils;
+
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+
+public final class FileUtils {
+ private static final String TAG = "FileUtils";
+
+ private FileUtils() {
+ // This class is not supposed to be instantiated
+ }
+
+ /**
+ * Reads the first line of text from the given file.
+ * Reference {@link BufferedReader#readLine()} for clarification on what a line is
+ *
+ * @return the read line contents, or null on failure
+ */
+ public static String readOneLine(String fileName) {
+ String line = null;
+ BufferedReader reader = null;
+
+ try {
+ reader = new BufferedReader(new FileReader(fileName), 512);
+ line = reader.readLine();
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "No such file " + fileName + " for reading", e);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not read from file " + fileName, e);
+ } finally {
+ try {
+ if (reader != null) {
+ reader.close();
+ }
+ } catch (IOException e) {
+ // Ignored, not much we can do anyway
+ }
+ }
+
+ return line;
+ }
+
+ /**
+ * Writes the given value into the given file
+ *
+ * @return true on success, false on failure
+ */
+ public static boolean writeLine(String fileName, String value) {
+ BufferedWriter writer = null;
+
+ try {
+ writer = new BufferedWriter(new FileWriter(fileName));
+ writer.write(value);
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "No such file " + fileName + " for writing", e);
+ return false;
+ } catch (IOException e) {
+ Log.e(TAG, "Could not write to file " + fileName, e);
+ return false;
+ } finally {
+ try {
+ if (writer != null) {
+ writer.close();
+ }
+ } catch (IOException e) {
+ // Ignored, not much we can do anyway
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks whether the given file exists
+ *
+ * @return true if exists, false if not
+ */
+ public static boolean fileExists(String fileName) {
+ final File file = new File(fileName);
+ return file.exists();
+ }
+
+ /**
+ * Checks whether the given file is readable
+ *
+ * @return true if readable, false if not
+ */
+ public static boolean isFileReadable(String fileName) {
+ final File file = new File(fileName);
+ return file.exists() && file.canRead();
+ }
+
+ /**
+ * Checks whether the given file is writable
+ *
+ * @return true if writable, false if not
+ */
+ public static boolean isFileWritable(String fileName) {
+ final File file = new File(fileName);
+ return file.exists() && file.canWrite();
+ }
+
+ /**
+ * Deletes an existing file
+ *
+ * @return true if the delete was successful, false if not
+ */
+ public static boolean delete(String fileName) {
+ final File file = new File(fileName);
+ boolean ok = false;
+ try {
+ ok = file.delete();
+ } catch (SecurityException e) {
+ Log.w(TAG, "SecurityException trying to delete " + fileName, e);
+ }
+ return ok;
+ }
+
+ /**
+ * Renames an existing file
+ *
+ * @return true if the rename was successful, false if not
+ */
+ public static boolean rename(String srcPath, String dstPath) {
+ final File srcFile = new File(srcPath);
+ final File dstFile = new File(dstPath);
+ boolean ok = false;
+ try {
+ ok = srcFile.renameTo(dstFile);
+ } catch (SecurityException e) {
+ Log.w(TAG, "SecurityException trying to rename " + srcPath + " to " + dstPath, e);
+ } catch (NullPointerException e) {
+ Log.e(TAG, "NullPointerException trying to rename " + srcPath + " to " + dstPath, e);
+ }
+ return ok;
+ }
+
+ public static boolean getFileValueAsBoolean(String filename, boolean defValue) {
+ String fileValue = readOneLine(filename);
+ if(fileValue!=null){
+ return (fileValue.equals("0")?false:true);
+ }
+ return defValue;
+ }
+
+ public static String getFileValue(String filename, String defValue) {
+ String fileValue = readOneLine(filename);
+ if(fileValue!=null){
+ return fileValue;
+ }
+ return defValue;
+ }
+}
diff --git a/product.prop b/product.prop
index 0427df9..87542e8 100644
--- a/product.prop
+++ b/product.prop
@@ -5,8 +5,5 @@ audio.offload.min.duration.secs=30
audio.offload.video=true
ro.af.client_heap_size_kbyte=7168
-# Device name
-ro.product.marketname=OnePlus 11 5G
-
# GMS
ro.opa.device_model_id=ga-oplus-skill-os121-211011