diff --git a/device.mk b/device.mk
index 138037d..4f06900 100644
--- a/device.mk
+++ b/device.mk
@@ -9,6 +9,10 @@ DEVICE_PATH := device/samsung/a71
# Inherit Common Device Tree
$(call inherit-product, device/samsung/a71-common/common.mk)
+# Core Packages
+PRODUCT_PACKAGES += \
+ Parts
+
# Audio
PRODUCT_COPY_FILES += \
$(DEVICE_PATH)/rootdir/vendor/etc/mixer_paths_idp.xml:$(TARGET_COPY_OUT_VENDOR)/etc/mixer_paths_idp.xml \
@@ -36,4 +40,4 @@ PRODUCT_SOONG_NAMESPACES += \
$(DEVICE_PATH) \
# Get non-open-source specific aspects
-$(call inherit-product, vendor/samsung/a71/a71-vendor.mk)
+$(call inherit-product, vendor/samsung/a71/a71-vendor.mk)
\ No newline at end of file
diff --git a/parts/Android.bp b/parts/Android.bp
new file mode 100644
index 0000000..1aa3bad
--- /dev/null
+++ b/parts/Android.bp
@@ -0,0 +1,43 @@
+//
+// Copyright (C) 2017-2020 The LineageOS Project
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+
+android_app {
+ name: "Parts",
+ defaults: [
+ "SettingsLibDefaults",
+ ],
+
+ srcs: [
+ "src/**/*.kt",
+ "src/**/*.java"
+ ],
+
+ certificate: "platform",
+ resource_dirs: ["res"],
+ platform_apis: true,
+ system_ext_specific: true,
+ privileged: true,
+
+ static_libs: [
+ "androidx.core_core",
+ "org.lineageos.settings.resources"
+ ],
+
+ optimize: {
+ proguard_flags_files: ["proguard.flags"],
+ },
+
+ required: [
+ "privapp_whitelist_org.lineageos.settings.xml",
+ ],
+}
+
+prebuilt_etc {
+ name: "privapp_whitelist_org.lineageos.settings.xml",
+ src: "permissions/privapp_whitelist_org.lineageos.settings.xml",
+ sub_dir: "permissions",
+ system_ext_specific: true,
+}
diff --git a/parts/AndroidManifest.xml b/parts/AndroidManifest.xml
new file mode 100644
index 0000000..f5cb21d
--- /dev/null
+++ b/parts/AndroidManifest.xml
@@ -0,0 +1,164 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/parts/permissions/privapp_whitelist_org.lineageos.settings.xml b/parts/permissions/privapp_whitelist_org.lineageos.settings.xml
new file mode 100644
index 0000000..b98a0ae
--- /dev/null
+++ b/parts/permissions/privapp_whitelist_org.lineageos.settings.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/parts/proguard.flags b/parts/proguard.flags
new file mode 100644
index 0000000..6877915
--- /dev/null
+++ b/parts/proguard.flags
@@ -0,0 +1,3 @@
+-keep class org.lineageos.settings.refreshrate.* {
+ *;
+}
diff --git a/parts/res/drawable/ic_gamebar_overlay_tile.xml b/parts/res/drawable/ic_gamebar_overlay_tile.xml
new file mode 100644
index 0000000..7d6200d
--- /dev/null
+++ b/parts/res/drawable/ic_gamebar_overlay_tile.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/parts/res/drawable/ic_qs_refresh_rate.xml b/parts/res/drawable/ic_qs_refresh_rate.xml
new file mode 100644
index 0000000..9074eb4
--- /dev/null
+++ b/parts/res/drawable/ic_qs_refresh_rate.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
diff --git a/parts/res/drawable/ic_refresh_120.xml b/parts/res/drawable/ic_refresh_120.xml
new file mode 100644
index 0000000..f81418b
--- /dev/null
+++ b/parts/res/drawable/ic_refresh_120.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/parts/res/drawable/ic_refresh_60.xml b/parts/res/drawable/ic_refresh_60.xml
new file mode 100644
index 0000000..2c4a62f
--- /dev/null
+++ b/parts/res/drawable/ic_refresh_60.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/parts/res/drawable/ic_refresh_default.xml b/parts/res/drawable/ic_refresh_default.xml
new file mode 100644
index 0000000..a4b7d3a
--- /dev/null
+++ b/parts/res/drawable/ic_refresh_default.xml
@@ -0,0 +1,11 @@
+
+
+
+
\ No newline at end of file
diff --git a/parts/res/drawable/ic_scenes.xml b/parts/res/drawable/ic_scenes.xml
new file mode 100644
index 0000000..1eee4f1
--- /dev/null
+++ b/parts/res/drawable/ic_scenes.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/parts/res/layout/activity_game_overlay.xml b/parts/res/layout/activity_game_overlay.xml
new file mode 100644
index 0000000..47bc00a
--- /dev/null
+++ b/parts/res/layout/activity_game_overlay.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/parts/res/layout/game_overlay.xml b/parts/res/layout/game_overlay.xml
new file mode 100644
index 0000000..615ad3a
--- /dev/null
+++ b/parts/res/layout/game_overlay.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/parts/res/layout/refresh_layout.xml b/parts/res/layout/refresh_layout.xml
new file mode 100644
index 0000000..6467efa
--- /dev/null
+++ b/parts/res/layout/refresh_layout.xml
@@ -0,0 +1,18 @@
+
+
+
\ No newline at end of file
diff --git a/parts/res/layout/refresh_list_item.xml b/parts/res/layout/refresh_list_item.xml
new file mode 100644
index 0000000..e2ce15f
--- /dev/null
+++ b/parts/res/layout/refresh_list_item.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/parts/res/values/arrays.xml b/parts/res/values/arrays.xml
new file mode 100644
index 0000000..7dce87c
--- /dev/null
+++ b/parts/res/values/arrays.xml
@@ -0,0 +1,98 @@
+
+
+
+
+
+ - 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/parts/res/values/strings.xml b/parts/res/values/strings.xml
new file mode 100644
index 0000000..e00fa35
--- /dev/null
+++ b/parts/res/values/strings.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+ Core Settings
+ Add tile
+ Tile added
+ Tile not added
+ Tile already added
+ On
+ Off
+
+
+ Per-app refresh rate
+ Refresh rate
+ Set the maximum refresh rate for a specific application
+ Default
+ 60Hz
+ 120Hz
+
+
+ GameBar
+ Enable the system overlay (FPS, Temp, etc.)
+ Overlay permission is required
+ Overlay permission granted
+ Overlay permission denied
+ GameBar
+ Toggle the game overlay
+
+
+ QS tiles Customization
+ Customize how tiles are displayed in quick settings
+ QS tiles style
+ Portrait tile grid
+ Rows
+ Columns
+ Rows
+ Columns
+
diff --git a/parts/res/xml/game_overlay_preferences.xml b/parts/res/xml/game_overlay_preferences.xml
new file mode 100644
index 0000000..8de7a32
--- /dev/null
+++ b/parts/res/xml/game_overlay_preferences.xml
@@ -0,0 +1,221 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/parts/src/org/lineageos/settings/BootCompletedReceiver.java b/parts/src/org/lineageos/settings/BootCompletedReceiver.java
new file mode 100644
index 0000000..b7941d8
--- /dev/null
+++ b/parts/src/org/lineageos/settings/BootCompletedReceiver.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2015 The CyanogenMod Project
+ * 2017-2019 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.settings;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.Display;
+
+import org.lineageos.settings.refreshrate.RefreshUtils;
+
+public class BootCompletedReceiver extends BroadcastReceiver {
+ private static final String TAG = "Parts";
+ private static final boolean DEBUG = true;
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ if (DEBUG) Log.i(TAG, "Received intent: " + intent.getAction());
+ switch (intent.getAction()) {
+ case Intent.ACTION_LOCKED_BOOT_COMPLETED:
+ handleLockedBootCompleted(context);
+ break;
+ case Intent.ACTION_BOOT_COMPLETED:
+ handleBootCompleted(context);
+ break;
+ }
+ }
+
+ private void handleLockedBootCompleted(Context context) {
+ if (DEBUG) Log.i(TAG, "Handling locked boot completed.");
+ try {
+ // Start necessary services
+ startServices(context);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error during locked boot completed processing", e);
+ }
+ }
+
+ private void handleBootCompleted(Context context) {
+ if (DEBUG) Log.i(TAG, "Handling boot completed.");
+ // Add additional boot-completed actions if needed
+ }
+
+ private void startServices(Context context) {
+ if (DEBUG) Log.i(TAG, "Starting services...");
+
+ // Start Refresh Rate Service
+ RefreshUtils.startService(context);
+ }
+}
diff --git a/parts/src/org/lineageos/settings/TileHandlerActivity.java b/parts/src/org/lineageos/settings/TileHandlerActivity.java
new file mode 100644
index 0000000..5839e67
--- /dev/null
+++ b/parts/src/org/lineageos/settings/TileHandlerActivity.java
@@ -0,0 +1,93 @@
+/*
+ * 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.settings;
+
+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.settings.refreshrate.RefreshActivity;
+import org.lineageos.settings.refreshrate.RefreshTileService;
+import org.lineageos.settings.gameoverlay.GameOverlaySettingsActivity;
+import org.lineageos.settings.gameoverlay.GameOverlayTileService;
+
+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(RefreshTileService.class.getName(), RefreshActivity.class);
+ TILE_ACTIVITY_MAP.put(GameOverlayTileService.class.getName(), GameOverlaySettingsActivity.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/parts/src/org/lineageos/settings/gameoverlay/ForegroundAppDetector.java b/parts/src/org/lineageos/settings/gameoverlay/ForegroundAppDetector.java
new file mode 100644
index 0000000..d371c11
--- /dev/null
+++ b/parts/src/org/lineageos/settings/gameoverlay/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.settings.gameoverlay;
+
+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/parts/src/org/lineageos/settings/gameoverlay/GameDataExport.java b/parts/src/org/lineageos/settings/gameoverlay/GameDataExport.java
new file mode 100644
index 0000000..aea42a5
--- /dev/null
+++ b/parts/src/org/lineageos/settings/gameoverlay/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.settings.gameoverlay;
+
+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/parts/src/org/lineageos/settings/gameoverlay/GameOverlay.java b/parts/src/org/lineageos/settings/gameoverlay/GameOverlay.java
new file mode 100644
index 0000000..06b732a
--- /dev/null
+++ b/parts/src/org/lineageos/settings/gameoverlay/GameOverlay.java
@@ -0,0 +1,776 @@
+/*
+ * 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.settings.gameoverlay;
+
+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.settings.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 GameOverlay {
+
+ private static GameOverlay sInstance;
+ public static synchronized GameOverlay getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new GameOverlay(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/thermal/thermal_zone78/temp";
+
+ private static final String PREF_KEY_X = "game_overlay_x";
+ private static final String PREF_KEY_Y = "game_overlay_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 mPosition = "top_left";
+ private String mSplitMode = "stacked";
+ private String mOverlayFormat = "full";
+ 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 GameOverlay(Context context) {
+ mContext = context;
+ mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ mHandler = new Handler(Looper.getMainLooper());
+
+ mBgDrawable = new GradientDrawable();
+ applyBackgroundStyle();
+ }
+
+ public void applyPreferences() {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
+
+ mShowFps = prefs.getBoolean("game_overlay_fps_enable", false);
+ mShowBatteryTemp = prefs.getBoolean("game_overlay_temp_enable", false);
+ mShowCpuUsage = prefs.getBoolean("game_overlay_cpu_usage_enable", false);
+ mShowCpuClock = prefs.getBoolean("game_overlay_cpu_clock_enable", false);
+ mShowCpuTemp = prefs.getBoolean("game_overlay_cpu_temp_enable", false);
+ mShowRam = prefs.getBoolean("game_overlay_ram_enable", false);
+
+ mShowGpuUsage = prefs.getBoolean("game_overlay_gpu_usage_enable", false);
+ mShowGpuClock = prefs.getBoolean("game_overlay_gpu_clock_enable", false);
+ mShowGpuTemp = prefs.getBoolean("game_overlay_gpu_temp_enable", false);
+
+ mDoubleTapCaptureEnabled = prefs.getBoolean("game_overlay_doubletap_capture", false);
+ mSingleTapToggleEnabled = prefs.getBoolean("game_overlay_single_tap_toggle", false);
+
+ updateSplitMode(prefs.getString("game_overlay_split_mode", "stacked"));
+ updateTextSize(prefs.getInt("game_overlay_text_size", 16));
+ updateBackgroundAlpha(prefs.getInt("game_overlay_background_alpha", 128));
+ updateCornerRadius(prefs.getInt("game_overlay_corner_radius", 16));
+ updatePadding(prefs.getInt("game_overlay_padding", 12));
+ updateTitleColor(prefs.getString("game_overlay_title_color", "#FFFFFF"));
+ updateValueColor(prefs.getString("game_overlay_value_color", "#4CAF50"));
+ updateOverlayFormat(prefs.getString("game_overlay_format", "full"));
+ updateUpdateInterval(prefs.getString("game_overlay_update_interval", "1000"));
+ updatePosition(prefs.getString("game_overlay_position", "top_left"));
+
+ int spacing = prefs.getInt("game_overlay_item_spacing", 8);
+ updateItemSpacing(spacing);
+
+ mLongPressEnabled = prefs.getBoolean("game_overlay_longpress_enable", false);
+ String lpTimeoutStr = prefs.getString("game_overlay_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();
+
+ 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";
+ Toast.makeText(mContext, "Overlay Format: " + mOverlayFormat, Toast.LENGTH_SHORT).show();
+ updateStats();
+ return true;
+ }
+ return super.onSingleTapConfirmed(e);
+ }
+ });
+
+ 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();
+ }
+
+ 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;
+ }
+
+ private void updateStats() {
+ if (!mIsShowing || mRootLayout == null) return;
+
+ mRootLayout.removeAllViews();
+
+ List statViews = new ArrayList<>();
+
+ // 1) FPS
+ float fpsVal = parseFps();
+ String fpsStr = fpsVal >= 0 ? String.format(Locale.getDefault(), "%.1f", 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 / 1000f;
+ 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 = GameOverlayCpuInfo.getCpuUsage();
+ String display = "N/A".equals(cpuUsageStr) ? "N/A" : cpuUsageStr + "%";
+ statViews.add(createStatLine("CPU", display));
+ }
+
+ // 4) CPU freq
+ if (mShowCpuClock) {
+ List freqs = GameOverlayCpuInfo.getCpuFrequencies();
+ if (!freqs.isEmpty()) {
+ statViews.add(buildCpuFreqView(freqs));
+ }
+ }
+
+ // 5) CPU temp
+ String cpuTempStr = "N/A";
+ if (mShowCpuTemp) {
+ cpuTempStr = GameOverlayCpuInfo.getCpuTemp();
+ statViews.add(createStatLine("CPU Temp", "N/A".equals(cpuTempStr) ? "N/A" : cpuTempStr + "°C"));
+ }
+
+ // 6) RAM usage
+ String ramStr = "N/A";
+ if (mShowRam) {
+ ramStr = GameOverlayMemInfo.getRamUsage();
+ statViews.add(createStatLine("RAM", "N/A".equals(ramStr) ? "N/A" : ramStr + " MB"));
+ }
+
+ // 7) GPU usage
+ String gpuUsageStr = "N/A";
+ if (mShowGpuUsage) {
+ gpuUsageStr = GameOverlayGpuInfo.getGpuUsage();
+ statViews.add(createStatLine("GPU", "N/A".equals(gpuUsageStr) ? "N/A" : gpuUsageStr + "%"));
+ }
+
+ // 8) GPU clock
+ String gpuClockStr = "N/A";
+ if (mShowGpuClock) {
+ gpuClockStr = GameOverlayGpuInfo.getGpuClock();
+ statViews.add(createStatLine("GPU Freq", "N/A".equals(gpuClockStr) ? "N/A" : gpuClockStr + "MHz"));
+ }
+
+ // 9) GPU temp
+ String gpuTempStr = "N/A";
+ if (mShowGpuTemp) {
+ gpuTempStr = GameOverlayGpuInfo.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, // PackageName
+ fpsStr, // FPS
+ batteryTempStr, // Battery_Temp
+ cpuUsageStr, // CPU_Usage
+ cpuTempStr, // CPU_Temp
+ gpuUsageStr, // GPU_Usage
+ gpuClockStr, // GPU_Clock
+ gpuTempStr // GPU_Temp
+ );
+ }
+
+ 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;
+ }
+
+ private float parseFps() {
+ String line = readLine(FPS_PATH);
+ if (line != null && line.startsWith("fps:")) {
+ String[] parts = line.split("\\s+");
+ if (parts.length >= 2) {
+ try {
+ return Float.parseFloat(parts[1].trim());
+ } catch (NumberFormatException ignored) {}
+ }
+ }
+ return -1f;
+ }
+
+ 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, GameOverlaySettingsActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mContext.startActivity(intent);
+ } catch (Exception e) {
+ }
+ }
+
+ private static int dpToPx(Context context, int dp) {
+ float scale = context.getResources().getDisplayMetrics().density;
+ return Math.round(dp * scale);
+ }
+}
diff --git a/parts/src/org/lineageos/settings/gameoverlay/GameOverlayBootReceiver.java b/parts/src/org/lineageos/settings/gameoverlay/GameOverlayBootReceiver.java
new file mode 100644
index 0000000..3276206
--- /dev/null
+++ b/parts/src/org/lineageos/settings/gameoverlay/GameOverlayBootReceiver.java
@@ -0,0 +1,46 @@
+/*
+ * 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.settings.gameoverlay;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import androidx.preference.PreferenceManager;
+
+public class GameOverlayBootReceiver 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 enabled = prefs.getBoolean("game_overlay_enable", false);
+ GameOverlay overlay = GameOverlay.getInstance(context);
+ if (!enabled) {
+ overlay.hide();
+ return;
+ }
+
+ overlay.applyPreferences();
+ overlay.show();
+ }
+}
diff --git a/parts/src/org/lineageos/settings/gameoverlay/GameOverlayCpuInfo.java b/parts/src/org/lineageos/settings/gameoverlay/GameOverlayCpuInfo.java
new file mode 100644
index 0000000..6fd3d31
--- /dev/null
+++ b/parts/src/org/lineageos/settings/gameoverlay/GameOverlayCpuInfo.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.settings.gameoverlay;
+
+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 GameOverlayCpuInfo {
+
+ 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(GameOverlayCpuInfo::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/parts/src/org/lineageos/settings/gameoverlay/GameOverlayFragment.java b/parts/src/org/lineageos/settings/gameoverlay/GameOverlayFragment.java
new file mode 100644
index 0000000..740a17f
--- /dev/null
+++ b/parts/src/org/lineageos/settings/gameoverlay/GameOverlayFragment.java
@@ -0,0 +1,358 @@
+/*
+ * 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.settings.gameoverlay;
+
+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 org.lineageos.settings.R;
+
+public class GameOverlayFragment extends PreferenceFragmentCompat {
+
+ private GameOverlay mOverlay;
+
+ private SwitchPreference mMasterSwitch;
+
+ private SwitchPreference mFpsSwitch;
+ private SwitchPreference mBatteryTempSwitch;
+ private SwitchPreference mCpuUsageSwitch;
+ private SwitchPreference mCpuClockSwitch;
+ private SwitchPreference mCpuTempSwitch;
+ private SwitchPreference mRamSwitch;
+
+ private SwitchPreference mGpuUsageSwitch;
+ private SwitchPreference mGpuClockSwitch;
+ private SwitchPreference mGpuTempSwitch;
+
+ private Preference mCaptureStartPref;
+ private Preference mCaptureStopPref;
+ private Preference mCaptureExportPref;
+
+ private SwitchPreference mDoubleTapCapturePref;
+ private SwitchPreference mSingleTapTogglePref;
+ private SwitchPreference 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_overlay_preferences, rootKey);
+
+ mOverlay = GameOverlay.getInstance(getContext());
+
+ mMasterSwitch = findPreference("game_overlay_enable");
+
+ mFpsSwitch = findPreference("game_overlay_fps_enable");
+ mBatteryTempSwitch = findPreference("game_overlay_temp_enable");
+ mCpuUsageSwitch = findPreference("game_overlay_cpu_usage_enable");
+ mCpuClockSwitch = findPreference("game_overlay_cpu_clock_enable");
+ mCpuTempSwitch = findPreference("game_overlay_cpu_temp_enable");
+ mRamSwitch = findPreference("game_overlay_ram_enable");
+
+ mGpuUsageSwitch = findPreference("game_overlay_gpu_usage_enable");
+ mGpuClockSwitch = findPreference("game_overlay_gpu_clock_enable");
+ mGpuTempSwitch = findPreference("game_overlay_gpu_temp_enable");
+
+ mCaptureStartPref = findPreference("game_overlay_capture_start");
+ mCaptureStopPref = findPreference("game_overlay_capture_stop");
+ mCaptureExportPref = findPreference("game_overlay_capture_export");
+
+ mDoubleTapCapturePref = findPreference("game_overlay_doubletap_capture");
+ mSingleTapTogglePref = findPreference("game_overlay_single_tap_toggle");
+ mLongPressEnablePref = findPreference("game_overlay_longpress_enable");
+ mLongPressTimeoutPref = findPreference("game_overlay_longpress_timeout");
+
+ mTextSizePref = findPreference("game_overlay_text_size");
+ mBgAlphaPref = findPreference("game_overlay_background_alpha");
+ mCornerRadiusPref = findPreference("game_overlay_corner_radius");
+ mPaddingPref = findPreference("game_overlay_padding");
+ mItemSpacingPref = findPreference("game_overlay_item_spacing");
+
+ mUpdateIntervalPref = findPreference("game_overlay_update_interval");
+ mTextColorPref = findPreference("game_overlay_text_color");
+ mTitleColorPref = findPreference("game_overlay_title_color");
+ mValueColorPref = findPreference("game_overlay_value_color");
+ mPositionPref = findPreference("game_overlay_position");
+ mSplitModePref = findPreference("game_overlay_split_mode");
+ mOverlayFormatPref = findPreference("game_overlay_format");
+
+ if (mMasterSwitch != null) {
+ mMasterSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ boolean enabled = (boolean) newValue;
+ if (enabled) {
+ if (Settings.canDrawOverlays(getContext())) {
+ mOverlay.applyPreferences();
+ mOverlay.show();
+ } else {
+ Toast.makeText(getContext(), R.string.overlay_permission_required, Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ } else {
+ mOverlay.hide();
+ }
+ return true;
+ });
+ }
+
+ if (mFpsSwitch != null) {
+ mFpsSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mOverlay.setShowFps((boolean) newValue);
+ return true;
+ });
+ }
+ if (mBatteryTempSwitch != null) {
+ mBatteryTempSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mOverlay.setShowBatteryTemp((boolean) newValue);
+ return true;
+ });
+ }
+ if (mCpuUsageSwitch != null) {
+ mCpuUsageSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mOverlay.setShowCpuUsage((boolean) newValue);
+ return true;
+ });
+ }
+ if (mCpuClockSwitch != null) {
+ mCpuClockSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mOverlay.setShowCpuClock((boolean) newValue);
+ return true;
+ });
+ }
+ if (mCpuTempSwitch != null) {
+ mCpuTempSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mOverlay.setShowCpuTemp((boolean) newValue);
+ return true;
+ });
+ }
+ if (mRamSwitch != null) {
+ mRamSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mOverlay.setShowRam((boolean) newValue);
+ return true;
+ });
+ }
+
+ if (mGpuUsageSwitch != null) {
+ mGpuUsageSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mOverlay.setShowGpuUsage((boolean) newValue);
+ return true;
+ });
+ }
+ if (mGpuClockSwitch != null) {
+ mGpuClockSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mOverlay.setShowGpuClock((boolean) newValue);
+ return true;
+ });
+ }
+ if (mGpuTempSwitch != null) {
+ mGpuTempSwitch.setOnPreferenceChangeListener((pref, newValue) -> {
+ mOverlay.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) -> {
+ mOverlay.setDoubleTapCaptureEnabled((boolean) newValue);
+ return true;
+ });
+ }
+ if (mSingleTapTogglePref != null) {
+ mSingleTapTogglePref.setOnPreferenceChangeListener((pref, newValue) -> {
+ mOverlay.setSingleTapToggleEnabled((boolean) newValue);
+ return true;
+ });
+ }
+ if (mLongPressEnablePref != null) {
+ mLongPressEnablePref.setOnPreferenceChangeListener((pref, newValue) -> {
+ mOverlay.setLongPressEnabled((boolean) newValue);
+ return true;
+ });
+ }
+ if (mLongPressTimeoutPref != null) {
+ mLongPressTimeoutPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof String) {
+ long ms = Long.parseLong((String) newValue);
+ mOverlay.setLongPressThresholdMs(ms);
+ }
+ return true;
+ });
+ }
+
+ if (mTextSizePref != null) {
+ mTextSizePref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof Integer) {
+ mOverlay.updateTextSize((Integer) newValue);
+ }
+ return true;
+ });
+ }
+ if (mBgAlphaPref != null) {
+ mBgAlphaPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof Integer) {
+ mOverlay.updateBackgroundAlpha((Integer) newValue);
+ }
+ return true;
+ });
+ }
+ if (mCornerRadiusPref != null) {
+ mCornerRadiusPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof Integer) {
+ mOverlay.updateCornerRadius((Integer) newValue);
+ }
+ return true;
+ });
+ }
+ if (mPaddingPref != null) {
+ mPaddingPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof Integer) {
+ mOverlay.updatePadding((Integer) newValue);
+ }
+ return true;
+ });
+ }
+
+ if (mItemSpacingPref != null) {
+ mItemSpacingPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof Integer) {
+ mOverlay.updateItemSpacing((Integer) newValue);
+ }
+ return true;
+ });
+ }
+
+ if (mUpdateIntervalPref != null) {
+ mUpdateIntervalPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof String) {
+ mOverlay.updateUpdateInterval((String) newValue);
+ }
+ return true;
+ });
+ }
+ if (mTextColorPref != null) {
+ mTextColorPref.setOnPreferenceChangeListener((pref, newValue) -> true);
+ }
+ if (mTitleColorPref != null) {
+ mTitleColorPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof String) {
+ mOverlay.updateTitleColor((String) newValue);
+ }
+ return true;
+ });
+ }
+ if (mValueColorPref != null) {
+ mValueColorPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof String) {
+ mOverlay.updateValueColor((String) newValue);
+ }
+ return true;
+ });
+ }
+ if (mPositionPref != null) {
+ mPositionPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof String) {
+ mOverlay.updatePosition((String) newValue);
+ }
+ return true;
+ });
+ }
+ if (mSplitModePref != null) {
+ mSplitModePref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof String) {
+ mOverlay.updateSplitMode((String) newValue);
+ }
+ return true;
+ });
+ }
+ if (mOverlayFormatPref != null) {
+ mOverlayFormatPref.setOnPreferenceChangeListener((pref, newValue) -> {
+ if (newValue instanceof String) {
+ mOverlay.updateOverlayFormat((String) newValue);
+ }
+ return true;
+ });
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (!hasUsageStatsPermission(requireContext())) {
+ requestUsageStatsPermission();
+ }
+ }
+
+ 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/parts/src/org/lineageos/settings/gameoverlay/GameOverlayGpuInfo.java b/parts/src/org/lineageos/settings/gameoverlay/GameOverlayGpuInfo.java
new file mode 100644
index 0000000..39fcb44
--- /dev/null
+++ b/parts/src/org/lineageos/settings/gameoverlay/GameOverlayGpuInfo.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.settings.gameoverlay;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+
+public class GameOverlayGpuInfo {
+
+ 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/parts/src/org/lineageos/settings/gameoverlay/GameOverlayMemInfo.java b/parts/src/org/lineageos/settings/gameoverlay/GameOverlayMemInfo.java
new file mode 100644
index 0000000..7afc9a6
--- /dev/null
+++ b/parts/src/org/lineageos/settings/gameoverlay/GameOverlayMemInfo.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.settings.gameoverlay;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+
+public class GameOverlayMemInfo {
+
+ 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/parts/src/org/lineageos/settings/gameoverlay/GameOverlaySettingsActivity.java b/parts/src/org/lineageos/settings/gameoverlay/GameOverlaySettingsActivity.java
new file mode 100644
index 0000000..852200f
--- /dev/null
+++ b/parts/src/org/lineageos/settings/gameoverlay/GameOverlaySettingsActivity.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.settings.gameoverlay;
+
+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.settings.R;
+
+public class GameOverlaySettingsActivity 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_overlay);
+ setTitle(getString(R.string.game_overlay_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/parts/src/org/lineageos/settings/gameoverlay/GameOverlayTileService.java b/parts/src/org/lineageos/settings/gameoverlay/GameOverlayTileService.java
new file mode 100644
index 0000000..d76f9da
--- /dev/null
+++ b/parts/src/org/lineageos/settings/gameoverlay/GameOverlayTileService.java
@@ -0,0 +1,73 @@
+/*
+ * 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.settings.gameoverlay;
+
+import android.service.quicksettings.Tile;
+import android.service.quicksettings.TileService;
+
+import androidx.preference.PreferenceManager;
+
+import org.lineageos.settings.R;
+
+public class GameOverlayTileService extends TileService {
+ private GameOverlay mOverlay;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mOverlay = GameOverlay.getInstance(this);
+ }
+
+ @Override
+ public void onStartListening() {
+ super.onStartListening();
+ boolean enabled = PreferenceManager.getDefaultSharedPreferences(this)
+ .getBoolean("game_overlay_enable", false);
+ updateTileState(enabled);
+ }
+
+ @Override
+ public void onClick() {
+ super.onClick();
+ boolean currentlyEnabled = PreferenceManager.getDefaultSharedPreferences(this)
+ .getBoolean("game_overlay_enable", false);
+ boolean newState = !currentlyEnabled;
+
+ PreferenceManager.getDefaultSharedPreferences(this)
+ .edit()
+ .putBoolean("game_overlay_enable", newState)
+ .commit();
+
+ updateTileState(newState);
+
+ if (newState) {
+ mOverlay.show();
+ } else {
+ mOverlay.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_overlay_tile_label));
+ tile.setContentDescription(getString(R.string.game_overlay_tile_description));
+ tile.updateTile();
+ }
+}
diff --git a/parts/src/org/lineageos/settings/refreshrate/RefreshActivity.java b/parts/src/org/lineageos/settings/refreshrate/RefreshActivity.java
new file mode 100644
index 0000000..02a10c2
--- /dev/null
+++ b/parts/src/org/lineageos/settings/refreshrate/RefreshActivity.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2020-2022 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.settings.refreshrate;
+
+import android.os.Bundle;
+
+import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity;
+
+public class RefreshActivity extends CollapsingToolbarBaseActivity {
+ private static final String TAG_REFRESH = "refresh";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getFragmentManager().beginTransaction().replace(com.android.settingslib.collapsingtoolbar.R.id.content_frame,
+ new RefreshSettingsFragment(), TAG_REFRESH).commit();
+ }
+}
diff --git a/parts/src/org/lineageos/settings/refreshrate/RefreshService.java b/parts/src/org/lineageos/settings/refreshrate/RefreshService.java
new file mode 100644
index 0000000..660bdd0
--- /dev/null
+++ b/parts/src/org/lineageos/settings/refreshrate/RefreshService.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 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.settings.refreshrate;
+
+import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.app.ActivityTaskManager.RootTaskInfo;
+import android.app.IActivityTaskManager;
+import android.app.TaskStackListener;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.IBinder;
+import android.util.Log;
+import android.os.RemoteException;
+
+public class RefreshService extends Service {
+
+ private static final String TAG = "RefreshService";
+ private static final boolean DEBUG = true;
+
+ private String mPreviousApp;
+ private RefreshUtils mRefreshUtils;
+ private IActivityTaskManager mActivityTaskManager;
+
+ private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mPreviousApp = "";
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ if (DEBUG) Log.d(TAG, "Creating service");
+ try {
+ mActivityTaskManager = ActivityTaskManager.getService();
+ mActivityTaskManager.registerTaskStackListener(mTaskListener);
+ } catch (RemoteException e) {
+ // Do nothing
+ }
+ mRefreshUtils = new RefreshUtils(this);
+ registerReceiver();
+ super.onCreate();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (DEBUG) Log.d(TAG, "Starting service");
+ return START_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ private void registerReceiver() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_SCREEN_OFF);
+ filter.addAction(Intent.ACTION_SCREEN_ON);
+ this.registerReceiver(mIntentReceiver, filter);
+ }
+
+ private final TaskStackListener mTaskListener = new TaskStackListener() {
+ @Override
+ public void onTaskStackChanged() {
+ try {
+ final RootTaskInfo info = mActivityTaskManager.getFocusedRootTaskInfo();
+ if (info == null || info.topActivity == null) {
+ return;
+ }
+ String foregroundApp = info.topActivity.getPackageName();
+ if (!mRefreshUtils.isAppInList) {
+ mRefreshUtils.getOldRate();
+ }
+ if (!foregroundApp.equals(mPreviousApp)) {
+ mRefreshUtils.setRefreshRate(foregroundApp);
+ mPreviousApp = foregroundApp;
+ }
+ } catch (Exception e) {}
+ }
+ };
+ }
\ No newline at end of file
diff --git a/parts/src/org/lineageos/settings/refreshrate/RefreshSettingsFragment.java b/parts/src/org/lineageos/settings/refreshrate/RefreshSettingsFragment.java
new file mode 100644
index 0000000..04a4063
--- /dev/null
+++ b/parts/src/org/lineageos/settings/refreshrate/RefreshSettingsFragment.java
@@ -0,0 +1,419 @@
+/**
+ * Copyright (C) 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.settings.refreshrate;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.SectionIndexer;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.preference.PreferenceFragment;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import com.android.settingslib.applications.ApplicationsState;
+
+import org.lineageos.settings.R;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class RefreshSettingsFragment extends PreferenceFragment
+ implements ApplicationsState.Callbacks {
+
+ private AllPackagesAdapter mAllPackagesAdapter;
+ private ApplicationsState mApplicationsState;
+ private ApplicationsState.Session mSession;
+ private ActivityFilter mActivityFilter;
+ private Map mEntryMap =
+ new HashMap();
+
+ private RefreshUtils mRefreshUtils;
+ private RecyclerView mAppsRecyclerView;
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mApplicationsState = ApplicationsState.getInstance(getActivity().getApplication());
+ mSession = mApplicationsState.newSession(this);
+ mSession.onResume();
+ mActivityFilter = new ActivityFilter(getActivity().getPackageManager());
+
+ mAllPackagesAdapter = new AllPackagesAdapter(getActivity());
+
+ mRefreshUtils = new RefreshUtils(getActivity());
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.refresh_layout, container, false);
+ }
+
+ @Override
+ public void onViewCreated(final View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ mAppsRecyclerView = view.findViewById(R.id.refresh_rv_view);
+ mAppsRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
+ mAppsRecyclerView.setAdapter(mAllPackagesAdapter);
+ }
+
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ getActivity().setTitle(getResources().getString(R.string.refresh_title));
+ rebuild();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ mSession.onPause();
+ mSession.onDestroy();
+ }
+
+ @Override
+ public void onPackageListChanged() {
+ mActivityFilter.updateLauncherInfoList();
+ rebuild();
+ }
+
+ @Override
+ public void onRebuildComplete(ArrayList entries) {
+ if (entries != null) {
+ handleAppEntries(entries);
+ mAllPackagesAdapter.notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void onLoadEntriesCompleted() {
+ rebuild();
+ }
+
+ @Override
+ public void onAllSizesComputed() {
+ }
+
+ @Override
+ public void onLauncherInfoChanged() {
+ }
+
+ @Override
+ public void onPackageIconChanged() {
+ }
+
+ @Override
+ public void onPackageSizeChanged(String packageName) {
+ }
+
+ @Override
+ public void onRunningStateChanged(boolean running) {
+ }
+
+ private void handleAppEntries(List entries) {
+ final ArrayList sections = new ArrayList();
+ final ArrayList positions = new ArrayList();
+ final PackageManager pm = getActivity().getPackageManager();
+ String lastSectionIndex = null;
+ int offset = 0;
+
+ for (int i = 0; i < entries.size(); i++) {
+ final ApplicationInfo info = entries.get(i).info;
+ final String label = (String) info.loadLabel(pm);
+ final String sectionIndex;
+
+ if (!info.enabled) {
+ sectionIndex = "--"; // XXX
+ } else if (TextUtils.isEmpty(label)) {
+ sectionIndex = "";
+ } else {
+ sectionIndex = label.substring(0, 1).toUpperCase();
+ }
+
+ if (lastSectionIndex == null ||
+ !TextUtils.equals(sectionIndex, lastSectionIndex)) {
+ sections.add(sectionIndex);
+ positions.add(offset);
+ lastSectionIndex = sectionIndex;
+ }
+
+ offset++;
+ }
+
+ mAllPackagesAdapter.setEntries(entries, sections, positions);
+ mEntryMap.clear();
+ for (ApplicationsState.AppEntry e : entries) {
+ mEntryMap.put(e.info.packageName, e);
+ }
+ }
+
+ private void rebuild() {
+ mSession.rebuild(mActivityFilter, ApplicationsState.ALPHA_COMPARATOR);
+ }
+
+ private int getStateDrawable(int state) {
+ switch (state) {
+ case RefreshUtils.STATE_STANDARD:
+ return R.drawable.ic_refresh_60;
+ case RefreshUtils.STATE_EXTREME:
+ return R.drawable.ic_refresh_120;
+ case RefreshUtils.STATE_DEFAULT:
+ default:
+ return R.drawable.ic_refresh_default;
+ }
+ }
+
+ private class ViewHolder extends RecyclerView.ViewHolder {
+ private TextView title;
+ private Spinner mode;
+ private ImageView icon;
+ private View rootView;
+ private ImageView stateIcon;
+
+ private ViewHolder(View view) {
+ super(view);
+ this.title = view.findViewById(R.id.app_name);
+ this.mode = view.findViewById(R.id.app_mode);
+ this.icon = view.findViewById(R.id.app_icon);
+ this.stateIcon = view.findViewById(R.id.state);
+ this.rootView = view;
+
+ view.setTag(this);
+ }
+ }
+
+ private class ModeAdapter extends BaseAdapter {
+
+ private final LayoutInflater inflater;
+ private final int[] items = {
+ R.string.refresh_default,
+ R.string.refresh_standard,
+ R.string.refresh_extreme
+ };
+
+ private ModeAdapter(Context context) {
+ inflater = LayoutInflater.from(context);
+ }
+
+ @Override
+ public int getCount() {
+ return items.length;
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return items[position];
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView view;
+ if (convertView != null) {
+ view = (TextView) convertView;
+ } else {
+ view = (TextView) inflater.inflate(android.R.layout.simple_spinner_dropdown_item,
+ parent, false);
+ }
+
+ view.setText(items[position]);
+ view.setTextSize(14f);
+
+ return view;
+ }
+ }
+
+ private class AllPackagesAdapter extends RecyclerView.Adapter
+ implements AdapterView.OnItemSelectedListener, SectionIndexer {
+
+ private List mEntries = new ArrayList<>();
+ private String[] mSections;
+ private int[] mPositions;
+
+ public AllPackagesAdapter(Context context) {
+ mActivityFilter = new ActivityFilter(context.getPackageManager());
+ }
+
+ @Override
+ public int getItemCount() {
+ return mEntries.size();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mEntries.get(position).id;
+ }
+@NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new ViewHolder(LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.refresh_list_item, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ Context context = holder.itemView.getContext();
+
+ ApplicationsState.AppEntry entry = mEntries.get(position);
+
+ if (entry == null) {
+ return;
+ }
+ holder.mode.setAdapter(new ModeAdapter(context));
+ holder.mode.setOnItemSelectedListener(this);
+ holder.title.setText(entry.label);
+ holder.title.setOnClickListener(v -> holder.mode.performClick());
+ mApplicationsState.ensureIcon(entry);
+ holder.icon.setImageDrawable(entry.icon);
+ int packageState = mRefreshUtils.getStateForPackage(entry.info.packageName);
+ holder.mode.setSelection(packageState, false);
+ holder.mode.setTag(entry);
+ holder.stateIcon.setImageResource(getStateDrawable(packageState));
+ }
+
+ private void setEntries(List entries,
+ List sections, List positions) {
+ mEntries = entries;
+ mSections = sections.toArray(new String[sections.size()]);
+ mPositions = new int[positions.size()];
+ for (int i = 0; i < positions.size(); i++) {
+ mPositions[i] = positions.get(i);
+ }
+ notifyDataSetChanged();
+ }
+
+
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ final ApplicationsState.AppEntry entry = (ApplicationsState.AppEntry) parent.getTag();
+
+ int currentState = mRefreshUtils.getStateForPackage(entry.info.packageName);
+ if (currentState != position) {
+ mRefreshUtils.writePackage(entry.info.packageName, position);
+ notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+ }
+
+ @Override
+ public int getPositionForSection(int section) {
+ if (section < 0 || section >= mSections.length) {
+ return -1;
+ }
+
+ return mPositions[section];
+ }
+
+ @Override
+ public int getSectionForPosition(int position) {
+ if (position < 0 || position >= getItemCount()) {
+ return -1;
+ }
+
+ final int index = Arrays.binarySearch(mPositions, position);
+
+ /*
+ * Consider this example: section positions are 0, 3, 5; the supplied
+ * position is 4. The section corresponding to position 4 starts at
+ * position 3, so the expected return value is 1. Binary search will not
+ * find 4 in the array and thus will return -insertPosition-1, i.e. -3.
+ * To get from that number to the expected value of 1 we need to negate
+ * and subtract 2.
+ */
+ return index >= 0 ? index : -index - 2;
+ }
+
+ @Override
+ public Object[] getSections() {
+ return mSections;
+ }
+ }
+
+ private class ActivityFilter implements ApplicationsState.AppFilter {
+
+ private final PackageManager mPackageManager;
+ private final List mLauncherResolveInfoList = new ArrayList();
+
+ private ActivityFilter(PackageManager packageManager) {
+ this.mPackageManager = packageManager;
+
+ updateLauncherInfoList();
+ }
+
+ public void updateLauncherInfoList() {
+ Intent i = new Intent(Intent.ACTION_MAIN);
+ i.addCategory(Intent.CATEGORY_LAUNCHER);
+ List resolveInfoList = mPackageManager.queryIntentActivities(i, 0);
+
+ synchronized (mLauncherResolveInfoList) {
+ mLauncherResolveInfoList.clear();
+ for (ResolveInfo ri : resolveInfoList) {
+ mLauncherResolveInfoList.add(ri.activityInfo.packageName);
+ }
+ }
+ }
+
+ @Override
+ public void init() {
+ }
+
+ @Override
+ public boolean filterApp(ApplicationsState.AppEntry entry) {
+ boolean show = !mAllPackagesAdapter.mEntries.contains(entry.info.packageName);
+ if (show) {
+ synchronized (mLauncherResolveInfoList) {
+ show = mLauncherResolveInfoList.contains(entry.info.packageName);
+ }
+ }
+ return show;
+ }
+ }
+}
diff --git a/parts/src/org/lineageos/settings/refreshrate/RefreshTileService.java b/parts/src/org/lineageos/settings/refreshrate/RefreshTileService.java
new file mode 100644
index 0000000..495a24a
--- /dev/null
+++ b/parts/src/org/lineageos/settings/refreshrate/RefreshTileService.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2021 crDroid Android Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.lineageos.settings.refreshrate;
+
+import android.content.Context;
+import android.provider.Settings;
+import android.service.quicksettings.Tile;
+import android.service.quicksettings.TileService;
+import android.view.Display;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+public class RefreshTileService extends TileService {
+ private static final String KEY_MIN_REFRESH_RATE = "min_refresh_rate";
+ private static final String KEY_PREFERRED_REFRESH_RATE = "preferred_refresh_rate";
+ private static final String KEY_PEAK_REFRESH_RATE = "peak_refresh_rate";
+
+ private Context context;
+ private Tile tile;
+
+ private final List availableRates = new ArrayList<>();
+ private int activeRateMin;
+ private int activeRateMax;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ context = getApplicationContext();
+ Display.Mode mode = context.getDisplay().getMode();
+ Display.Mode[] modes = context.getDisplay().getSupportedModes();
+ for (Display.Mode m : modes) {
+ float rate = Float.valueOf(String.format(Locale.US, "%.02f", m.getRefreshRate()));
+ if (m.getPhysicalWidth() == mode.getPhysicalWidth() &&
+ m.getPhysicalHeight() == mode.getPhysicalHeight()) {
+ availableRates.add(rate);
+ }
+ }
+ syncFromSettings();
+ }
+
+ private int getSettingOf(String key) {
+ float rate = Settings.System.getFloat(context.getContentResolver(), key, 60);
+ return availableRates.indexOf(
+ Float.valueOf(String.format(Locale.US, "%.02f", rate)));
+ }
+
+ private void syncFromSettings() {
+ activeRateMin = getSettingOf(KEY_MIN_REFRESH_RATE);
+ activeRateMax = getSettingOf(KEY_PEAK_REFRESH_RATE);
+ }
+
+ private void cycleRefreshRate() {
+ if (activeRateMin < availableRates.size() - 1) {
+ activeRateMin++;
+ } else {
+ activeRateMin = 0;
+ }
+
+ float rate = availableRates.get(activeRateMin);
+ Settings.System.putFloat(context.getContentResolver(), KEY_MIN_REFRESH_RATE, rate);
+ Settings.System.putFloat(context.getContentResolver(), KEY_PREFERRED_REFRESH_RATE, rate);
+ Settings.System.putFloat(context.getContentResolver(), KEY_PEAK_REFRESH_RATE, rate);
+ }
+
+ private String getFormatRate(float rate) {
+ return String.format("%.02f Hz", rate)
+ .replaceAll("[\\.,]00", "");
+ }
+
+ private void updateTileView() {
+ String displayText;
+ float min = availableRates.get(activeRateMin);
+ float max = availableRates.get(activeRateMax);
+
+ displayText = String.format(Locale.US, min == max ? "%s" : "%s - %s",
+ getFormatRate(min), getFormatRate(max));
+ tile.setContentDescription(displayText);
+ tile.setSubtitle(displayText);
+ tile.setState(min == max ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE);
+ tile.updateTile();
+ }
+
+ @Override
+ public void onStartListening() {
+ super.onStartListening();
+ tile = getQsTile();
+ syncFromSettings();
+ updateTileView();
+ }
+
+ @Override
+ public void onClick() {
+ super.onClick();
+ cycleRefreshRate();
+ syncFromSettings();
+ updateTileView();
+ }
+}
diff --git a/parts/src/org/lineageos/settings/refreshrate/RefreshUtils.java b/parts/src/org/lineageos/settings/refreshrate/RefreshUtils.java
new file mode 100644
index 0000000..d5b7acf
--- /dev/null
+++ b/parts/src/org/lineageos/settings/refreshrate/RefreshUtils.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 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.settings.refreshrate;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.UserHandle;
+import android.view.Display;
+
+import android.provider.Settings;
+import androidx.preference.PreferenceManager;
+
+public final class RefreshUtils {
+
+ private static final String REFRESH_CONTROL = "refresh_control";
+
+ private static float defaultMaxRate;
+ private static float defaultMinRate;
+ private static final String KEY_PEAK_REFRESH_RATE = "peak_refresh_rate";
+ private static final String KEY_MIN_REFRESH_RATE = "min_refresh_rate";
+ private Context mContext;
+ protected static boolean isAppInList = false;
+
+ protected static final int STATE_DEFAULT = 0;
+ protected static final int STATE_STANDARD = 1;
+ protected static final int STATE_EXTREME = 2;
+
+ private static final float REFRESH_STATE_DEFAULT = 120f;
+ private static final float REFRESH_STATE_STANDARD = 60f;
+ private static final float REFRESH_STATE_EXTREME = 120f;
+
+ private static final String REFRESH_STANDARD = "refresh.standard=";
+ private static final String REFRESH_EXTREME = "refresh.extreme=";
+
+ private SharedPreferences mSharedPrefs;
+
+ protected RefreshUtils(Context context) {
+ mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
+ mContext = context;
+ }
+
+ public static void startService(Context context) {
+ context.startServiceAsUser(new Intent(context, RefreshService.class),
+ UserHandle.CURRENT);
+ }
+
+ private void writeValue(String profiles) {
+ mSharedPrefs.edit().putString(REFRESH_CONTROL, profiles).apply();
+ }
+
+ protected void getOldRate(){
+ defaultMaxRate = Settings.System.getFloat(mContext.getContentResolver(), KEY_PEAK_REFRESH_RATE, REFRESH_STATE_DEFAULT);
+ defaultMinRate = Settings.System.getFloat(mContext.getContentResolver(), KEY_MIN_REFRESH_RATE, REFRESH_STATE_DEFAULT);
+ }
+
+
+ private String getValue() {
+ String value = mSharedPrefs.getString(REFRESH_CONTROL, null);
+
+ if (value == null || value.isEmpty()) {
+ value = REFRESH_STANDARD + ":" + REFRESH_EXTREME;
+ writeValue(value);
+ }
+ return value;
+ }
+
+ protected void writePackage(String packageName, int mode) {
+ String value = getValue();
+ value = value.replace(packageName + ",", "");
+ String[] modes = value.split(":");
+ String finalString;
+
+ switch (mode) {
+ case STATE_STANDARD:
+ modes[0] = modes[0] + packageName + ",";
+ break;
+ case STATE_EXTREME:
+ modes[1] = modes[1] + packageName + ",";
+ break;
+ }
+
+ finalString = modes[0] + ":" + modes[1];
+
+ writeValue(finalString);
+ }
+
+ protected int getStateForPackage(String packageName) {
+ String value = getValue();
+ String[] modes = value.split(":");
+ int state = STATE_DEFAULT;
+ if (modes[0].contains(packageName + ",")) {
+ state = STATE_STANDARD;
+ } else if (modes[1].contains(packageName + ",")) {
+ state = STATE_EXTREME;
+ }
+ return state;
+ }
+
+ protected void setRefreshRate(String packageName) {
+ String value = getValue();
+ String modes[];
+ float maxrate = defaultMaxRate;
+ float minrate = defaultMinRate;
+ isAppInList = false;
+
+ if (value != null) {
+ modes = value.split(":");
+
+ if (modes[0].contains(packageName + ",")) {
+ maxrate = REFRESH_STATE_STANDARD;
+ if ( minrate > maxrate){
+ minrate = maxrate;
+ }
+ isAppInList = true;
+ } else if (modes[1].contains(packageName + ",")) {
+ maxrate = REFRESH_STATE_EXTREME;
+ if ( minrate > maxrate){
+ minrate = maxrate;
+ }
+ isAppInList = true;
+ }
+ }
+ Settings.System.putFloat(mContext.getContentResolver(), KEY_MIN_REFRESH_RATE, minrate);
+ Settings.System.putFloat(mContext.getContentResolver(), KEY_PEAK_REFRESH_RATE, maxrate);
+ }
+}
diff --git a/parts/src/org/lineageos/settings/utils/ComponentUtils.java b/parts/src/org/lineageos/settings/utils/ComponentUtils.java
new file mode 100644
index 0000000..28701b5
--- /dev/null
+++ b/parts/src/org/lineageos/settings/utils/ComponentUtils.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 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.settings.utils;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+public class ComponentUtils {
+
+ /**
+ * Enables or disables a specified Android component dynamically at runtime.
+ *
+ * @param context The context from which the component will be enabled or disabled.
+ * @param componentClass The class of the component to be enabled or disabled.
+ * @param enable If true, the component will be enabled; if false, it will be disabled.
+ */
+ public static void toggleComponent(Context context, Class> componentClass, boolean enable) {
+ ComponentName componentName = new ComponentName(context, componentClass);
+ PackageManager packageManager = context.getPackageManager();
+ int currentState = packageManager.getComponentEnabledSetting(componentName);
+ int newState = enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+
+ if (currentState != newState) {
+ packageManager.setComponentEnabledSetting(
+ componentName,
+ newState,
+ PackageManager.DONT_KILL_APP
+ );
+ }
+ }
+}
diff --git a/parts/src/org/lineageos/settings/utils/FileUtils.java b/parts/src/org/lineageos/settings/utils/FileUtils.java
new file mode 100644
index 0000000..b2bc40d
--- /dev/null
+++ b/parts/src/org/lineageos/settings/utils/FileUtils.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2016 The CyanogenMod Project
+ * 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.settings.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);
+ writer.flush();
+ } 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;
+ }
+
+ /**
+ * Writes the given value into the given file.
+ * @return true on success, false on failure
+ */
+ public static boolean writeValue(String fileName, String value) {
+ BufferedWriter writer = null;
+
+ try {
+ writer = new BufferedWriter(new FileWriter(fileName));
+ writer.write(value);
+ writer.flush();
+ } 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;
+ }
+
+ /**
+ * Reads the value from the given file.
+ * @return the value read from the file, or the default value if an error occurs
+ */
+ public static String getFileValue(String fileName, String defaultValue) {
+ String value = defaultValue;
+ BufferedReader reader = null;
+
+ try {
+ reader = new BufferedReader(new FileReader(fileName), 512);
+ value = 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 value;
+ }
+}
+
+
diff --git a/parts/src/org/lineageos/settings/utils/TileUtils.java b/parts/src/org/lineageos/settings/utils/TileUtils.java
new file mode 100644
index 0000000..b3feff4
--- /dev/null
+++ b/parts/src/org/lineageos/settings/utils/TileUtils.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 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.settings.utils;
+
+import android.app.StatusBarManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.widget.Toast;
+
+import org.lineageos.settings.R;
+
+public class TileUtils {
+
+ public static void requestAddTileService(Context context, Class> tileServiceClass, int labelResId, int iconResId) {
+ ComponentName componentName = new ComponentName(context, tileServiceClass);
+ String label = context.getString(labelResId);
+ Icon icon = Icon.createWithResource(context, iconResId);
+
+ StatusBarManager sbm = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE);
+
+ if (sbm != null) {
+ sbm.requestAddTileService(
+ componentName,
+ label,
+ icon,
+ context.getMainExecutor(),
+ result -> handleResult(context, result)
+ );
+ }
+ }
+
+ private static void handleResult(Context context, Integer result) {
+ if (result == null)
+ return;
+ switch (result) {
+ case StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED:
+ Toast.makeText(context, R.string.tile_added, Toast.LENGTH_SHORT).show();
+ break;
+ case StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED:
+ Toast.makeText(context, R.string.tile_not_added, Toast.LENGTH_SHORT).show();
+ break;
+ case StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED:
+ Toast.makeText(context, R.string.tile_already_added, Toast.LENGTH_SHORT).show();
+ break;
+ }
+ }
+}
diff --git a/rootdir/vendor/etc/init/hw/init.a71.rc b/rootdir/vendor/etc/init/hw/init.a71.rc
index daa175a..d81c93d 100755
--- a/rootdir/vendor/etc/init/hw/init.a71.rc
+++ b/rootdir/vendor/etc/init/hw/init.a71.rc
@@ -48,6 +48,12 @@ on boot
chown radio system /sys/power/cpufreq_table
chmod 0664 /sys/power/cpufreq_table
+ chown system graphics /sys/class/drm/sde-crtc-0/measured_fps
+ chmod 0660 /sys/class/drm/sde-crtc-0/measured_fps
+
+ chown system system /sys/class/thermal/thermal_zone78/temp
+ chmod 0660 /sys/class/thermal/thermal_zone78/temp
+
on post-fs
on post-fs-data
@@ -126,6 +132,8 @@ on post-fs
chown radio system /efs/carrier
chown radio system /efs/carrier/HiddenMenu
+
+
# MST/NFC Switch
chown system system /dev/mst_ctrl
chmod 0660 /dev/mst_ctrl