From b7d8c1682c232e9dc67de350a596d8dbc16b4873 Mon Sep 17 00:00:00 2001 From: just_inntime Date: Thu, 3 Apr 2025 00:45:07 +0800 Subject: [PATCH] a71: Introduce parts [1/2] Based on XiaomiParts for peridot Special thanks to kenway214 --- device.mk | 6 +- parts/Android.bp | 43 + parts/AndroidManifest.xml | 164 ++++ ...ivapp_whitelist_org.lineageos.settings.xml | 21 + parts/proguard.flags | 3 + .../res/drawable/ic_gamebar_overlay_tile.xml | 10 + parts/res/drawable/ic_qs_refresh_rate.xml | 29 + parts/res/drawable/ic_refresh_120.xml | 22 + parts/res/drawable/ic_refresh_60.xml | 17 + parts/res/drawable/ic_refresh_default.xml | 11 + parts/res/drawable/ic_scenes.xml | 9 + parts/res/layout/activity_game_overlay.xml | 12 + parts/res/layout/game_overlay.xml | 10 + parts/res/layout/refresh_layout.xml | 18 + parts/res/layout/refresh_list_item.xml | 64 ++ parts/res/values/arrays.xml | 98 +++ parts/res/values/strings.xml | 54 ++ parts/res/xml/game_overlay_preferences.xml | 221 +++++ .../settings/BootCompletedReceiver.java | 75 ++ .../settings/TileHandlerActivity.java | 93 +++ .../gameoverlay/ForegroundAppDetector.java | 95 +++ .../settings/gameoverlay/GameDataExport.java | 132 +++ .../settings/gameoverlay/GameOverlay.java | 776 ++++++++++++++++++ .../gameoverlay/GameOverlayBootReceiver.java | 46 ++ .../gameoverlay/GameOverlayCpuInfo.java | 130 +++ .../gameoverlay/GameOverlayFragment.java | 358 ++++++++ .../gameoverlay/GameOverlayGpuInfo.java | 80 ++ .../gameoverlay/GameOverlayMemInfo.java | 65 ++ .../GameOverlaySettingsActivity.java | 56 ++ .../gameoverlay/GameOverlayTileService.java | 73 ++ .../settings/refreshrate/RefreshActivity.java | 33 + .../settings/refreshrate/RefreshService.java | 102 +++ .../refreshrate/RefreshSettingsFragment.java | 419 ++++++++++ .../refreshrate/RefreshTileService.java | 114 +++ .../settings/refreshrate/RefreshUtils.java | 141 ++++ .../settings/utils/ComponentUtils.java | 47 ++ .../lineageos/settings/utils/FileUtils.java | 222 +++++ .../lineageos/settings/utils/TileUtils.java | 62 ++ rootdir/vendor/etc/init/hw/init.a71.rc | 8 + 39 files changed, 3938 insertions(+), 1 deletion(-) create mode 100644 parts/Android.bp create mode 100644 parts/AndroidManifest.xml create mode 100644 parts/permissions/privapp_whitelist_org.lineageos.settings.xml create mode 100644 parts/proguard.flags create mode 100644 parts/res/drawable/ic_gamebar_overlay_tile.xml create mode 100644 parts/res/drawable/ic_qs_refresh_rate.xml create mode 100644 parts/res/drawable/ic_refresh_120.xml create mode 100644 parts/res/drawable/ic_refresh_60.xml create mode 100644 parts/res/drawable/ic_refresh_default.xml create mode 100644 parts/res/drawable/ic_scenes.xml create mode 100644 parts/res/layout/activity_game_overlay.xml create mode 100644 parts/res/layout/game_overlay.xml create mode 100644 parts/res/layout/refresh_layout.xml create mode 100644 parts/res/layout/refresh_list_item.xml create mode 100644 parts/res/values/arrays.xml create mode 100644 parts/res/values/strings.xml create mode 100644 parts/res/xml/game_overlay_preferences.xml create mode 100644 parts/src/org/lineageos/settings/BootCompletedReceiver.java create mode 100644 parts/src/org/lineageos/settings/TileHandlerActivity.java create mode 100644 parts/src/org/lineageos/settings/gameoverlay/ForegroundAppDetector.java create mode 100644 parts/src/org/lineageos/settings/gameoverlay/GameDataExport.java create mode 100644 parts/src/org/lineageos/settings/gameoverlay/GameOverlay.java create mode 100644 parts/src/org/lineageos/settings/gameoverlay/GameOverlayBootReceiver.java create mode 100644 parts/src/org/lineageos/settings/gameoverlay/GameOverlayCpuInfo.java create mode 100644 parts/src/org/lineageos/settings/gameoverlay/GameOverlayFragment.java create mode 100644 parts/src/org/lineageos/settings/gameoverlay/GameOverlayGpuInfo.java create mode 100644 parts/src/org/lineageos/settings/gameoverlay/GameOverlayMemInfo.java create mode 100644 parts/src/org/lineageos/settings/gameoverlay/GameOverlaySettingsActivity.java create mode 100644 parts/src/org/lineageos/settings/gameoverlay/GameOverlayTileService.java create mode 100644 parts/src/org/lineageos/settings/refreshrate/RefreshActivity.java create mode 100644 parts/src/org/lineageos/settings/refreshrate/RefreshService.java create mode 100644 parts/src/org/lineageos/settings/refreshrate/RefreshSettingsFragment.java create mode 100644 parts/src/org/lineageos/settings/refreshrate/RefreshTileService.java create mode 100644 parts/src/org/lineageos/settings/refreshrate/RefreshUtils.java create mode 100644 parts/src/org/lineageos/settings/utils/ComponentUtils.java create mode 100644 parts/src/org/lineageos/settings/utils/FileUtils.java create mode 100644 parts/src/org/lineageos/settings/utils/TileUtils.java 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