38
Android.bp
Normal file
38
Android.bp
Normal file
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// Copyright (C) 2025 kenway214
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
android_app {
|
||||
name: "GameBar",
|
||||
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"
|
||||
],
|
||||
|
||||
required: [
|
||||
"privapp_whitelist_com.android.gamebar.xml",
|
||||
],
|
||||
}
|
||||
|
||||
prebuilt_etc {
|
||||
name: "privapp_whitelist_com.android.gamebar.xml",
|
||||
src: "permissions/privapp_whitelist_com.android.gamebar.xml",
|
||||
sub_dir: "permissions",
|
||||
system_ext_specific: true,
|
||||
}
|
||||
149
AndroidManifest.xml
Normal file
149
AndroidManifest.xml
Normal file
@@ -0,0 +1,149 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.android.gamebar"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0"
|
||||
android:sharedUserId="android.uid.system">
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.GET_TASKS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" />
|
||||
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" tools:ignore="ProtectedPermissions" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_APPLICATION_OVERLAY" />
|
||||
<uses-permission android:name="android.permission.MANAGE_GAME_MODE" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FPS_COUNTER" />
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="33"
|
||||
android:targetSdkVersion="35"/>
|
||||
|
||||
<application
|
||||
android:label="@string/app_title"
|
||||
android:persistent="true"
|
||||
android:defaultToDeviceProtectedStorage="true"
|
||||
android:directBootAware="true"
|
||||
android:theme="@style/Theme.SubSettingsBase">
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
tools:replace="android:authorities"/>
|
||||
|
||||
<!-- TileHandler activity -->
|
||||
<activity
|
||||
android:name=".utils.TileHandlerActivity"
|
||||
android:exported="true"
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- GameBar Overlay -->
|
||||
<activity
|
||||
android:name=".GameBarSettingsActivity"
|
||||
android:label="@string/game_bar_title"
|
||||
android:theme="@style/Theme.SubSettingsBase"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="com.android.settings.action.IA_SETTINGS" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="com.android.settings.category"
|
||||
android:value="com.android.settings.category.ia.apps" />
|
||||
<meta-data
|
||||
android:name="com.android.settings.icon"
|
||||
android:resource="@drawable/ic_gamebar" />
|
||||
</activity>
|
||||
|
||||
<!-- GameBar Overlay Tile Service -->
|
||||
<service
|
||||
android:name=".GameBarTileService"
|
||||
android:label="@string/game_bar_tile_label"
|
||||
android:icon="@drawable/ic_gamebar"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.service.quicksettings.TOGGLEABLE_TILE"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<!-- GameBar Overlay Monitor Service -->
|
||||
<service
|
||||
android:name=".GameBarMonitorService"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Per-App Logging Service -->
|
||||
<service
|
||||
android:name=".PerAppLogService"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- GameBar BootReceiver -->
|
||||
<receiver
|
||||
android:name=".GameBarBootReceiver"
|
||||
android:exported="true"
|
||||
android:enabled="true"
|
||||
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".GameBarPerAppConfigActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.SubSettingsBase" />
|
||||
|
||||
<activity
|
||||
android:name=".GameBarLogActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.SubSettingsBase" />
|
||||
|
||||
<activity
|
||||
android:name=".PerAppLogViewActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.SubSettingsBase" />
|
||||
|
||||
<activity
|
||||
android:name=".LogAnalyticsActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.SubSettingsBase" />
|
||||
|
||||
<!-- FileProvider for sharing log files -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_provider_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
15
gamebar.mk
Normal file
15
gamebar.mk
Normal file
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Copyright (C) 2025 kenway214
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
# GameBar app
|
||||
PRODUCT_PACKAGES += \
|
||||
GameBar
|
||||
|
||||
# GameBar init rc
|
||||
PRODUCT_PACKAGES += \
|
||||
init.gamebar.rc
|
||||
|
||||
# Gamebar sepolicy
|
||||
include packages/apps/GameBar/sepolicy/SEPolicy.mk
|
||||
11
init/Android.bp
Normal file
11
init/Android.bp
Normal file
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// Copyright (C) 2025 kenway214
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
prebuilt_etc {
|
||||
name: "init.gamebar.rc",
|
||||
src: "init.gamebar.rc",
|
||||
sub_dir: "init",
|
||||
vendor: true,
|
||||
}
|
||||
6
init/init.gamebar.rc
Normal file
6
init/init.gamebar.rc
Normal file
@@ -0,0 +1,6 @@
|
||||
on boot
|
||||
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/power_supply/battery/temp
|
||||
chmod 0660 /sys/class/power_supply/battery/temp
|
||||
20
permissions/privapp_whitelist_com.android.gamebar.xml
Normal file
20
permissions/privapp_whitelist_com.android.gamebar.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<permissions>
|
||||
<privapp-permissions package="com.android.gamebar">
|
||||
<permission name="android.permission.WRITE_SECURE_SETTINGS"/>
|
||||
<permission name="android.permission.GET_TASKS" />
|
||||
<permission name="android.permission.INTERACT_ACROSS_USERS" />
|
||||
<permission name="android.permission.MANAGE_GAME_MODE" />
|
||||
<permission name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
<permission name="android.permission.READ_DEVICE_CONFIG" />
|
||||
<permission name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<permission name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<permission name="android.permission.WRITE_DEVICE_CONFIG" />
|
||||
<permission name="android.permission.WAKE_LOCK" />
|
||||
<permission name="android.permission.ACCESS_FPS_COUNTER" />
|
||||
</privapp-permissions>
|
||||
</permissions>
|
||||
8
res/color-night/app_name_text_selector.xml
Normal file
8
res/color-night/app_name_text_selector.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#FFFFFF"/>
|
||||
</selector>
|
||||
8
res/color-night/app_package_text_selector.xml
Normal file
8
res/color-night/app_package_text_selector.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#B0B0B0"/>
|
||||
</selector>
|
||||
8
res/color-night/button_bg_selector.xml
Normal file
8
res/color-night/button_bg_selector.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#232B36"/>
|
||||
</selector>
|
||||
8
res/color-night/button_text_selector.xml
Normal file
8
res/color-night/button_text_selector.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#FFFFFF"/>
|
||||
</selector>
|
||||
8
res/color-night/search_bg_selector.xml
Normal file
8
res/color-night/search_bg_selector.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#232B36"/>
|
||||
</selector>
|
||||
8
res/color/app_name_text_selector.xml
Normal file
8
res/color/app_name_text_selector.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#000000"/>
|
||||
</selector>
|
||||
8
res/color/app_package_text_selector.xml
Normal file
8
res/color/app_package_text_selector.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#444444"/>
|
||||
</selector>
|
||||
8
res/color/button_bg_selector.xml
Normal file
8
res/color/button_bg_selector.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#E3EAF5"/>
|
||||
</selector>
|
||||
8
res/color/button_text_selector.xml
Normal file
8
res/color/button_text_selector.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#232B36"/>
|
||||
</selector>
|
||||
8
res/color/gamebar_green.xml
Normal file
8
res/color/gamebar_green.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#4CAF50"/>
|
||||
</selector>
|
||||
8
res/color/search_bg_selector.xml
Normal file
8
res/color/search_bg_selector.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="#F0F0F0"/>
|
||||
</selector>
|
||||
9
res/drawable/bg_button_rounded.xml
Normal file
9
res/drawable/bg_button_rounded.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/button_bg_selector"/>
|
||||
<corners android:radius="24dp"/>
|
||||
</shape>
|
||||
11
res/drawable/bg_round_icon.xml
Normal file
11
res/drawable/bg_round_icon.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#E0E0E0"/>
|
||||
<corners android:radius="24dp"/>
|
||||
<size android:width="48dp" android:height="48dp"/>
|
||||
<padding android:left="2dp" android:top="2dp" android:right="2dp" android:bottom="2dp"/>
|
||||
</shape>
|
||||
10
res/drawable/bg_search_rounded.xml
Normal file
10
res/drawable/bg_search_rounded.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/search_bg_selector"/>
|
||||
<corners android:radius="16dp"/>
|
||||
<padding android:left="8dp" android:top="4dp" android:right="8dp" android:bottom="4dp"/>
|
||||
</shape>
|
||||
14
res/drawable/ic_article_shortcut.xml
Normal file
14
res/drawable/ic_article_shortcut.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M400,680L560,680L560,600L400,600L400,680ZM400,520L680,520L680,440L400,440L400,520ZM280,360L680,360L680,280L280,280L280,360ZM480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480ZM265,880Q186,880 130.5,824.5Q75,769 75,690Q75,633 104.5,588Q134,543 182,520L80,520L80,440L320,440L320,680L240,680L240,583Q203,591 179,621Q155,651 155,690Q155,736 187.5,768Q220,800 265,800L265,880ZM400,840L400,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,360L120,360L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L400,840Z"/>
|
||||
</vector>
|
||||
14
res/drawable/ic_chevron_right.xml
Normal file
14
res/drawable/ic_chevron_right.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M8.59,16.59L13.17,12 8.59,7.41 10,6l6,6 -6,6z"/>
|
||||
</vector>
|
||||
14
res/drawable/ic_custom_seekbar_minus.xml
Normal file
14
res/drawable/ic_custom_seekbar_minus.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2018-2022 crDroid Android Project
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24.0dp"
|
||||
android:height="24.0dp"
|
||||
android:viewportWidth="48.0"
|
||||
android:viewportHeight="48.0" >
|
||||
<path
|
||||
android:fillColor="?android:attr/colorControlNormal"
|
||||
android:pathData="M24 4C12.972 4 4 12.972 4 24s8.972 20 20 20s20 -8.972 20 -20S35.028 4 24 4zM32.5 25.5h-17c-0.829 0 -1.5 -0.671 -1.5 -1.5s0.671 -1.5 1.5 -1.5h17c0.829 0 1.5 0.671 1.5 1.5S33.329 25.5 32.5 25.5z" />
|
||||
</vector>
|
||||
14
res/drawable/ic_custom_seekbar_plus.xml
Normal file
14
res/drawable/ic_custom_seekbar_plus.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2018-2022 crDroid Android Project
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24.0dp"
|
||||
android:height="24.0dp"
|
||||
android:viewportWidth="48.0"
|
||||
android:viewportHeight="48.0" >
|
||||
<path
|
||||
android:fillColor="?android:attr/colorControlNormal"
|
||||
android:pathData="M24 4C12.972 4 4 12.972 4 24s8.972 20 20 20s20 -8.972 20 -20S35.028 4 24 4zM32.5 25.5h-7v7c0 0.829 -0.672 1.5 -1.5 1.5s-1.5 -0.671 -1.5 -1.5v-7h-7c-0.828 0 -1.5 -0.671 -1.5 -1.5s0.672 -1.5 1.5 -1.5h7v-7c0 -0.829 0.672 -1.5 1.5 -1.5s1.5 0.671 1.5 1.5v7h7c0.828 0 1.5 0.671 1.5 1.5S33.328 25.5 32.5 25.5z" />
|
||||
</vector>
|
||||
15
res/drawable/ic_custom_seekbar_reset.xml
Normal file
15
res/drawable/ic_custom_seekbar_reset.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2019 Havoc-OS
|
||||
2022 DerpFest
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24.0dp"
|
||||
android:height="24.0dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0" >
|
||||
<path
|
||||
android:fillColor="?android:attr/colorControlNormal"
|
||||
android:pathData="M14.123456746339798,12.042328000068665 c0,-0.7430335164070133 -0.6079365134239197,-1.3509700298309326 -1.3509700298309326,-1.3509700298309326 s-1.3509700298309326,0.6079365134239197 -1.3509700298309326,1.3509700298309326 s0.6079365134239197,1.3509700298309326 1.3509700298309326,1.3509700298309326 s1.3509700298309326,-0.6079365134239197 1.3509700298309326,-1.3509700298309326 zM12.772486716508865,5.962962865829468 c-3.3571605241298674,0 -6.079365134239197,2.7222046101093293 -6.079365134239197,6.079365134239197 L4.66666653752327,12.042328000068665 l2.7019400596618652,2.7019400596618652 l2.7019400596618652,-2.7019400596618652 L8.044091612100601,12.042328000068665 c0,-2.6141270077228547 2.1142680966854095,-4.728395104408264 4.728395104408264,-4.728395104408264 s4.728395104408264,2.1142680966854095 4.728395104408264,4.728395104408264 s-2.1142680966854095,4.728395104408264 -4.728395104408264,4.728395104408264 c-1.0199823725223542,0 -1.965661393404007,-0.3309876573085789 -2.742469160556793,-0.8781305193901066 l-0.9591887211799618,0.9726984214782719 C10.097566057443618,17.648853623867033 11.380987585783004,18.12169313430786 12.772486716508865,18.12169313430786 c3.3571605241298674,0 6.079365134239197,-2.7222046101093293 6.079365134239197,-6.079365134239197 s-2.7222046101093293,-6.079365134239197 -6.079365134239197,-6.079365134239197 z" />
|
||||
</vector>
|
||||
15
res/drawable/ic_delete.xml
Normal file
15
res/drawable/ic_delete.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorOnSurface">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
||||
15
res/drawable/ic_export.xml
Normal file
15
res/drawable/ic_export.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorOnSurface">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M9,3L5,6.99h3V14h2V6.99h3L9,3zM16,17.01V10h-2v7.01h-3L15,21l4,-3.99H16z"/>
|
||||
</vector>
|
||||
15
res/drawable/ic_gamebar.xml
Normal file
15
res/drawable/ic_gamebar.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24.0dp"
|
||||
android:height="24.0dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?android:attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M7,6H17A6,6 0 0,1 23,12A6,6 0 0,1 17,18C15.22,18 13.63,17.23 12.53,16H11.47C10.37,17.23 8.78,18 7,18A6,6 0 0,1 1,12A6,6 0 0,1 7,6M6,9V11H4V13H6V15H8V13H10V11H8V9H6M15.5,12A1.5,1.5 0 0,0 14,13.5A1.5,1.5 0 0,0 15.5,15A1.5,1.5 0 0,0 17,13.5A1.5,1.5 0 0,0 15.5,12M18.5,9A1.5,1.5 0 0,0 17,10.5A1.5,1.5 0 0,0 18.5,12A1.5,1.5 0 0,0 20,10.5A1.5,1.5 0 0,0 18.5,9Z" />
|
||||
</vector>
|
||||
15
res/drawable/ic_open_in_new.xml
Normal file
15
res/drawable/ic_open_in_new.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorOnSurface">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,19H5V5h7V3H5c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41V10h2V3h-7z"/>
|
||||
</vector>
|
||||
15
res/drawable/ic_share.xml
Normal file
15
res/drawable/ic_share.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorOnSurface">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
||||
13
res/drawable/spinner_popup_background.xml
Normal file
13
res/drawable/spinner_popup_background.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="?android:attr/colorBackground" />
|
||||
<corners android:radius="8dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="?android:attr/colorControlHighlight" />
|
||||
</shape>
|
||||
16
res/layout/activity_game_bar.xml
Normal file
16
res/layout/activity_game_bar.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/game_bar_fragment"
|
||||
android:name="com.android.gamebar.GameBarFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</FrameLayout>
|
||||
9
res/layout/activity_game_bar_app_selector.xml
Normal file
9
res/layout/activity_game_bar_app_selector.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/content_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
10
res/layout/activity_gamebar_log.xml
Normal file
10
res/layout/activity_gamebar_log.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/content_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#121212" />
|
||||
603
res/layout/dialog_log_analytics.xml
Normal file
603
res/layout/dialog_log_analytics.xml
Normal file
@@ -0,0 +1,603 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fillViewport="true"
|
||||
android:background="#121212">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- Session Info Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:backgroundTint="#121212"
|
||||
android:elevation="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_session_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Session Information"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="13sp"
|
||||
android:padding="12dp"
|
||||
android:fontFamily="monospace"
|
||||
android:lineSpacingExtra="2dp" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- FPS Graph Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:backgroundTint="#121212"
|
||||
android:elevation="4dp">
|
||||
|
||||
<com.android.gamebar.FpsGraphView
|
||||
android:id="@+id/fps_graph_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="400dp"
|
||||
android:padding="8dp" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Frame Time Graph Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:backgroundTint="#121212"
|
||||
android:elevation="4dp">
|
||||
|
||||
<com.android.gamebar.FrameTimeGraphView
|
||||
android:id="@+id/frame_time_graph_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="400dp"
|
||||
android:padding="8dp" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Divider -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="#333333"
|
||||
android:layout_marginVertical="8dp" />
|
||||
|
||||
<!-- FPS Statistics -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="FPS STATISTICS"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingVertical="8dp"
|
||||
android:gravity="center" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="2">
|
||||
|
||||
<!-- Left Column -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:paddingEnd="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_max_fps"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Max FPS: 0.0"
|
||||
android:textColor="#4CAF50"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_min_fps"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Min FPS: 0.0"
|
||||
android:textColor="#F44336"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_avg_fps"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Avg FPS: 0.0"
|
||||
android:textColor="#FF9800"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Right Column -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_variance"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Variance: 0.00"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_std_dev"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Std Dev: 0.00"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_smoothness"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Smoothness: 0.0%"
|
||||
android:textColor="#4CAF50"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Frame Time Lows (under FPS Statistics) -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="#333333"
|
||||
android:layout_marginVertical="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="2">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_1percent_low"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="1% Low: 0.0 FPS"
|
||||
android:textColor="#F44336"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp"
|
||||
android:paddingEnd="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_01percent_low"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="0.1% Low: 0.0 FPS"
|
||||
android:textColor="#FF5252"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp"
|
||||
android:paddingStart="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Divider -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="#333333"
|
||||
android:layout_marginVertical="16dp" />
|
||||
|
||||
<!-- CPU Statistics Section -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="CPU STATISTICS"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingVertical="8dp"
|
||||
android:gravity="center" />
|
||||
|
||||
<!-- CPU Usage Graph Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:backgroundTint="#121212"
|
||||
android:elevation="4dp">
|
||||
|
||||
<com.android.gamebar.CpuGraphView
|
||||
android:id="@+id/cpu_usage_graph_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="250dp"
|
||||
android:padding="8dp" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- CPU Temperature Graph Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:backgroundTint="#121212"
|
||||
android:elevation="4dp">
|
||||
|
||||
<com.android.gamebar.CpuTempGraphView
|
||||
android:id="@+id/cpu_temp_graph_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="250dp"
|
||||
android:padding="8dp" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- CPU Clock Graph Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:backgroundTint="#121212"
|
||||
android:elevation="4dp">
|
||||
|
||||
<com.android.gamebar.CpuClockGraphView
|
||||
android:id="@+id/cpu_clock_graph_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="300dp"
|
||||
android:padding="8dp" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Divider -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="#333333"
|
||||
android:layout_marginVertical="8dp" />
|
||||
|
||||
<!-- CPU Stats Summary -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="CPU SUMMARY"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingVertical="8dp"
|
||||
android:gravity="center" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="2">
|
||||
|
||||
<!-- Left Column -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:paddingEnd="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_max_cpu_usage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Max Usage: 0%"
|
||||
android:textColor="#4CAF50"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_min_cpu_usage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Min Usage: 0%"
|
||||
android:textColor="#F44336"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_avg_cpu_usage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Avg Usage: 0%"
|
||||
android:textColor="#FF9800"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Right Column -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_max_cpu_temp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Max Temp: 0.0°C"
|
||||
android:textColor="#FF5722"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_min_cpu_temp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Min Temp: 0.0°C"
|
||||
android:textColor="#4CAF50"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_avg_cpu_temp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Avg Temp: 0.0°C"
|
||||
android:textColor="#FF9800"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Divider -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="#333333"
|
||||
android:layout_marginVertical="16dp" />
|
||||
|
||||
<!-- GPU Statistics Section -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="GPU STATISTICS"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingVertical="8dp"
|
||||
android:gravity="center" />
|
||||
|
||||
<!-- GPU Usage Graph Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:backgroundTint="#121212"
|
||||
android:elevation="4dp">
|
||||
|
||||
<com.android.gamebar.GpuUsageGraphView
|
||||
android:id="@+id/gpu_usage_graph_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="250dp"
|
||||
android:padding="8dp" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- GPU Temperature Graph Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:backgroundTint="#121212"
|
||||
android:elevation="4dp">
|
||||
|
||||
<com.android.gamebar.GpuTempGraphView
|
||||
android:id="@+id/gpu_temp_graph_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="250dp"
|
||||
android:padding="8dp" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- GPU Clock Graph Card -->
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:backgroundTint="#121212"
|
||||
android:elevation="4dp">
|
||||
|
||||
<com.android.gamebar.GpuClockGraphView
|
||||
android:id="@+id/gpu_clock_graph_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="250dp"
|
||||
android:padding="8dp" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<!-- Divider -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="#333333"
|
||||
android:layout_marginVertical="8dp" />
|
||||
|
||||
<!-- GPU Stats Summary -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="GPU SUMMARY"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingVertical="8dp"
|
||||
android:gravity="center" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="2">
|
||||
|
||||
<!-- Left Column -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:paddingEnd="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_max_gpu_usage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Max Usage: 0%"
|
||||
android:textColor="#4CAF50"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_min_gpu_usage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Min Usage: 0%"
|
||||
android:textColor="#F44336"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_avg_gpu_usage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Avg Usage: 0%"
|
||||
android:textColor="#FF9800"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Right Column -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_max_gpu_clock"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Max Clock: 0 MHz"
|
||||
android:textColor="#4CAF50"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_min_gpu_clock"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Min Clock: 0 MHz"
|
||||
android:textColor="#F44336"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_avg_gpu_clock"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Avg Clock: 0 MHz"
|
||||
android:textColor="#FF9800"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- GPU Temperature Stats -->
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="#333333"
|
||||
android:layout_marginVertical="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="3">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_max_gpu_temp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Max Temp: 0.0°C"
|
||||
android:textColor="#FF5722"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp"
|
||||
android:paddingEnd="8dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_min_gpu_temp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Min Temp: 0.0°C"
|
||||
android:textColor="#4CAF50"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_avg_gpu_temp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Avg Temp: 0.0°C"
|
||||
android:textColor="#FF9800"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingVertical="4dp"
|
||||
android:paddingStart="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
117
res/layout/fragment_gamebar_log.xml
Normal file
117
res/layout/fragment_gamebar_log.xml
Normal file
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/search_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Search logs..."
|
||||
android:inputType="text"
|
||||
android:background="@drawable/bg_search_rounded"
|
||||
android:padding="12dp"
|
||||
android:textColor="@android:color/primary_text_light"
|
||||
android:textColorHint="@android:color/darker_gray"
|
||||
android:elevation="2dp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/rg_log_type"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:gravity="center">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_global_logging"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Global"
|
||||
android:textColor="@color/app_name_text_selector"
|
||||
android:checked="true"/>
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_per_app_logging"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Per App"
|
||||
android:textColor="@color/app_name_text_selector"/>
|
||||
</RadioGroup>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_manual_logs"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Manual Logs"
|
||||
android:background="@drawable/bg_button_rounded"
|
||||
android:textColor="@color/button_text_selector"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:elevation="2dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_start_capture"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Start Capture"
|
||||
android:background="@drawable/bg_button_rounded"
|
||||
android:textColor="@color/button_text_selector"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:elevation="2dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_stop_capture"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Stop Capture"
|
||||
android:background="@drawable/bg_button_rounded"
|
||||
android:textColor="@color/button_text_selector"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:elevation="2dp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_log_history"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
</LinearLayout>
|
||||
73
res/layout/fragment_per_app_log_view.xml
Normal file
73
res/layout/fragment_per_app_log_view.xml
Normal file
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/search_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Search logs..."
|
||||
android:inputType="text"
|
||||
android:background="@drawable/bg_search_rounded"
|
||||
android:padding="12dp"
|
||||
android:textColor="@android:color/primary_text_light"
|
||||
android:textColorHint="@android:color/darker_gray"
|
||||
android:elevation="2dp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_log_history"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/empty_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:alpha="0.5"
|
||||
android:src="@drawable/ic_article_shortcut"
|
||||
android:tint="@android:color/darker_gray" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_empty_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="No logs available"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:text="Enable per-app logging and use the app to generate logs"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:textSize="14sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
14
res/layout/game_bar.xml
Normal file
14
res/layout/game_bar.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/game_bar_root"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#80000000"
|
||||
android:padding="8dp"
|
||||
android:orientation="vertical">
|
||||
</LinearLayout>
|
||||
50
res/layout/item_log_file.xml
Normal file
50
res/layout/item_log_file.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:src="@drawable/ic_export"
|
||||
android:scaleType="centerInside"
|
||||
android:background="@drawable/bg_round_icon"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_file_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textStyle="bold"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@color/app_name_text_selector"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_file_info"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/app_package_text_selector"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
65
res/layout/item_per_app_log.xml
Normal file
65
res/layout/item_per_app_log.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:background="?android:attr/selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_app_icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:scaleType="centerInside" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_app_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textStyle="bold"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@color/app_name_text_selector"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_package_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@color/app_package_text_selector"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_view_logs"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:src="@drawable/ic_article_shortcut"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="4dp"
|
||||
android:scaleType="centerInside" />
|
||||
|
||||
<Switch
|
||||
android:id="@+id/switch_per_app_logging"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
40
res/layout/preference_category_expandable.xml
Normal file
40
res/layout/preference_category_expandable.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:background="?android:attr/selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/expand_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:src="@drawable/ic_chevron_right"
|
||||
android:tint="?android:attr/textColorSecondary"
|
||||
android:rotation="0"
|
||||
android:contentDescription="Expand/Collapse" />
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textStyle="bold"
|
||||
android:textSize="14sp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end" />
|
||||
|
||||
</LinearLayout>
|
||||
131
res/layout/preference_custom_seekbar.xml
Normal file
131
res/layout/preference_custom_seekbar.xml
Normal file
@@ -0,0 +1,131 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2017-2022 crDroid Android Project
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?android:attr/listPreferredItemHeightSmall"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||
android:background="?android:attr/activatedBackgroundIndicator"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@android:id/icon_frame"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="-4dp"
|
||||
android:minWidth="60dp"
|
||||
android:gravity="start|center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp">
|
||||
<com.android.internal.widget.PreferenceImageView
|
||||
android:id="@android:id/icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxWidth="48dp"
|
||||
android:maxHeight="48dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:ellipsize="marquee" />
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/summary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@android:id/title"
|
||||
android:layout_alignStart="@android:id/title"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:maxLines="10"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/value_frame"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@android:id/summary"
|
||||
android:layout_alignStart="@android:id/title" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/reset"
|
||||
android:src="@drawable/ic_custom_seekbar_reset"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_toEndOf="@id/value"
|
||||
android:layout_centerVertical="true" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/seekbar_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/value_frame"
|
||||
android:layout_alignStart="@android:id/title" >
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/minus"
|
||||
android:src="@drawable/ic_custom_seekbar_minus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/plus"
|
||||
android:src="@drawable/ic_custom_seekbar_plus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/seekbar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_toEndOf="@id/minus"
|
||||
android:layout_toStartOf="@id/plus"
|
||||
android:layout_centerVertical="true" />
|
||||
</RelativeLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
<!-- Preference should place its actual preference widget here. -->
|
||||
<LinearLayout android:id="@android:id/widget_frame"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="end|center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:orientation="vertical" />
|
||||
</LinearLayout>
|
||||
80
res/layout/preference_slider.xml
Normal file
80
res/layout/preference_slider.xml
Normal file
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2015 The Android Open Source Project
|
||||
(C) 2018-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.
|
||||
-->
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@android:id/icon_frame"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@android:id/summary">
|
||||
|
||||
<ImageView
|
||||
android:id="@android:id/icon"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_gravity="start|center_vertical" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toEndOf="@android:id/icon_frame"
|
||||
android:ellipsize="marquee"
|
||||
android:fadingEdge="horizontal"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/summary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@android:id/title"
|
||||
android:layout_toEndOf="@android:id/icon_frame"
|
||||
android:maxLines="4"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seekbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_below="@android:id/summary"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_toEndOf="@android:id/icon_frame"
|
||||
android:tickMark="@null"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
style="@android:style/Widget.Material.SeekBar.Discrete" />
|
||||
|
||||
</RelativeLayout>
|
||||
18
res/layout/spinner_dropdown_item.xml
Normal file
18
res/layout/spinner_dropdown_item.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@android:id/text1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:gravity="start|center_vertical"
|
||||
android:minHeight="48dp"
|
||||
android:background="?android:attr/selectableItemBackground" />
|
||||
17
res/layout/spinner_item.xml
Normal file
17
res/layout/spinner_item.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@android:id/text1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:gravity="center_vertical"
|
||||
android:ellipsize="marquee" />
|
||||
11
res/menu/gamebar_log_menu.xml
Normal file
11
res/menu/gamebar_log_menu.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/menu_logging_parameters"
|
||||
android:title="Logging Parameters"
|
||||
android:showAsAction="never" />
|
||||
</menu>
|
||||
19
res/menu/gamebar_settings_menu.xml
Normal file
19
res/menu/gamebar_settings_menu.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/menu_log_monitor"
|
||||
android:title="Log Monitor"
|
||||
android:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/menu_open_external_log"
|
||||
android:title="Open External Log"
|
||||
android:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/menu_user_guide"
|
||||
android:title="@string/game_bar_user_guide"
|
||||
android:showAsAction="never" />
|
||||
</menu>
|
||||
28
res/menu/log_file_popup_menu.xml
Normal file
28
res/menu/log_file_popup_menu.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_open"
|
||||
android:title="Open"
|
||||
android:icon="@drawable/ic_open_in_new" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_share"
|
||||
android:title="Share"
|
||||
android:icon="@drawable/ic_share" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_export"
|
||||
android:title="Export"
|
||||
android:icon="@drawable/ic_export" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_delete"
|
||||
android:title="Delete"
|
||||
android:icon="@drawable/ic_delete" />
|
||||
|
||||
</menu>
|
||||
108
res/values/arrays.xml
Normal file
108
res/values/arrays.xml
Normal file
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<resources>
|
||||
<!-- FPS Overlay -->
|
||||
<string-array name="game_bar_fps_method_entries">
|
||||
<item>New API (Default)</item>
|
||||
<item>Legacy Sysfs</item>
|
||||
</string-array>
|
||||
<string-array name="game_bar_fps_method_values">
|
||||
<item>new</item>
|
||||
<item>legacy</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Update Interval -->
|
||||
<string-array name="fps_overlay_update_interval_entries">
|
||||
<item>Every 500ms</item>
|
||||
<item>Every second</item>
|
||||
<item>Every 2 seconds</item>
|
||||
<item>Every 5 seconds</item>
|
||||
</string-array>
|
||||
<string-array name="fps_overlay_update_interval_values">
|
||||
<item>500</item>
|
||||
<item>1000</item>
|
||||
<item>2000</item>
|
||||
<item>5000</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Position -->
|
||||
<string-array name="fps_overlay_position_entries">
|
||||
<item>Top Left</item>
|
||||
<item>Top Center</item>
|
||||
<item>Top Right</item>
|
||||
<item>Bottom Left</item>
|
||||
<item>Bottom Center</item>
|
||||
<item>Bottom Right</item>
|
||||
<item>Custom Draggable</item>
|
||||
</string-array>
|
||||
<string-array name="fps_overlay_position_values">
|
||||
<item>top_left</item>
|
||||
<item>top_center</item>
|
||||
<item>top_right</item>
|
||||
<item>bottom_left</item>
|
||||
<item>bottom_center</item>
|
||||
<item>bottom_right</item>
|
||||
<item>draggable</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Overlay color -->
|
||||
<string-array name="fps_overlay_color_entries">
|
||||
<item>White</item>
|
||||
<item>Crimson</item>
|
||||
<item>Fruit Salad</item>
|
||||
<item>Royal Blue</item>
|
||||
<item>Amber</item>
|
||||
<item>Teal</item>
|
||||
<item>Electric Violet</item>
|
||||
<item>Magenta</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="fps_overlay_color_values">
|
||||
<item>#FFFFFF</item>
|
||||
<item>#DC143C</item>
|
||||
<item>#4CAF50</item>
|
||||
<item>#4169E1</item>
|
||||
<item>#FFBF00</item>
|
||||
<item>#008080</item>
|
||||
<item>#8A2BE2</item>
|
||||
<item>#FF1493</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Overlay format -->
|
||||
<string-array name="game_bar_format_entries">
|
||||
<item>Full</item>
|
||||
<item>Minimal</item>
|
||||
</string-array>
|
||||
<string-array name="game_bar_format_values">
|
||||
<item>full</item>
|
||||
<item>minimal</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Split Mode -->
|
||||
<string-array name="game_bar_split_mode_entries">
|
||||
<item>Side-by-Side</item>
|
||||
<item>Stacked</item>
|
||||
</string-array>
|
||||
<string-array name="game_bar_split_mode_values">
|
||||
<item>side_by_side</item>
|
||||
<item>stacked</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Long press timeouts -->
|
||||
<string-array name="game_bar_longpress_entries">
|
||||
<item>1 second</item>
|
||||
<item>3 seconds</item>
|
||||
<item>5 seconds</item>
|
||||
<item>10 seconds</item>
|
||||
</string-array>
|
||||
<string-array name="game_bar_longpress_values">
|
||||
<item>1000</item>
|
||||
<item>3000</item>
|
||||
<item>5000</item>
|
||||
<item>10000</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
20
res/values/attrs.xml
Normal file
20
res/values/attrs.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<resources>
|
||||
<declare-styleable name="MinMaxSeekBarPreference">
|
||||
<attr name="minValue" format="integer" />
|
||||
<attr name="maxValue" format="integer" />
|
||||
</declare-styleable>
|
||||
|
||||
<!-- Base attributes available to PartsCustomSeekBarPreference. -->
|
||||
<declare-styleable name="PartsCustomSeekBarPreference">
|
||||
<attr name="defaultValueText" format="string" />
|
||||
<attr name="interval" format="integer" />
|
||||
<attr name="showSign" format="boolean" />
|
||||
<attr name="units" format="string|reference" />
|
||||
<attr name="continuousUpdates" format="boolean" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
35
res/values/strings.xml
Normal file
35
res/values/strings.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
|
||||
<!-- GameBar -->
|
||||
<string name="app_title">GameBar</string>
|
||||
<string name="tile_add">Add tile</string>
|
||||
<string name="tile_added">Tile added</string>
|
||||
<string name="tile_not_added">Tile not added</string>
|
||||
<string name="tile_already_added">Tile already added</string>
|
||||
<string name="tile_on">On</string>
|
||||
<string name="tile_off">Off</string>
|
||||
|
||||
<!-- Custom seekbar -->
|
||||
<string name="custom_seekbar_value">Value: <xliff:g id="v">%s</xliff:g></string>
|
||||
<string name="custom_seekbar_default_value">by default</string>
|
||||
<string name="custom_seekbar_default_value_to_set">Default value: <xliff:g id="v">%s</xliff:g>\nLong tap to set</string>
|
||||
<string name="custom_seekbar_default_value_is_set">Default value is set</string>
|
||||
|
||||
<!-- GameBar Overlay -->
|
||||
<string name="game_bar_title">GameBar</string>
|
||||
<string name="game_bar_summary">Enable the system overlay (FPS, Temp, etc.)</string>
|
||||
<string name="overlay_permission_required">Overlay permission is required</string>
|
||||
<string name="overlay_permission_granted">Overlay permission granted</string>
|
||||
<string name="overlay_permission_denied">Overlay permission denied</string>
|
||||
<string name="game_bar_tile_label">GameBar</string>
|
||||
<string name="game_bar_tile_description">Toggle the game overlay</string>
|
||||
<string name="game_bar_user_guide">User Guide</string>
|
||||
<string name="game_bar_user_guide_url">https://github.com/kenway214/packages_apps_GameBar/blob/main/GAMEBAR_USER_GUIDE.md</string>
|
||||
</resources>
|
||||
15
res/xml/file_provider_paths.xml
Normal file
15
res/xml/file_provider_paths.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<paths>
|
||||
<!-- External storage directory for log files -->
|
||||
<external-path name="external_files" path="." />
|
||||
|
||||
<!-- External storage files directory -->
|
||||
<external-files-path name="external_app_files" path="." />
|
||||
|
||||
<!-- Cache directory -->
|
||||
<external-cache-path name="external_cache" path="." />
|
||||
</paths>
|
||||
257
res/xml/game_bar_preferences.xml
Normal file
257
res/xml/game_bar_preferences.xml
Normal file
@@ -0,0 +1,257 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2025 kenway214
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:settings="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<com.android.settingslib.widget.MainSwitchPreference
|
||||
android:key="game_bar_enable"
|
||||
android:title="Enable GameBar Overlay"
|
||||
android:defaultValue="false" />
|
||||
|
||||
<com.android.gamebar.ExpandablePreferenceCategory
|
||||
android:key="category_overlay_features"
|
||||
android:title="Overlay Features"
|
||||
android:dependency="game_bar_enable">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="game_bar_fps_enable"
|
||||
android:title="FPS Overlay"
|
||||
android:summary="Show current FPS on screen"
|
||||
android:defaultValue="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="game_bar_frame_time_enable"
|
||||
android:title="Frame Time"
|
||||
android:summary="Show frame time in milliseconds"
|
||||
android:defaultValue="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="game_bar_temp_enable"
|
||||
android:title="Device Temperature"
|
||||
android:summary="Show device temperature"
|
||||
android:defaultValue="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="game_bar_cpu_usage_enable"
|
||||
android:title="CPU Usage"
|
||||
android:summary="Show current CPU usage percentage"
|
||||
android:defaultValue="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="game_bar_cpu_clock_enable"
|
||||
android:title="CPU Clock Speeds"
|
||||
android:summary="Show current CPU clock speeds for each core"
|
||||
android:defaultValue="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="game_bar_cpu_temp_enable"
|
||||
android:title="CPU Temperature"
|
||||
android:summary="Show CPU temperature"
|
||||
android:defaultValue="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="game_bar_ram_enable"
|
||||
android:title="RAM Usage"
|
||||
android:summary="Show current RAM usage in MB"
|
||||
android:defaultValue="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="game_bar_ram_speed_enable"
|
||||
android:title="RAM Frequency"
|
||||
android:summary="Show current RAM frequency(bus) in GHz"
|
||||
android:defaultValue="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="game_bar_ram_temp_enable"
|
||||
android:title="RAM Temperature"
|
||||
android:summary="Show current RAM temperature"
|
||||
android:defaultValue="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="game_bar_gpu_usage_enable"
|
||||
android:title="GPU Usage"
|
||||
android:summary="Show GPU usage percentage"
|
||||
android:defaultValue="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="game_bar_gpu_clock_enable"
|
||||
android:title="GPU Clock Speed"
|
||||
android:summary="Show current GPU clock frequency"
|
||||
android:defaultValue="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="game_bar_gpu_temp_enable"
|
||||
android:title="GPU Temperature"
|
||||
android:summary="Show current GPU temperature"
|
||||
android:defaultValue="false" />
|
||||
</com.android.gamebar.ExpandablePreferenceCategory>
|
||||
|
||||
<com.android.gamebar.ExpandablePreferenceCategory
|
||||
android:key="category_fps_method"
|
||||
android:title="FPS Measurement Method"
|
||||
android:dependency="game_bar_fps_enable">
|
||||
<ListPreference
|
||||
android:key="game_bar_fps_method"
|
||||
android:title="Select FPS Method"
|
||||
android:summary="Choose between the New API method (default) and Legacy Sysfs"
|
||||
android:defaultValue="new"
|
||||
android:entries="@array/game_bar_fps_method_entries"
|
||||
android:entryValues="@array/game_bar_fps_method_values" />
|
||||
</com.android.gamebar.ExpandablePreferenceCategory>
|
||||
|
||||
<com.android.gamebar.ExpandablePreferenceCategory
|
||||
android:key="category_customization"
|
||||
android:title="Customization"
|
||||
android:dependency="game_bar_enable">
|
||||
|
||||
<com.android.gamebar.utils.PartsCustomSeekBarPreference
|
||||
android:key="game_bar_text_size"
|
||||
android:title="Text Size"
|
||||
android:summary="Adjust the size of overlay text"
|
||||
android:defaultValue="12"
|
||||
android:max="50"
|
||||
android:min="5"
|
||||
settings:units="" />
|
||||
<com.android.gamebar.utils.PartsCustomSeekBarPreference
|
||||
android:key="game_bar_background_alpha"
|
||||
android:title="Background Transparency"
|
||||
android:summary="Adjust the transparency of the background"
|
||||
android:defaultValue="95"
|
||||
android:max="255"
|
||||
android:min="0"
|
||||
settings:units="" />
|
||||
<com.android.gamebar.utils.PartsCustomSeekBarPreference
|
||||
android:key="game_bar_corner_radius"
|
||||
android:title="Overlay Corner Radius"
|
||||
android:summary="Adjust how rounded the overlay corners should be"
|
||||
android:defaultValue="100"
|
||||
android:max="100"
|
||||
android:min="0"
|
||||
settings:units="" />
|
||||
<com.android.gamebar.utils.PartsCustomSeekBarPreference
|
||||
android:key="game_bar_padding"
|
||||
android:title="Overlay Padding"
|
||||
android:summary="Adjust the space around the stats"
|
||||
android:defaultValue="4"
|
||||
android:max="64"
|
||||
android:min="0"
|
||||
settings:units="" />
|
||||
|
||||
<com.android.gamebar.utils.PartsCustomSeekBarPreference
|
||||
android:key="game_bar_item_spacing"
|
||||
android:title="Item Spacing"
|
||||
android:summary="Adjust spacing between overlay lines"
|
||||
android:defaultValue="8"
|
||||
android:max="50"
|
||||
android:min="0"
|
||||
settings:units="" />
|
||||
|
||||
<ListPreference
|
||||
android:key="game_bar_update_interval"
|
||||
android:title="Update Interval"
|
||||
android:summary="Set how often the overlay values update"
|
||||
android:defaultValue="1000"
|
||||
android:entries="@array/fps_overlay_update_interval_entries"
|
||||
android:entryValues="@array/fps_overlay_update_interval_values" />
|
||||
|
||||
<ListPreference
|
||||
android:key="game_bar_title_color"
|
||||
android:title="Stat Title Color"
|
||||
android:summary="Color for 'FPS', 'Temp', 'CPU', etc. text"
|
||||
android:defaultValue="#FFFFFF"
|
||||
android:entries="@array/fps_overlay_color_entries"
|
||||
android:entryValues="@array/fps_overlay_color_values" />
|
||||
|
||||
<ListPreference
|
||||
android:key="game_bar_value_color"
|
||||
android:title="Stat Value Color"
|
||||
android:summary="Color for numeric stats (e.g., '29', '32.0°C')"
|
||||
android:defaultValue="#4CAF50"
|
||||
android:entries="@array/fps_overlay_color_entries"
|
||||
android:entryValues="@array/fps_overlay_color_values" />
|
||||
|
||||
<ListPreference
|
||||
android:key="game_bar_position"
|
||||
android:title="Overlay Position"
|
||||
android:summary="Select the position of the overlay on screen"
|
||||
android:defaultValue="draggable"
|
||||
android:entries="@array/fps_overlay_position_entries"
|
||||
android:entryValues="@array/fps_overlay_position_values" />
|
||||
|
||||
<ListPreference
|
||||
android:key="game_bar_format"
|
||||
android:title="Overlay Format"
|
||||
android:summary="Choose between Full or Minimal display"
|
||||
android:defaultValue="full"
|
||||
android:entries="@array/game_bar_format_entries"
|
||||
android:entryValues="@array/game_bar_format_values" />
|
||||
</com.android.gamebar.ExpandablePreferenceCategory>
|
||||
|
||||
<com.android.gamebar.ExpandablePreferenceCategory
|
||||
android:key="category_split_config"
|
||||
android:title="Split Config"
|
||||
android:dependency="game_bar_enable">
|
||||
|
||||
<ListPreference
|
||||
android:key="game_bar_split_mode"
|
||||
android:title="Split Mode"
|
||||
android:summary="Choose Side-by-Side or Stacked arrangement"
|
||||
android:defaultValue="side_by_side"
|
||||
android:entries="@array/game_bar_split_mode_entries"
|
||||
android:entryValues="@array/game_bar_split_mode_values" />
|
||||
</com.android.gamebar.ExpandablePreferenceCategory>
|
||||
|
||||
<com.android.gamebar.ExpandablePreferenceCategory
|
||||
android:key="category_gesture_controls"
|
||||
android:title="Overlay Gesture Controls"
|
||||
android:dependency="game_bar_enable">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="game_bar_single_tap_toggle"
|
||||
android:title="Enable Single Tap to Toggle"
|
||||
android:summary="Tap once to switch between full and minimal overlay formats"
|
||||
android:defaultValue="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="game_bar_doubletap_capture"
|
||||
android:title="Enable Double Tap to Capture"
|
||||
android:summary="Double-tap overlay to start/stop capture logs"
|
||||
android:defaultValue="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="game_bar_longpress_enable"
|
||||
android:title="Enable Long Press"
|
||||
android:summary="Long-press overlay to access GameBar settings"
|
||||
android:defaultValue="true" />
|
||||
|
||||
<ListPreference
|
||||
android:key="game_bar_longpress_timeout"
|
||||
android:title="Long Press Duration"
|
||||
android:summary="Set the duration required to long-press the overlay"
|
||||
android:defaultValue="500"
|
||||
android:entries="@array/game_bar_longpress_entries"
|
||||
android:entryValues="@array/game_bar_longpress_values"
|
||||
android:dependency="game_bar_longpress_enable" />
|
||||
</com.android.gamebar.ExpandablePreferenceCategory>
|
||||
|
||||
<com.android.gamebar.ExpandablePreferenceCategory
|
||||
android:key="category_per_app"
|
||||
android:title="Per-App GameBar">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="game_bar_auto_enable"
|
||||
android:title="Auto-Enable GameBar for Selected Apps"
|
||||
android:summary="If turned on, selected apps will auto-enable GameBar even if the main switch is off"
|
||||
android:defaultValue="false" />
|
||||
|
||||
<Preference
|
||||
android:key="game_bar_per_app_config"
|
||||
android:title="Configure Apps"
|
||||
android:summary="Choose which apps will auto-enable GameBar" />
|
||||
</com.android.gamebar.ExpandablePreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
8
sepolicy/SEPolicy.mk
Normal file
8
sepolicy/SEPolicy.mk
Normal file
@@ -0,0 +1,8 @@
|
||||
# GameBar sepolicy
|
||||
|
||||
BOARD_VENDOR_SEPOLICY_DIRS += \
|
||||
packages/apps/GameBar/sepolicy/vendor
|
||||
PRODUCT_PRIVATE_SEPOLICY_DIRS += \
|
||||
packages/apps/GameBar/sepolicy/private
|
||||
PRODUCT_PUBLIC_SEPOLICY_DIRS += \
|
||||
packages/apps/GameBar/sepolicy/public
|
||||
40
sepolicy/private/gamebar_app.te
Normal file
40
sepolicy/private/gamebar_app.te
Normal file
@@ -0,0 +1,40 @@
|
||||
app_domain(gamebar_app)
|
||||
binder_use(gamebar_app)
|
||||
|
||||
allow gamebar_app {
|
||||
activity_service
|
||||
activity_task_service
|
||||
app_api_service
|
||||
batterystats_service
|
||||
cameraserver_service
|
||||
content_capture_service
|
||||
device_state_service
|
||||
drmserver_service
|
||||
game_service
|
||||
gpu_service
|
||||
hint_service
|
||||
media_session_service
|
||||
mediaextractor_service
|
||||
mediametrics_service
|
||||
mediaserver_service
|
||||
netstats_service
|
||||
permission_checker_service
|
||||
sensorservice_service
|
||||
statusbar_service
|
||||
thermal_service
|
||||
}:service_manager find;
|
||||
|
||||
allow gamebar_app system_app_data_file:dir create_dir_perms;
|
||||
allow gamebar_app system_app_data_file:{ file lnk_file } create_file_perms;
|
||||
|
||||
binder_call(gamebar_app, gpuservice)
|
||||
|
||||
get_prop(gamebar_app, settingslib_prop)
|
||||
|
||||
# Allow reading thermal sensor data
|
||||
r_dir_file(gamebar_app, sysfs_thermal)
|
||||
allow gamebar_app sysfs_thermal:file { read open };
|
||||
allow gamebar_app sysfs:file { read open };
|
||||
allow gamebar_app sysfs:file { read open getattr ioctl };
|
||||
allow gamebar_app proc_stat:file r_file_perms;
|
||||
allow gamebar_app system_server:binder { call transfer };
|
||||
1
sepolicy/private/property_contexts
Normal file
1
sepolicy/private/property_contexts
Normal file
@@ -0,0 +1 @@
|
||||
settingsdebug.instant.packages u:object_r:settingslib_prop:s0
|
||||
3
sepolicy/private/seapp_contexts
Normal file
3
sepolicy/private/seapp_contexts
Normal file
@@ -0,0 +1,3 @@
|
||||
#GameBar
|
||||
user=system seinfo=platform isPrivApp=true name=com.android.gamebar domain=gamebar_app type=system_app_data_file levelFrom=all
|
||||
|
||||
2
sepolicy/public/gamebar_app.te
Normal file
2
sepolicy/public/gamebar_app.te
Normal file
@@ -0,0 +1,2 @@
|
||||
type gamebar_app, domain;
|
||||
typeattribute gamebar_app mlstrustedsubject;
|
||||
2
sepolicy/public/property.te
Normal file
2
sepolicy/public/property.te
Normal file
@@ -0,0 +1,2 @@
|
||||
# SettingsLib
|
||||
system_public_prop(settingslib_prop)
|
||||
2
sepolicy/vendor/file_contexts
vendored
Normal file
2
sepolicy/vendor/file_contexts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Process and system statistics files
|
||||
/proc/stat u:object_r:proc_stat:s0
|
||||
8
sepolicy/vendor/gamebar_app.te
vendored
Normal file
8
sepolicy/vendor/gamebar_app.te
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
allow gamebar_app vendor_sysfs_graphics:dir search;
|
||||
allow gamebar_app vendor_sysfs_graphics:file rw_file_perms;
|
||||
allow gamebar_app vendor_sysfs_kgsl:dir search;
|
||||
allow gamebar_app vendor_sysfs_kgsl:{ file lnk_file } rw_file_perms;
|
||||
allow gamebar_app vendor_sysfs_battery_supply:dir search;
|
||||
allow gamebar_app vendor_sysfs_battery_supply:file r_file_perms;
|
||||
allow gamebar_app proc_stat:file { read open getattr };
|
||||
allow gamebar_app vendor_sysfs_kgsl_gpuclk:file { read open getattr };
|
||||
220
src/com/android/gamebar/CpuClockGraphView.kt
Normal file
220
src/com/android/gamebar/CpuClockGraphView.kt
Normal file
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import kotlin.math.max
|
||||
|
||||
class CpuClockGraphView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
// Color palette for CPU cores (8 distinct colors)
|
||||
private val coreColors = arrayOf(
|
||||
"#FF5252", // Red
|
||||
"#FF9800", // Orange
|
||||
"#FFEB3B", // Yellow
|
||||
"#4CAF50", // Green
|
||||
"#2196F3", // Blue
|
||||
"#9C27B0", // Purple
|
||||
"#E91E63", // Pink
|
||||
"#00BCD4" // Cyan
|
||||
)
|
||||
|
||||
private val corePaints = mutableListOf<Paint>()
|
||||
private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#30FFFFFF")
|
||||
strokeWidth = 1f
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
|
||||
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FFFFFF")
|
||||
textSize = 24f
|
||||
typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
|
||||
}
|
||||
|
||||
private val legendTextPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FFFFFF")
|
||||
textSize = 20f
|
||||
}
|
||||
|
||||
// Data: Map of coreIndex -> List<Pair<timestamp, clockMhz>>
|
||||
private var clockData: Map<Int, List<Pair<Long, Double>>> = emptyMap()
|
||||
private var maxClock = 3000.0 // Dynamic max
|
||||
private var numCores = 8
|
||||
|
||||
private val padding = 80f
|
||||
private val topPadding = 60f
|
||||
private val bottomPadding = 120f // Extra space for legend
|
||||
|
||||
init {
|
||||
// Initialize paints for each core
|
||||
for (color in coreColors) {
|
||||
corePaints.add(Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
this.color = Color.parseColor(color)
|
||||
strokeWidth = 2.5f
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun setData(data: Map<Int, List<Pair<Long, Double>>>) {
|
||||
this.clockData = data
|
||||
this.numCores = data.keys.size
|
||||
|
||||
// Calculate dynamic max (round up to nearest 500)
|
||||
if (data.isNotEmpty()) {
|
||||
val dataMax = data.values.flatten().maxOfOrNull { it.second } ?: 3000.0
|
||||
maxClock = ((dataMax / 500).toInt() + 1) * 500.0
|
||||
}
|
||||
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
if (clockData.isEmpty()) {
|
||||
drawEmptyState(canvas)
|
||||
return
|
||||
}
|
||||
|
||||
val graphWidth = width - 2 * padding
|
||||
val graphHeight = height - topPadding - bottomPadding
|
||||
|
||||
drawGrid(canvas, graphWidth, graphHeight)
|
||||
drawGraphs(canvas, graphWidth, graphHeight)
|
||||
drawTitle(canvas)
|
||||
drawLegend(canvas)
|
||||
}
|
||||
|
||||
private fun drawEmptyState(canvas: Canvas) {
|
||||
val message = "No CPU clock data available"
|
||||
val textWidth = textPaint.measureText(message)
|
||||
canvas.drawText(
|
||||
message,
|
||||
(width - textWidth) / 2,
|
||||
height / 2f,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
|
||||
private fun drawGrid(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
// Calculate clock steps
|
||||
val step = (maxClock / 4).toInt()
|
||||
val clockSteps = (0..4).map { it * step }
|
||||
|
||||
for (clock in clockSteps) {
|
||||
val y = (topPadding + graphHeight * (1 - clock / maxClock)).toFloat()
|
||||
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, gridPaint)
|
||||
val label = "${clock}MHz"
|
||||
canvas.drawText(label, padding - 75f, y + 8f, textPaint)
|
||||
}
|
||||
|
||||
// Y-axis label
|
||||
canvas.save()
|
||||
canvas.rotate(-90f, 15f, height / 2f)
|
||||
val yLabel = "Clock Speed (MHz)"
|
||||
val yLabelWidth = textPaint.measureText(yLabel)
|
||||
canvas.drawText(yLabel, (width - yLabelWidth) / 2, 30f, textPaint)
|
||||
canvas.restore()
|
||||
|
||||
// Time labels
|
||||
if (clockData.isNotEmpty() && clockData.values.first().isNotEmpty()) {
|
||||
val firstData = clockData.values.first()
|
||||
val startTime = firstData.first().first
|
||||
val endTime = firstData.last().first
|
||||
val duration = endTime - startTime
|
||||
|
||||
canvas.drawText("0s", padding, height - bottomPadding + 25f, textPaint)
|
||||
|
||||
val middleTime = formatDuration(duration / 2)
|
||||
canvas.drawText(middleTime, padding + graphWidth / 2 - 30f, height - bottomPadding + 25f, textPaint)
|
||||
|
||||
val endTimeStr = formatDuration(duration)
|
||||
val endX = padding + graphWidth - textPaint.measureText(endTimeStr)
|
||||
canvas.drawText(endTimeStr, endX, height - bottomPadding + 25f, textPaint)
|
||||
}
|
||||
|
||||
val xLabel = "Time"
|
||||
val xLabelWidth = textPaint.measureText(xLabel)
|
||||
canvas.drawText(xLabel, (width - xLabelWidth) / 2, height - bottomPadding + 55f, textPaint)
|
||||
}
|
||||
|
||||
private fun drawGraphs(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
clockData.forEach { (coreIndex, data) ->
|
||||
if (data.size < 2) return@forEach
|
||||
|
||||
val paint = corePaints[coreIndex % corePaints.size]
|
||||
val startTime = data.first().first
|
||||
val endTime = data.last().first
|
||||
val timeDuration = max(endTime - startTime, 1L)
|
||||
|
||||
val path = Path()
|
||||
|
||||
data.forEachIndexed { index, (timestamp, clock) ->
|
||||
val x = padding + ((timestamp - startTime).toFloat() / timeDuration) * graphWidth
|
||||
val y = (topPadding + graphHeight * (1 - clock / maxClock)).toFloat()
|
||||
|
||||
if (index == 0) {
|
||||
path.moveTo(x, y)
|
||||
} else {
|
||||
path.lineTo(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
canvas.drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawTitle(canvas: Canvas) {
|
||||
val title = "CPU Clock Speed vs Time (Per Core)"
|
||||
val titleWidth = textPaint.measureText(title)
|
||||
canvas.drawText(title, (width - titleWidth) / 2, topPadding - 15f, textPaint)
|
||||
}
|
||||
|
||||
private fun drawLegend(canvas: Canvas) {
|
||||
val legendY = height - bottomPadding + 85f
|
||||
val legendStartX = padding
|
||||
val itemWidth = (width - 2 * padding) / 4 // 4 items per row
|
||||
|
||||
for (i in 0 until numCores) {
|
||||
val col = i % 4
|
||||
val row = i / 4
|
||||
val x = legendStartX + col * itemWidth
|
||||
val y = legendY + row * 30f
|
||||
|
||||
val paint = corePaints[i % corePaints.size]
|
||||
|
||||
// Draw color box
|
||||
canvas.drawRect(x, y - 12f, x + 20f, y, paint)
|
||||
|
||||
// Draw label
|
||||
canvas.drawText("CPU$i", x + 25f, y, legendTextPaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDuration(durationMs: Long): String {
|
||||
val seconds = durationMs / 1000
|
||||
return if (seconds < 60) {
|
||||
"${seconds}s"
|
||||
} else {
|
||||
val minutes = seconds / 60
|
||||
val remainingSeconds = seconds % 60
|
||||
"${minutes}m ${remainingSeconds}s"
|
||||
}
|
||||
}
|
||||
}
|
||||
204
src/com/android/gamebar/CpuGraphView.kt
Normal file
204
src/com/android/gamebar/CpuGraphView.kt
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import kotlin.math.max
|
||||
|
||||
class CpuGraphView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#2196F3") // Blue for CPU
|
||||
strokeWidth = 3f
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
}
|
||||
|
||||
private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
shader = LinearGradient(
|
||||
0f, 0f, 0f, 600f,
|
||||
Color.parseColor("#802196F3"),
|
||||
Color.parseColor("#00000000"),
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#30FFFFFF")
|
||||
strokeWidth = 1f
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
|
||||
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FFFFFF")
|
||||
textSize = 28f
|
||||
typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
|
||||
}
|
||||
|
||||
private val avgLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FF9800")
|
||||
strokeWidth = 2f
|
||||
style = Paint.Style.STROKE
|
||||
pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||
}
|
||||
|
||||
private var cpuData: List<Pair<Long, Double>> = emptyList()
|
||||
private var avgCpu: Double = 0.0
|
||||
private val maxCpu = 100.0
|
||||
private val minCpu = 0.0
|
||||
|
||||
private val padding = 80f
|
||||
private val topPadding = 40f
|
||||
private val bottomPadding = 80f
|
||||
|
||||
fun setData(data: List<Pair<Long, Double>>, avg: Double) {
|
||||
this.cpuData = data
|
||||
this.avgCpu = avg
|
||||
|
||||
post {
|
||||
fillPaint.shader = LinearGradient(
|
||||
0f, topPadding, 0f, height - bottomPadding,
|
||||
Color.parseColor("#802196F3"),
|
||||
Color.parseColor("#102196F3"),
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
if (cpuData.isEmpty()) {
|
||||
drawEmptyState(canvas)
|
||||
return
|
||||
}
|
||||
|
||||
val graphWidth = width - 2 * padding
|
||||
val graphHeight = height - topPadding - bottomPadding
|
||||
|
||||
drawGrid(canvas, graphWidth, graphHeight)
|
||||
drawAverageLine(canvas, graphWidth, graphHeight)
|
||||
drawGraph(canvas, graphWidth, graphHeight)
|
||||
drawTitle(canvas)
|
||||
}
|
||||
|
||||
private fun drawEmptyState(canvas: Canvas) {
|
||||
val message = "No CPU data available"
|
||||
val textWidth = textPaint.measureText(message)
|
||||
canvas.drawText(
|
||||
message,
|
||||
(width - textWidth) / 2,
|
||||
height / 2f,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
|
||||
private fun drawGrid(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
val cpuSteps = listOf(0, 25, 50, 75, 100)
|
||||
|
||||
for (cpu in cpuSteps) {
|
||||
val y = (topPadding + graphHeight * (1 - cpu / maxCpu.toFloat())).toFloat()
|
||||
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, gridPaint)
|
||||
val label = "$cpu%"
|
||||
canvas.drawText(label, padding - 70f, y + 10f, textPaint)
|
||||
}
|
||||
|
||||
// Y-axis label
|
||||
canvas.save()
|
||||
canvas.rotate(-90f, 15f, height / 2f)
|
||||
val yLabel = "CPU Usage (%)"
|
||||
val yLabelWidth = textPaint.measureText(yLabel)
|
||||
canvas.drawText(yLabel, (width - yLabelWidth) / 2, 30f, textPaint)
|
||||
canvas.restore()
|
||||
|
||||
// Time labels on X-axis
|
||||
if (cpuData.isNotEmpty()) {
|
||||
val startTime = cpuData.first().first
|
||||
val endTime = cpuData.last().first
|
||||
val duration = endTime - startTime
|
||||
|
||||
canvas.drawText("0s", padding, height - bottomPadding + 25f, textPaint)
|
||||
|
||||
val middleTime = formatDuration(duration / 2)
|
||||
canvas.drawText(middleTime, padding + graphWidth / 2 - 30f, height - bottomPadding + 25f, textPaint)
|
||||
|
||||
val endTimeStr = formatDuration(duration)
|
||||
val endX = padding + graphWidth - textPaint.measureText(endTimeStr)
|
||||
canvas.drawText(endTimeStr, endX, height - bottomPadding + 25f, textPaint)
|
||||
}
|
||||
|
||||
val xLabel = "Time"
|
||||
val xLabelWidth = textPaint.measureText(xLabel)
|
||||
canvas.drawText(xLabel, (width - xLabelWidth) / 2, height - bottomPadding + 55f, textPaint)
|
||||
}
|
||||
|
||||
private fun drawAverageLine(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
val y = (topPadding + graphHeight * (1 - avgCpu / maxCpu)).toFloat()
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, avgLinePaint)
|
||||
}
|
||||
|
||||
private fun drawGraph(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
if (cpuData.size < 2) return
|
||||
|
||||
val startTime = cpuData.first().first
|
||||
val endTime = cpuData.last().first
|
||||
val timeDuration = max(endTime - startTime, 1L)
|
||||
|
||||
val path = Path()
|
||||
val fillPath = Path()
|
||||
|
||||
cpuData.forEachIndexed { index, (timestamp, cpu) ->
|
||||
val x = padding + ((timestamp - startTime).toFloat() / timeDuration) * graphWidth
|
||||
val y = (topPadding + graphHeight * (1 - cpu / maxCpu)).toFloat()
|
||||
|
||||
if (index == 0) {
|
||||
path.moveTo(x, y)
|
||||
fillPath.moveTo(x, height - bottomPadding)
|
||||
fillPath.lineTo(x, y)
|
||||
} else {
|
||||
path.lineTo(x, y)
|
||||
fillPath.lineTo(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
fillPath.lineTo(padding + graphWidth, height - bottomPadding)
|
||||
fillPath.close()
|
||||
|
||||
canvas.drawPath(fillPath, fillPaint)
|
||||
canvas.drawPath(path, linePaint)
|
||||
}
|
||||
|
||||
private fun drawTitle(canvas: Canvas) {
|
||||
val title = "CPU Usage vs Time"
|
||||
val titleWidth = textPaint.measureText(title)
|
||||
canvas.drawText(title, (width - titleWidth) / 2, topPadding - 10f, textPaint)
|
||||
}
|
||||
|
||||
private fun formatDuration(durationMs: Long): String {
|
||||
val seconds = durationMs / 1000
|
||||
return if (seconds < 60) {
|
||||
"${seconds}s"
|
||||
} else {
|
||||
val minutes = seconds / 60
|
||||
val remainingSeconds = seconds % 60
|
||||
"${minutes}m ${remainingSeconds}s"
|
||||
}
|
||||
}
|
||||
}
|
||||
212
src/com/android/gamebar/CpuTempGraphView.kt
Normal file
212
src/com/android/gamebar/CpuTempGraphView.kt
Normal file
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import kotlin.math.max
|
||||
|
||||
class CpuTempGraphView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FF5722") // Orange/Red for temperature
|
||||
strokeWidth = 3f
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
}
|
||||
|
||||
private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
shader = LinearGradient(
|
||||
0f, 0f, 0f, 600f,
|
||||
Color.parseColor("#80FF5722"),
|
||||
Color.parseColor("#00000000"),
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#30FFFFFF")
|
||||
strokeWidth = 1f
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
|
||||
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FFFFFF")
|
||||
textSize = 28f
|
||||
typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
|
||||
}
|
||||
|
||||
private val avgLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FF9800")
|
||||
strokeWidth = 2f
|
||||
style = Paint.Style.STROKE
|
||||
pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||
}
|
||||
|
||||
private var tempData: List<Pair<Long, Double>> = emptyList()
|
||||
private var avgTemp: Double = 0.0
|
||||
private var maxTemp = 100.0 // Dynamic max based on data
|
||||
private var minTemp = 0.0
|
||||
|
||||
private val padding = 80f
|
||||
private val topPadding = 40f
|
||||
private val bottomPadding = 80f
|
||||
|
||||
fun setData(data: List<Pair<Long, Double>>, avg: Double) {
|
||||
this.tempData = data
|
||||
this.avgTemp = avg
|
||||
|
||||
// Calculate dynamic max (round up to nearest 10)
|
||||
if (data.isNotEmpty()) {
|
||||
val dataMax = data.maxOf { it.second }
|
||||
maxTemp = ((dataMax / 10).toInt() + 1) * 10.0
|
||||
}
|
||||
|
||||
post {
|
||||
fillPaint.shader = LinearGradient(
|
||||
0f, topPadding, 0f, height - bottomPadding,
|
||||
Color.parseColor("#80FF5722"),
|
||||
Color.parseColor("#10FF5722"),
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
if (tempData.isEmpty()) {
|
||||
drawEmptyState(canvas)
|
||||
return
|
||||
}
|
||||
|
||||
val graphWidth = width - 2 * padding
|
||||
val graphHeight = height - topPadding - bottomPadding
|
||||
|
||||
drawGrid(canvas, graphWidth, graphHeight)
|
||||
drawAverageLine(canvas, graphWidth, graphHeight)
|
||||
drawGraph(canvas, graphWidth, graphHeight)
|
||||
drawTitle(canvas)
|
||||
}
|
||||
|
||||
private fun drawEmptyState(canvas: Canvas) {
|
||||
val message = "No temperature data available"
|
||||
val textWidth = textPaint.measureText(message)
|
||||
canvas.drawText(
|
||||
message,
|
||||
(width - textWidth) / 2,
|
||||
height / 2f,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
|
||||
private fun drawGrid(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
// Calculate temp steps based on maxTemp
|
||||
val step = (maxTemp / 4).toInt()
|
||||
val tempSteps = (0..4).map { it * step }
|
||||
|
||||
for (temp in tempSteps) {
|
||||
val y = (topPadding + graphHeight * (1 - temp / maxTemp)).toFloat()
|
||||
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, gridPaint)
|
||||
val label = "${temp}°C"
|
||||
canvas.drawText(label, padding - 70f, y + 10f, textPaint)
|
||||
}
|
||||
|
||||
// Y-axis label
|
||||
canvas.save()
|
||||
canvas.rotate(-90f, 15f, height / 2f)
|
||||
val yLabel = "Temperature (°C)"
|
||||
val yLabelWidth = textPaint.measureText(yLabel)
|
||||
canvas.drawText(yLabel, (width - yLabelWidth) / 2, 30f, textPaint)
|
||||
canvas.restore()
|
||||
|
||||
// Time labels
|
||||
if (tempData.isNotEmpty()) {
|
||||
val startTime = tempData.first().first
|
||||
val endTime = tempData.last().first
|
||||
val duration = endTime - startTime
|
||||
|
||||
canvas.drawText("0s", padding, height - bottomPadding + 25f, textPaint)
|
||||
|
||||
val middleTime = formatDuration(duration / 2)
|
||||
canvas.drawText(middleTime, padding + graphWidth / 2 - 30f, height - bottomPadding + 25f, textPaint)
|
||||
|
||||
val endTimeStr = formatDuration(duration)
|
||||
val endX = padding + graphWidth - textPaint.measureText(endTimeStr)
|
||||
canvas.drawText(endTimeStr, endX, height - bottomPadding + 25f, textPaint)
|
||||
}
|
||||
|
||||
val xLabel = "Time"
|
||||
val xLabelWidth = textPaint.measureText(xLabel)
|
||||
canvas.drawText(xLabel, (width - xLabelWidth) / 2, height - bottomPadding + 55f, textPaint)
|
||||
}
|
||||
|
||||
private fun drawAverageLine(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
val y = (topPadding + graphHeight * (1 - avgTemp / maxTemp)).toFloat()
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, avgLinePaint)
|
||||
}
|
||||
|
||||
private fun drawGraph(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
if (tempData.size < 2) return
|
||||
|
||||
val startTime = tempData.first().first
|
||||
val endTime = tempData.last().first
|
||||
val timeDuration = max(endTime - startTime, 1L)
|
||||
|
||||
val path = Path()
|
||||
val fillPath = Path()
|
||||
|
||||
tempData.forEachIndexed { index, (timestamp, temp) ->
|
||||
val x = padding + ((timestamp - startTime).toFloat() / timeDuration) * graphWidth
|
||||
val y = (topPadding + graphHeight * (1 - temp / maxTemp)).toFloat()
|
||||
|
||||
if (index == 0) {
|
||||
path.moveTo(x, y)
|
||||
fillPath.moveTo(x, height - bottomPadding)
|
||||
fillPath.lineTo(x, y)
|
||||
} else {
|
||||
path.lineTo(x, y)
|
||||
fillPath.lineTo(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
fillPath.lineTo(padding + graphWidth, height - bottomPadding)
|
||||
fillPath.close()
|
||||
|
||||
canvas.drawPath(fillPath, fillPaint)
|
||||
canvas.drawPath(path, linePaint)
|
||||
}
|
||||
|
||||
private fun drawTitle(canvas: Canvas) {
|
||||
val title = "CPU Temperature vs Time"
|
||||
val titleWidth = textPaint.measureText(title)
|
||||
canvas.drawText(title, (width - titleWidth) / 2, topPadding - 10f, textPaint)
|
||||
}
|
||||
|
||||
private fun formatDuration(durationMs: Long): String {
|
||||
val seconds = durationMs / 1000
|
||||
return if (seconds < 60) {
|
||||
"${seconds}s"
|
||||
} else {
|
||||
val minutes = seconds / 60
|
||||
val remainingSeconds = seconds % 60
|
||||
"${minutes}m ${remainingSeconds}s"
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src/com/android/gamebar/ExpandablePreferenceCategory.kt
Normal file
94
src/com/android/gamebar/ExpandablePreferenceCategory.kt
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import com.android.gamebar.R
|
||||
|
||||
class ExpandablePreferenceCategory @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = androidx.preference.R.attr.preferenceCategoryStyle,
|
||||
defStyleRes: Int = 0
|
||||
) : PreferenceCategory(context, attrs, defStyleAttr, defStyleRes) {
|
||||
|
||||
private var isExpanded = false
|
||||
|
||||
init {
|
||||
layoutResource = R.layout.preference_category_expandable
|
||||
isIconSpaceReserved = false
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
|
||||
val expandIcon = holder.findViewById(R.id.expand_icon) as? ImageView
|
||||
val titleView = holder.findViewById(android.R.id.title) as? TextView
|
||||
|
||||
// Update icon rotation based on expanded state with animation
|
||||
expandIcon?.animate()
|
||||
?.rotation(if (isExpanded) 90f else 0f)
|
||||
?.setDuration(200)
|
||||
?.start()
|
||||
|
||||
// Make the entire view clickable
|
||||
holder.itemView.isClickable = true
|
||||
holder.itemView.isFocusable = true
|
||||
holder.itemView.setOnClickListener {
|
||||
toggleExpanded()
|
||||
}
|
||||
|
||||
// Update title style
|
||||
titleView?.let {
|
||||
it.textSize = 14f
|
||||
it.setTextColor(context.getColor(android.R.color.white))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttached() {
|
||||
super.onAttached()
|
||||
|
||||
// Initially collapse all children
|
||||
if (!isExpanded) {
|
||||
syncChildrenVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleExpanded() {
|
||||
isExpanded = !isExpanded
|
||||
syncChildrenVisibility()
|
||||
notifyChanged()
|
||||
}
|
||||
|
||||
private fun syncChildrenVisibility() {
|
||||
for (i in 0 until preferenceCount) {
|
||||
getPreference(i)?.isVisible = isExpanded
|
||||
}
|
||||
}
|
||||
|
||||
fun setExpanded(expanded: Boolean) {
|
||||
if (isExpanded != expanded) {
|
||||
isExpanded = expanded
|
||||
syncChildrenVisibility()
|
||||
notifyChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun addPreference(preference: Preference): Boolean {
|
||||
val result = super.addPreference(preference)
|
||||
if (result && !isExpanded) {
|
||||
preference.isVisible = false
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
104
src/com/android/gamebar/ForegroundAppDetector.kt
Normal file
104
src/com/android/gamebar/ForegroundAppDetector.kt
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import java.lang.reflect.Method
|
||||
|
||||
object ForegroundAppDetector {
|
||||
|
||||
private const val TAG = "ForegroundAppDetector"
|
||||
private var lastKnownPackage = "Unknown"
|
||||
private var lastUpdateTime = 0L
|
||||
private const val CACHE_TIMEOUT = 500L // Reduce cache timeout
|
||||
|
||||
// Simple reflection caching
|
||||
private var reflectionSetupFailed = false
|
||||
|
||||
fun getForegroundPackageName(context: Context): String {
|
||||
// Use cached result if still valid
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastUpdateTime < CACHE_TIMEOUT && lastKnownPackage != "Unknown") {
|
||||
return lastKnownPackage
|
||||
}
|
||||
|
||||
val pkg = tryGetRunningTasks(context)
|
||||
if (pkg != null) {
|
||||
lastKnownPackage = pkg
|
||||
lastUpdateTime = currentTime
|
||||
return pkg
|
||||
}
|
||||
|
||||
if (!reflectionSetupFailed) {
|
||||
val reflectionPkg = tryReflectActivityTaskManager()
|
||||
if (reflectionPkg != null) {
|
||||
lastKnownPackage = reflectionPkg
|
||||
lastUpdateTime = currentTime
|
||||
return reflectionPkg
|
||||
}
|
||||
}
|
||||
|
||||
// Return cached value if available, otherwise "Unknown"
|
||||
return lastKnownPackage
|
||||
}
|
||||
|
||||
private fun tryGetRunningTasks(context: Context): String? {
|
||||
try {
|
||||
if (context.checkSelfPermission("android.permission.GET_TASKS") == PackageManager.PERMISSION_GRANTED) {
|
||||
val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
val tasks = am.getRunningTasks(1)
|
||||
if (tasks.isNotEmpty()) {
|
||||
val top = tasks[0]
|
||||
top.topActivity?.let {
|
||||
return it.packageName
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "GET_TASKS permission not granted to this system app?")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "tryGetRunningTasks error: ", e)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun tryReflectActivityTaskManager(): String? {
|
||||
try {
|
||||
if (reflectionSetupFailed) {
|
||||
return null
|
||||
}
|
||||
|
||||
val atmClass = Class.forName("android.app.ActivityTaskManager")
|
||||
val getServiceMethod = atmClass.getDeclaredMethod("getService")
|
||||
getServiceMethod.isAccessible = true
|
||||
val atmService = getServiceMethod.invoke(null)
|
||||
val getTasksMethod = atmService.javaClass.getMethod("getTasks", Int::class.java)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val taskList = getTasksMethod.invoke(atmService, 1) as? List<*>
|
||||
if (!taskList.isNullOrEmpty()) {
|
||||
val firstTask = taskList[0]
|
||||
val rtiClass = firstTask!!.javaClass
|
||||
val getTopActivityMethod = rtiClass.getDeclaredMethod("getTopActivity")
|
||||
val compName = getTopActivityMethod.invoke(firstTask)
|
||||
if (compName != null) {
|
||||
val getPackageNameMethod = compName.javaClass.getMethod("getPackageName")
|
||||
val pkgName = getPackageNameMethod.invoke(compName) as String
|
||||
return pkgName
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "tryReflectActivityTaskManager error: ", e)
|
||||
reflectionSetupFailed = true // Disable reflection on error
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
318
src/com/android/gamebar/FpsGraphView.kt
Normal file
318
src/com/android/gamebar/FpsGraphView.kt
Normal file
@@ -0,0 +1,318 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class FpsGraphView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#4CAF50")
|
||||
strokeWidth = 3f
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
}
|
||||
|
||||
private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
shader = LinearGradient(
|
||||
0f, 0f, 0f, 800f,
|
||||
Color.parseColor("#804CAF50"),
|
||||
Color.parseColor("#00000000"),
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#30FFFFFF")
|
||||
strokeWidth = 1f
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
|
||||
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FFFFFF")
|
||||
textSize = 28f
|
||||
typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
|
||||
}
|
||||
|
||||
private val avgLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FF9800")
|
||||
strokeWidth = 2f
|
||||
style = Paint.Style.STROKE
|
||||
pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||
}
|
||||
|
||||
private val lowLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#F44336")
|
||||
strokeWidth = 2f
|
||||
style = Paint.Style.STROKE
|
||||
pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||
}
|
||||
|
||||
private var fpsData: List<Pair<Long, Double>> = emptyList()
|
||||
private var avgFps: Double = 0.0
|
||||
private var fps1PercentLow: Double = 0.0
|
||||
private val maxFps = 144.0 // Absolute max for display
|
||||
private val functionalMaxFps = 120.0 // Treat 120 as 100%
|
||||
private val minFps = 0.0
|
||||
|
||||
private val padding = 80f
|
||||
private val topPadding = 40f
|
||||
private val bottomPadding = 80f // Increased for axis label
|
||||
|
||||
fun setData(data: List<Pair<Long, Double>>, avg: Double, low1Percent: Double) {
|
||||
this.fpsData = data
|
||||
this.avgFps = avg
|
||||
this.fps1PercentLow = low1Percent
|
||||
|
||||
// Update gradient shader with actual view height
|
||||
post {
|
||||
fillPaint.shader = LinearGradient(
|
||||
0f, topPadding, 0f, height - bottomPadding,
|
||||
Color.parseColor("#804CAF50"),
|
||||
Color.parseColor("#104CAF50"),
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
if (fpsData.isEmpty()) {
|
||||
drawEmptyState(canvas)
|
||||
return
|
||||
}
|
||||
|
||||
val graphWidth = width - 2 * padding
|
||||
val graphHeight = height - topPadding - bottomPadding
|
||||
|
||||
// Draw grid and labels
|
||||
drawGrid(canvas, graphWidth, graphHeight)
|
||||
|
||||
// Draw average line
|
||||
drawAverageLine(canvas, graphWidth, graphHeight)
|
||||
|
||||
// Draw 1% low line
|
||||
draw1PercentLowLine(canvas, graphWidth, graphHeight)
|
||||
|
||||
// Draw FPS graph
|
||||
drawGraph(canvas, graphWidth, graphHeight)
|
||||
|
||||
// Draw legend
|
||||
drawLegend(canvas)
|
||||
}
|
||||
|
||||
private fun drawEmptyState(canvas: Canvas) {
|
||||
val message = "No data to display"
|
||||
val textWidth = textPaint.measureText(message)
|
||||
canvas.drawText(
|
||||
message,
|
||||
(width - textWidth) / 2,
|
||||
height / 2f,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
|
||||
private fun drawGrid(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
// Draw horizontal grid lines (FPS) - using 120 as functional max (100%)
|
||||
val fpsSteps = listOf(0, 30, 60, 90, 120, 144)
|
||||
|
||||
for (fps in fpsSteps) {
|
||||
// Map FPS values so 120 appears at 100% height
|
||||
val normalizedFps = if (fps <= 120) {
|
||||
fps.toFloat()
|
||||
} else {
|
||||
// Squeeze 120-144 into the remaining space
|
||||
120f + (fps - 120) * 0.2f
|
||||
}
|
||||
val y = (topPadding + graphHeight * (1 - normalizedFps / (functionalMaxFps.toFloat() + 4.8f))).toFloat()
|
||||
|
||||
// Grid line
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, gridPaint)
|
||||
|
||||
// Label
|
||||
val label = "$fps"
|
||||
canvas.drawText(label, padding - 60f, y + 10f, textPaint)
|
||||
}
|
||||
|
||||
// Draw Y-axis label (far left, separate from numbers)
|
||||
canvas.save()
|
||||
canvas.rotate(-90f, 15f, height / 2f)
|
||||
val yLabel = "FPS"
|
||||
val yLabelWidth = textPaint.measureText(yLabel)
|
||||
canvas.drawText(yLabel, (width - yLabelWidth) / 2, 30f, textPaint) // Position at x=30 (far left)
|
||||
canvas.restore()
|
||||
|
||||
// Draw time labels on X-axis (higher up - first line)
|
||||
if (fpsData.isNotEmpty()) {
|
||||
val startTime = fpsData.first().first
|
||||
val endTime = fpsData.last().first
|
||||
val duration = endTime - startTime
|
||||
|
||||
// Draw start time
|
||||
canvas.drawText("0s", padding, height - bottomPadding + 25f, textPaint)
|
||||
|
||||
// Draw middle time
|
||||
val middleTime = formatDuration(duration / 2)
|
||||
val middleX = padding + graphWidth / 2
|
||||
canvas.drawText(middleTime, middleX - 30f, height - bottomPadding + 25f, textPaint)
|
||||
|
||||
// Draw end time
|
||||
val endTimeStr = formatDuration(duration)
|
||||
val endX = padding + graphWidth - textPaint.measureText(endTimeStr)
|
||||
canvas.drawText(endTimeStr, endX, height - bottomPadding + 25f, textPaint)
|
||||
}
|
||||
|
||||
// Draw X-axis label (second line - below time values)
|
||||
val xLabel = "Time"
|
||||
val xLabelWidth = textPaint.measureText(xLabel)
|
||||
canvas.drawText(xLabel, (width - xLabelWidth) / 2, height - bottomPadding + 55f, textPaint)
|
||||
}
|
||||
|
||||
private fun drawGraph(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
if (fpsData.size < 2) return
|
||||
|
||||
val startTime = fpsData.first().first
|
||||
val endTime = fpsData.last().first
|
||||
val timeDuration = max(endTime - startTime, 1L)
|
||||
|
||||
val path = Path()
|
||||
val fillPath = Path()
|
||||
|
||||
fpsData.forEachIndexed { index, (timestamp, fps) ->
|
||||
val x = padding + ((timestamp - startTime).toFloat() / timeDuration) * graphWidth
|
||||
|
||||
// Normalize FPS using 120 as functional max
|
||||
val normalizedFps = max(minFps, min(maxFps, fps))
|
||||
val mappedFps = if (normalizedFps <= 120.0) {
|
||||
normalizedFps.toFloat()
|
||||
} else {
|
||||
// Squeeze 120-144 range into remaining space
|
||||
120f + (normalizedFps.toFloat() - 120f) * 0.2f
|
||||
}
|
||||
val y = (topPadding + graphHeight * (1 - mappedFps / (functionalMaxFps.toFloat() + 4.8f))).toFloat()
|
||||
|
||||
if (index == 0) {
|
||||
path.moveTo(x, y)
|
||||
fillPath.moveTo(x, height - bottomPadding)
|
||||
fillPath.lineTo(x, y)
|
||||
} else {
|
||||
path.lineTo(x, y)
|
||||
fillPath.lineTo(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
// Complete fill path
|
||||
val lastX = padding + graphWidth
|
||||
fillPath.lineTo(lastX, height - bottomPadding)
|
||||
fillPath.close()
|
||||
|
||||
// Draw fill
|
||||
canvas.drawPath(fillPath, fillPaint)
|
||||
|
||||
// Draw line
|
||||
canvas.drawPath(path, linePaint)
|
||||
}
|
||||
|
||||
private fun drawAverageLine(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
// Map average line position
|
||||
val normalizedAvg = max(minFps, min(maxFps, avgFps))
|
||||
val mappedAvg = if (normalizedAvg <= 120.0) {
|
||||
normalizedAvg.toFloat()
|
||||
} else {
|
||||
120f + (normalizedAvg.toFloat() - 120f) * 0.2f
|
||||
}
|
||||
val y = (topPadding + graphHeight * (1 - mappedAvg / (functionalMaxFps.toFloat() + 4.8f))).toFloat()
|
||||
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, avgLinePaint)
|
||||
}
|
||||
|
||||
private fun draw1PercentLowLine(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
// Map 1% low line position
|
||||
val normalizedLow = max(minFps, min(maxFps, fps1PercentLow))
|
||||
val mappedLow = if (normalizedLow <= 120.0) {
|
||||
normalizedLow.toFloat()
|
||||
} else {
|
||||
120f + (normalizedLow.toFloat() - 120f) * 0.2f
|
||||
}
|
||||
val y = (topPadding + graphHeight * (1 - mappedLow / (functionalMaxFps.toFloat() + 4.8f))).toFloat()
|
||||
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, lowLinePaint)
|
||||
}
|
||||
|
||||
private fun drawLegend(canvas: Canvas) {
|
||||
val legendX = padding
|
||||
val legendY = 20f
|
||||
val lineLength = 40f
|
||||
val spacing = 150f
|
||||
|
||||
// FPS line
|
||||
canvas.drawLine(legendX, legendY, legendX + lineLength, legendY, linePaint)
|
||||
canvas.drawText("FPS", legendX + lineLength + 10f, legendY + 8f, textPaint.apply { textSize = 24f })
|
||||
|
||||
// Average line
|
||||
canvas.drawLine(legendX + spacing, legendY, legendX + spacing + lineLength, legendY, avgLinePaint)
|
||||
canvas.drawText("Avg", legendX + spacing + lineLength + 10f, legendY + 8f, textPaint)
|
||||
|
||||
// 1% Low line
|
||||
canvas.drawLine(legendX + spacing * 2, legendY, legendX + spacing * 2 + lineLength, legendY, lowLinePaint)
|
||||
canvas.drawText("1% Low", legendX + spacing * 2 + lineLength + 10f, legendY + 8f, textPaint)
|
||||
|
||||
// Reset text size
|
||||
textPaint.textSize = 28f
|
||||
}
|
||||
|
||||
private fun formatDuration(durationMs: Long): String {
|
||||
val seconds = durationMs / 1000
|
||||
val minutes = seconds / 60
|
||||
val hours = minutes / 60
|
||||
|
||||
return when {
|
||||
hours > 0 -> String.format("%dh%dm", hours, minutes % 60)
|
||||
minutes > 0 -> String.format("%dm%ds", minutes, seconds % 60)
|
||||
else -> String.format("%ds", seconds)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val desiredWidth = 800
|
||||
val desiredHeight = 600
|
||||
|
||||
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
|
||||
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
|
||||
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
|
||||
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
|
||||
|
||||
val width = when (widthMode) {
|
||||
MeasureSpec.EXACTLY -> widthSize
|
||||
MeasureSpec.AT_MOST -> min(desiredWidth, widthSize)
|
||||
else -> desiredWidth
|
||||
}
|
||||
|
||||
val height = when (heightMode) {
|
||||
MeasureSpec.EXACTLY -> heightSize
|
||||
MeasureSpec.AT_MOST -> min(desiredHeight, heightSize)
|
||||
else -> desiredHeight
|
||||
}
|
||||
|
||||
setMeasuredDimension(width, height)
|
||||
}
|
||||
}
|
||||
275
src/com/android/gamebar/FrameTimeGraphView.kt
Normal file
275
src/com/android/gamebar/FrameTimeGraphView.kt
Normal file
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class FrameTimeGraphView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FFC107") // Amber for frame time
|
||||
strokeWidth = 3f
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
}
|
||||
|
||||
private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
shader = LinearGradient(
|
||||
0f, 0f, 0f, 800f,
|
||||
Color.parseColor("#80FFC107"),
|
||||
Color.parseColor("#00000000"),
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#30FFFFFF")
|
||||
strokeWidth = 1f
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
|
||||
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FFFFFF")
|
||||
textSize = 28f
|
||||
typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
|
||||
}
|
||||
|
||||
private val avgLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FF9800")
|
||||
strokeWidth = 2f
|
||||
style = Paint.Style.STROKE
|
||||
pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||
}
|
||||
|
||||
private val targetLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#4CAF50") // Green for 60fps target (16.67ms)
|
||||
strokeWidth = 2f
|
||||
style = Paint.Style.STROKE
|
||||
pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||
}
|
||||
|
||||
private var frameTimeData: List<Pair<Long, Double>> = emptyList()
|
||||
private var avgFrameTime: Double = 0.0
|
||||
private var maxFrameTime = 50.0 // Dynamic max
|
||||
private var minFrameTime = 0.0
|
||||
|
||||
private val padding = 80f
|
||||
private val topPadding = 40f
|
||||
private val bottomPadding = 80f
|
||||
|
||||
fun setData(data: List<Pair<Long, Double>>, avg: Double) {
|
||||
this.frameTimeData = data
|
||||
this.avgFrameTime = avg
|
||||
|
||||
// Calculate dynamic max (round up to nearest 10)
|
||||
if (data.isNotEmpty()) {
|
||||
val dataMax = data.maxOf { it.second }
|
||||
maxFrameTime = max(((dataMax / 10).toInt() + 1) * 10.0, 50.0)
|
||||
}
|
||||
|
||||
post {
|
||||
fillPaint.shader = LinearGradient(
|
||||
0f, topPadding, 0f, height - bottomPadding,
|
||||
Color.parseColor("#80FFC107"),
|
||||
Color.parseColor("#10FFC107"),
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
if (frameTimeData.isEmpty()) {
|
||||
drawEmptyState(canvas)
|
||||
return
|
||||
}
|
||||
|
||||
val graphWidth = width - 2 * padding
|
||||
val graphHeight = height - topPadding - bottomPadding
|
||||
|
||||
drawGrid(canvas, graphWidth, graphHeight)
|
||||
drawTargetLine(canvas, graphWidth, graphHeight) // 60fps target line
|
||||
drawAverageLine(canvas, graphWidth, graphHeight)
|
||||
drawGraph(canvas, graphWidth, graphHeight)
|
||||
drawLegend(canvas)
|
||||
}
|
||||
|
||||
private fun drawEmptyState(canvas: Canvas) {
|
||||
val message = "No frame time data available"
|
||||
val textWidth = textPaint.measureText(message)
|
||||
canvas.drawText(
|
||||
message,
|
||||
(width - textWidth) / 2,
|
||||
height / 2f,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
|
||||
private fun drawGrid(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
// Calculate frame time steps
|
||||
val step = (maxFrameTime / 4).toInt()
|
||||
val frameTimeSteps = (0..4).map { it * step }
|
||||
|
||||
for (frameTime in frameTimeSteps) {
|
||||
val y = (topPadding + graphHeight * (1 - frameTime / maxFrameTime)).toFloat()
|
||||
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, gridPaint)
|
||||
val label = "${frameTime}ms"
|
||||
canvas.drawText(label, padding - 70f, y + 10f, textPaint)
|
||||
}
|
||||
|
||||
// Y-axis label
|
||||
canvas.save()
|
||||
canvas.rotate(-90f, 15f, height / 2f)
|
||||
val yLabel = "Frame Time (ms)"
|
||||
val yLabelWidth = textPaint.measureText(yLabel)
|
||||
canvas.drawText(yLabel, (width - yLabelWidth) / 2, 30f, textPaint)
|
||||
canvas.restore()
|
||||
|
||||
// Time labels on X-axis
|
||||
if (frameTimeData.isNotEmpty()) {
|
||||
val startTime = frameTimeData.first().first
|
||||
val endTime = frameTimeData.last().first
|
||||
val duration = endTime - startTime
|
||||
|
||||
canvas.drawText("0s", padding, height - bottomPadding + 25f, textPaint)
|
||||
|
||||
val middleTime = formatDuration(duration / 2)
|
||||
val middleX = padding + graphWidth / 2
|
||||
canvas.drawText(middleTime, middleX - 30f, height - bottomPadding + 25f, textPaint)
|
||||
|
||||
val endTimeStr = formatDuration(duration)
|
||||
val endX = padding + graphWidth - textPaint.measureText(endTimeStr)
|
||||
canvas.drawText(endTimeStr, endX, height - bottomPadding + 25f, textPaint)
|
||||
}
|
||||
|
||||
val xLabel = "Time"
|
||||
val xLabelWidth = textPaint.measureText(xLabel)
|
||||
canvas.drawText(xLabel, (width - xLabelWidth) / 2, height - bottomPadding + 55f, textPaint)
|
||||
}
|
||||
|
||||
private fun drawTargetLine(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
// 16.67ms = 60fps target
|
||||
val targetFrameTime = 16.67
|
||||
if (targetFrameTime <= maxFrameTime) {
|
||||
val y = (topPadding + graphHeight * (1 - targetFrameTime / maxFrameTime)).toFloat()
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, targetLinePaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawAverageLine(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
val y = (topPadding + graphHeight * (1 - avgFrameTime / maxFrameTime)).toFloat()
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, avgLinePaint)
|
||||
}
|
||||
|
||||
private fun drawGraph(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
if (frameTimeData.size < 2) return
|
||||
|
||||
val startTime = frameTimeData.first().first
|
||||
val endTime = frameTimeData.last().first
|
||||
val timeDuration = max(endTime - startTime, 1L)
|
||||
|
||||
val path = Path()
|
||||
val fillPath = Path()
|
||||
|
||||
frameTimeData.forEachIndexed { index, (timestamp, frameTime) ->
|
||||
val x = padding + ((timestamp - startTime).toFloat() / timeDuration) * graphWidth
|
||||
|
||||
val normalizedFrameTime = max(minFrameTime, min(maxFrameTime, frameTime))
|
||||
val y = (topPadding + graphHeight * (1 - normalizedFrameTime / maxFrameTime)).toFloat()
|
||||
|
||||
if (index == 0) {
|
||||
path.moveTo(x, y)
|
||||
fillPath.moveTo(x, height - bottomPadding)
|
||||
fillPath.lineTo(x, y)
|
||||
} else {
|
||||
path.lineTo(x, y)
|
||||
fillPath.lineTo(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
val lastX = padding + graphWidth
|
||||
fillPath.lineTo(lastX, height - bottomPadding)
|
||||
fillPath.close()
|
||||
|
||||
canvas.drawPath(fillPath, fillPaint)
|
||||
canvas.drawPath(path, linePaint)
|
||||
}
|
||||
|
||||
private fun drawLegend(canvas: Canvas) {
|
||||
val legendX = padding
|
||||
val legendY = 20f
|
||||
val lineLength = 40f
|
||||
val spacing = 150f
|
||||
|
||||
// Frame Time line
|
||||
canvas.drawLine(legendX, legendY, legendX + lineLength, legendY, linePaint)
|
||||
canvas.drawText("Frame Time", legendX + lineLength + 10f, legendY + 8f, textPaint.apply { textSize = 24f })
|
||||
|
||||
// Average line
|
||||
canvas.drawLine(legendX + spacing * 1.5f, legendY, legendX + spacing * 1.5f + lineLength, legendY, avgLinePaint)
|
||||
canvas.drawText("Avg", legendX + spacing * 1.5f + lineLength + 10f, legendY + 8f, textPaint)
|
||||
|
||||
// 60fps target line
|
||||
canvas.drawLine(legendX + spacing * 2.5f, legendY, legendX + spacing * 2.5f + lineLength, legendY, targetLinePaint)
|
||||
canvas.drawText("60fps", legendX + spacing * 2.5f + lineLength + 10f, legendY + 8f, textPaint)
|
||||
|
||||
// Reset text size
|
||||
textPaint.textSize = 28f
|
||||
}
|
||||
|
||||
private fun formatDuration(durationMs: Long): String {
|
||||
val seconds = durationMs / 1000
|
||||
val minutes = seconds / 60
|
||||
val hours = minutes / 60
|
||||
|
||||
return when {
|
||||
hours > 0 -> String.format("%dh%dm", hours, minutes % 60)
|
||||
minutes > 0 -> String.format("%dm%ds", minutes, seconds % 60)
|
||||
else -> String.format("%ds", seconds)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val desiredWidth = 800
|
||||
val desiredHeight = 600
|
||||
|
||||
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
|
||||
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
|
||||
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
|
||||
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
|
||||
|
||||
val width = when (widthMode) {
|
||||
MeasureSpec.EXACTLY -> widthSize
|
||||
MeasureSpec.AT_MOST -> min(desiredWidth, widthSize)
|
||||
else -> desiredWidth
|
||||
}
|
||||
|
||||
val height = when (heightMode) {
|
||||
MeasureSpec.EXACTLY -> heightSize
|
||||
MeasureSpec.AT_MOST -> min(desiredHeight, heightSize)
|
||||
else -> desiredHeight
|
||||
}
|
||||
|
||||
setMeasuredDimension(width, height)
|
||||
}
|
||||
}
|
||||
981
src/com/android/gamebar/GameBar.kt
Normal file
981
src/com/android/gamebar/GameBar.kt
Normal file
@@ -0,0 +1,981 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
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 com.android.gamebar.R
|
||||
import java.io.BufferedReader
|
||||
import java.io.FileReader
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class GameBar private constructor(context: Context) {
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var sInstance: GameBar? = null
|
||||
|
||||
@JvmStatic
|
||||
@Synchronized
|
||||
fun getInstance(context: Context): GameBar {
|
||||
return sInstance ?: synchronized(this) {
|
||||
sInstance ?: GameBar(context.applicationContext).also { sInstance = it }
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Synchronized
|
||||
fun destroyInstance() {
|
||||
sInstance?.cleanup()
|
||||
sInstance = null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isInstanceCreated(): Boolean {
|
||||
return sInstance != null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isShowing(): Boolean {
|
||||
return sInstance?.isShowing == true
|
||||
}
|
||||
|
||||
private const val FPS_PATH = "/sys/class/drm/sde-crtc-0/measured_fps"
|
||||
private const val BATTERY_TEMP_PATH = "/sys/class/power_supply/battery/temp"
|
||||
private const val PREF_KEY_X = "game_bar_x"
|
||||
private const val PREF_KEY_Y = "game_bar_y"
|
||||
private const val TOUCH_SLOP = 30f
|
||||
}
|
||||
|
||||
private val context: Context = context.applicationContext
|
||||
private val windowManager: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
private val handler: Handler = Handler(Looper.getMainLooper())
|
||||
|
||||
private var overlayView: View? = null
|
||||
private var rootLayout: LinearLayout? = null
|
||||
private var layoutParams: WindowManager.LayoutParams? = null
|
||||
@Volatile
|
||||
private var isShowing = false
|
||||
|
||||
// Style properties
|
||||
private var textSizeSp = 14
|
||||
private var backgroundAlpha = 128
|
||||
private var cornerRadius = 90
|
||||
private var paddingDp = 8
|
||||
private var titleColorHex = "#FFFFFF"
|
||||
private var valueColorHex = "#FFFFFF"
|
||||
private var overlayFormat = "full"
|
||||
private var position = "top_center"
|
||||
private var splitMode = "side_by_side"
|
||||
private var updateIntervalMs = 1000
|
||||
private var draggable = false
|
||||
|
||||
// Display toggles
|
||||
private var showBatteryTemp = false
|
||||
private var showCpuUsage = true
|
||||
private var showCpuClock = false
|
||||
private var showCpuTemp = false
|
||||
private var showRam = false
|
||||
private var showFps = true
|
||||
private var showFrameTime = false
|
||||
private var showGpuUsage = true
|
||||
private var showGpuClock = false
|
||||
private var showGpuTemp = false
|
||||
private var showRamSpeed = false
|
||||
private var showRamTemp = false
|
||||
|
||||
// Touch handling
|
||||
private var longPressEnabled = false
|
||||
private var longPressThresholdMs = 500L
|
||||
private var pressActive = false
|
||||
private var downX = 0f
|
||||
private var downY = 0f
|
||||
|
||||
private var gestureDetector: GestureDetector? = null
|
||||
private var doubleTapCaptureEnabled = true
|
||||
private var singleTapToggleEnabled = true
|
||||
private var bgDrawable: GradientDrawable? = null
|
||||
|
||||
private var itemSpacingDp = 8
|
||||
private var layoutChanged = false
|
||||
|
||||
// Touch coordinates for dragging
|
||||
private var initialX = 0
|
||||
private var initialY = 0
|
||||
private var initialTouchX = 0f
|
||||
private var initialTouchY = 0f
|
||||
|
||||
init {
|
||||
bgDrawable = GradientDrawable()
|
||||
applyBackgroundStyle()
|
||||
|
||||
gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||
if (doubleTapCaptureEnabled) {
|
||||
val dataExport = GameDataExport.getInstance()
|
||||
val perAppLogManager = dataExport.getPerAppLogManager()
|
||||
val currentPackage = ForegroundAppDetector.getForegroundPackageName(context)
|
||||
|
||||
if (dataExport.getLoggingMode() == GameDataExport.LoggingMode.PER_APP) {
|
||||
// Per-app mode: Handle double-tap for manual logging
|
||||
|
||||
// Check if this app already has auto-logging enabled
|
||||
if (perAppLogManager.isAppLoggingEnabled(context, currentPackage)) {
|
||||
Toast.makeText(context, "This app has auto-logging enabled. Logs are saved automatically.", Toast.LENGTH_SHORT).show()
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if manually logging for this app
|
||||
if (perAppLogManager.isAppLoggingActive(currentPackage)) {
|
||||
// Stop manual logging
|
||||
perAppLogManager.stopManualLoggingForApp(currentPackage)
|
||||
Toast.makeText(context, "Manual logging stopped and saved", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
// Start manual logging
|
||||
perAppLogManager.startManualLoggingForApp(currentPackage)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Global mode: Original behavior
|
||||
if (dataExport.isCapturing()) {
|
||||
dataExport.stopCapture()
|
||||
dataExport.exportDataToCsv()
|
||||
Toast.makeText(context, "Capture Stopped and Data Exported", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
dataExport.startCapture()
|
||||
Toast.makeText(context, "Capture Started", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return true
|
||||
}
|
||||
return super.onDoubleTap(e)
|
||||
}
|
||||
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
if (singleTapToggleEnabled) {
|
||||
overlayFormat = if (overlayFormat == "full") "minimal" else "full"
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putString("game_bar_format", overlayFormat)
|
||||
.apply()
|
||||
Toast.makeText(context, "Overlay Format: $overlayFormat", Toast.LENGTH_SHORT).show()
|
||||
updateStats()
|
||||
return true
|
||||
}
|
||||
return super.onSingleTapConfirmed(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Long press runnable
|
||||
private val longPressRunnable = Runnable {
|
||||
if (pressActive) {
|
||||
openOverlaySettings()
|
||||
pressActive = false
|
||||
}
|
||||
}
|
||||
|
||||
// Update runnable
|
||||
private val updateRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (isShowing) {
|
||||
updateStats()
|
||||
handler.postDelayed(this, updateIntervalMs.toLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun applyPreferences() {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
showFps = prefs.getBoolean("game_bar_fps_enable", true)
|
||||
showFrameTime = prefs.getBoolean("game_bar_frame_time_enable", false)
|
||||
showBatteryTemp = prefs.getBoolean("game_bar_temp_enable", false)
|
||||
showCpuUsage = prefs.getBoolean("game_bar_cpu_usage_enable", true)
|
||||
showCpuClock = prefs.getBoolean("game_bar_cpu_clock_enable", false)
|
||||
showCpuTemp = prefs.getBoolean("game_bar_cpu_temp_enable", false)
|
||||
showRam = prefs.getBoolean("game_bar_ram_enable", false)
|
||||
|
||||
showGpuUsage = prefs.getBoolean("game_bar_gpu_usage_enable", true)
|
||||
showGpuClock = prefs.getBoolean("game_bar_gpu_clock_enable", false)
|
||||
showGpuTemp = prefs.getBoolean("game_bar_gpu_temp_enable", false)
|
||||
|
||||
showRamSpeed = prefs.getBoolean("game_bar_ram_speed_enable", false)
|
||||
showRamTemp = prefs.getBoolean("game_bar_ram_temp_enable", false)
|
||||
|
||||
doubleTapCaptureEnabled = prefs.getBoolean("game_bar_doubletap_capture", true)
|
||||
singleTapToggleEnabled = prefs.getBoolean("game_bar_single_tap_toggle", true)
|
||||
|
||||
updateSplitMode(prefs.getString("game_bar_split_mode", "side_by_side") ?: "side_by_side")
|
||||
updateTextSize(prefs.getInt("game_bar_text_size", 12))
|
||||
updateBackgroundAlpha(prefs.getInt("game_bar_background_alpha", 95))
|
||||
updateCornerRadius(prefs.getInt("game_bar_corner_radius", 100))
|
||||
updatePadding(prefs.getInt("game_bar_padding", 4))
|
||||
updateTitleColor(prefs.getString("game_bar_title_color", "#FFFFFF") ?: "#FFFFFF")
|
||||
updateValueColor(prefs.getString("game_bar_value_color", "#4CAF50") ?: "#4CAF50")
|
||||
updateOverlayFormat(prefs.getString("game_bar_format", "full") ?: "full")
|
||||
updateUpdateInterval(prefs.getString("game_bar_update_interval", "1000") ?: "1000")
|
||||
updatePosition(prefs.getString("game_bar_position", "draggable") ?: "draggable")
|
||||
|
||||
val spacing = prefs.getInt("game_bar_item_spacing", 8)
|
||||
updateItemSpacing(spacing)
|
||||
|
||||
longPressEnabled = prefs.getBoolean("game_bar_longpress_enable", true)
|
||||
val lpTimeoutStr = prefs.getString("game_bar_longpress_timeout", "500") ?: "500"
|
||||
try {
|
||||
val lpt = lpTimeoutStr.toLong()
|
||||
setLongPressThresholdMs(lpt)
|
||||
} catch (ignored: NumberFormatException) {}
|
||||
}
|
||||
|
||||
fun show() {
|
||||
// Force cleanup any existing overlay before showing new one
|
||||
if (isShowing) {
|
||||
hide()
|
||||
}
|
||||
|
||||
// Double check to make sure no overlay view exists
|
||||
overlayView?.let { view ->
|
||||
try {
|
||||
windowManager.removeView(view)
|
||||
} catch (e: Exception) {
|
||||
// View might already be removed
|
||||
}
|
||||
overlayView = null
|
||||
}
|
||||
|
||||
applyPreferences()
|
||||
|
||||
layoutParams = WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT
|
||||
)
|
||||
|
||||
if ("draggable" == position) {
|
||||
draggable = true
|
||||
loadSavedPosition(layoutParams!!)
|
||||
if (layoutParams!!.x == 0 && layoutParams!!.y == 0) {
|
||||
layoutParams!!.gravity = Gravity.TOP or Gravity.START
|
||||
layoutParams!!.x = 0
|
||||
layoutParams!!.y = 100
|
||||
}
|
||||
} else {
|
||||
draggable = false
|
||||
applyPosition(layoutParams!!, position)
|
||||
}
|
||||
|
||||
overlayView = LinearLayout(context).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
rootLayout = overlayView as LinearLayout
|
||||
applySplitMode()
|
||||
applyBackgroundStyle()
|
||||
applyPadding()
|
||||
|
||||
overlayView?.setOnTouchListener { _, event ->
|
||||
gestureDetector?.let {
|
||||
if (it.onTouchEvent(event)) {
|
||||
return@setOnTouchListener true
|
||||
}
|
||||
}
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (draggable) {
|
||||
initialX = layoutParams!!.x
|
||||
initialY = layoutParams!!.y
|
||||
initialTouchX = event.rawX
|
||||
initialTouchY = event.rawY
|
||||
}
|
||||
if (longPressEnabled) {
|
||||
pressActive = true
|
||||
downX = event.rawX
|
||||
downY = event.rawY
|
||||
handler.postDelayed(longPressRunnable, longPressThresholdMs)
|
||||
}
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (longPressEnabled && pressActive) {
|
||||
val dx = Math.abs(event.rawX - downX)
|
||||
val dy = Math.abs(event.rawY - downY)
|
||||
if (dx > TOUCH_SLOP || dy > TOUCH_SLOP) {
|
||||
pressActive = false
|
||||
handler.removeCallbacks(longPressRunnable)
|
||||
}
|
||||
}
|
||||
if (draggable) {
|
||||
val deltaX = (event.rawX - initialTouchX).toInt()
|
||||
val deltaY = (event.rawY - initialTouchY).toInt()
|
||||
layoutParams!!.x = initialX + deltaX
|
||||
layoutParams!!.y = initialY + deltaY
|
||||
windowManager.updateViewLayout(overlayView, layoutParams)
|
||||
}
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
if (longPressEnabled && pressActive) {
|
||||
pressActive = false
|
||||
handler.removeCallbacks(longPressRunnable)
|
||||
}
|
||||
if (draggable) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
prefs.edit()
|
||||
.putInt(PREF_KEY_X, layoutParams!!.x)
|
||||
.putInt(PREF_KEY_Y, layoutParams!!.y)
|
||||
.apply()
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
windowManager.addView(overlayView, layoutParams)
|
||||
isShowing = true
|
||||
startUpdates()
|
||||
|
||||
// Start the FPS meter if using the new API method
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
|
||||
GameBarFpsMeter.getInstance(context).start()
|
||||
}
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
// Set showing to false first to prevent any further operations
|
||||
isShowing = false
|
||||
|
||||
stopUpdates()
|
||||
|
||||
// Stop FPS meter first
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
|
||||
GameBarFpsMeter.getInstance(context).stop()
|
||||
}
|
||||
|
||||
// Remove overlay view
|
||||
overlayView?.let { view ->
|
||||
try {
|
||||
if (view.parent != null) {
|
||||
windowManager.removeView(view)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// View might already be removed, log but continue cleanup
|
||||
android.util.Log.w("GameBar", "Error removing overlay view: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all references
|
||||
overlayView = null
|
||||
rootLayout = null
|
||||
layoutParams = null
|
||||
layoutChanged = true // Mark layout as changed
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
hide()
|
||||
|
||||
// Remove all handler callbacks and messages
|
||||
handler.removeCallbacks(updateRunnable)
|
||||
handler.removeCallbacks(longPressRunnable)
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
|
||||
// Clear all object references to prevent memory leaks
|
||||
gestureDetector = null
|
||||
bgDrawable = null
|
||||
|
||||
// Reset all state variables
|
||||
pressActive = false
|
||||
layoutChanged = false
|
||||
}
|
||||
|
||||
private fun stopUpdates() {
|
||||
handler.removeCallbacks(updateRunnable)
|
||||
handler.removeCallbacks(longPressRunnable)
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
private fun startUpdates() {
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
handler.post(updateRunnable)
|
||||
}
|
||||
|
||||
private fun updateStats() {
|
||||
// Early return if not showing or no layout
|
||||
if (!isShowing) return
|
||||
val layout = rootLayout ?: return
|
||||
|
||||
try {
|
||||
// Always clear views to prevent duplication
|
||||
layout.removeAllViews()
|
||||
layoutChanged = false
|
||||
|
||||
// Create fresh views each time
|
||||
val statViews = mutableListOf<View>()
|
||||
|
||||
// 1) FPS - Always collect for logging
|
||||
val fpsVal = GameBarFpsMeter.getInstance(context).getFps()
|
||||
val fpsStr = if (fpsVal >= 0) String.format(Locale.getDefault(), "%.0f", fpsVal) else "N/A"
|
||||
if (showFps) {
|
||||
statViews.add(createStatLine("FPS", fpsStr))
|
||||
}
|
||||
|
||||
// 1.1) Frame Time - Calculate from FPS
|
||||
var frameTimeStr = "N/A"
|
||||
if (fpsVal > 0) {
|
||||
val frameTime = 1000.0 / fpsVal
|
||||
frameTimeStr = String.format(Locale.getDefault(), "%.2f", frameTime)
|
||||
}
|
||||
if (showFrameTime) {
|
||||
statViews.add(createStatLine("Frame Time", if (frameTimeStr == "N/A") "N/A" else "${frameTimeStr}ms"))
|
||||
}
|
||||
|
||||
// 2) Battery temp - Always collect for logging
|
||||
var batteryTempStr = "N/A"
|
||||
val tmp = readLine(BATTERY_TEMP_PATH)
|
||||
if (!tmp.isNullOrEmpty()) {
|
||||
try {
|
||||
val raw = tmp.trim().toInt()
|
||||
val celsius = raw / 10f
|
||||
batteryTempStr = String.format(Locale.getDefault(), "%.1f", celsius)
|
||||
} catch (ignored: NumberFormatException) {}
|
||||
}
|
||||
if (showBatteryTemp) {
|
||||
statViews.add(createStatLine("Temp", "${batteryTempStr}°C"))
|
||||
}
|
||||
|
||||
// 3) CPU usage - Always collect for logging
|
||||
var cpuUsageStr = "N/A"
|
||||
cpuUsageStr = GameBarCpuInfo.getCpuUsage()
|
||||
if (showCpuUsage) {
|
||||
val display = if (cpuUsageStr == "N/A") "N/A" else "${cpuUsageStr}%"
|
||||
statViews.add(createStatLine("CPU", display))
|
||||
}
|
||||
|
||||
// 4) CPU freq - Always collect for logging
|
||||
var cpuClockStr = "N/A"
|
||||
if (showCpuClock) {
|
||||
val freqs = GameBarCpuInfo.getCpuFrequencies()
|
||||
if (freqs.isNotEmpty()) {
|
||||
statViews.add(buildCpuFreqView(freqs))
|
||||
cpuClockStr = freqs.joinToString("; ")
|
||||
}
|
||||
} else {
|
||||
// Still collect even if not shown, for potential logging
|
||||
val freqs = GameBarCpuInfo.getCpuFrequencies()
|
||||
if (freqs.isNotEmpty()) {
|
||||
cpuClockStr = freqs.joinToString("; ")
|
||||
}
|
||||
}
|
||||
|
||||
// 5) CPU temp
|
||||
var cpuTempStr = "N/A"
|
||||
if (showCpuTemp) {
|
||||
cpuTempStr = GameBarCpuInfo.getCpuTemp()
|
||||
statViews.add(createStatLine("CPU Temp", if (cpuTempStr == "N/A") "N/A" else "${cpuTempStr}°C"))
|
||||
} else {
|
||||
// Still collect even if not shown
|
||||
cpuTempStr = GameBarCpuInfo.getCpuTemp()
|
||||
}
|
||||
|
||||
// 6) RAM usage
|
||||
var ramStr = "N/A"
|
||||
if (showRam) {
|
||||
ramStr = GameBarMemInfo.getRamUsage()
|
||||
statViews.add(createStatLine("RAM", if (ramStr == "N/A") "N/A" else "$ramStr MB"))
|
||||
} else {
|
||||
// Still collect even if not shown
|
||||
ramStr = GameBarMemInfo.getRamUsage()
|
||||
}
|
||||
|
||||
// 6.1) RAM speed
|
||||
var ramSpeedStr = "N/A"
|
||||
if (showRamSpeed) {
|
||||
ramSpeedStr = GameBarMemInfo.getRamSpeed()
|
||||
statViews.add(createStatLine("RAM Freq", ramSpeedStr))
|
||||
} else {
|
||||
// Still collect even if not shown
|
||||
ramSpeedStr = GameBarMemInfo.getRamSpeed()
|
||||
}
|
||||
|
||||
// 6.2) RAM temp
|
||||
var ramTempStr = "N/A"
|
||||
if (showRamTemp) {
|
||||
ramTempStr = GameBarMemInfo.getRamTemp()
|
||||
statViews.add(createStatLine("RAM Temp", ramTempStr))
|
||||
} else {
|
||||
// Still collect even if not shown
|
||||
ramTempStr = GameBarMemInfo.getRamTemp()
|
||||
}
|
||||
|
||||
// 7) GPU usage - Always collect for logging
|
||||
var gpuUsageStr = "N/A"
|
||||
gpuUsageStr = GameBarGpuInfo.getGpuUsage()
|
||||
if (showGpuUsage) {
|
||||
statViews.add(createStatLine("GPU", if (gpuUsageStr == "N/A") "N/A" else "${gpuUsageStr}%"))
|
||||
}
|
||||
|
||||
// 8) GPU clock - Always collect for logging
|
||||
var gpuClockStr = "N/A"
|
||||
gpuClockStr = GameBarGpuInfo.getGpuClock()
|
||||
if (showGpuClock) {
|
||||
statViews.add(createStatLine("GPU Freq", if (gpuClockStr == "N/A") "N/A" else "${gpuClockStr}MHz"))
|
||||
}
|
||||
|
||||
// 9) GPU temp - Always collect for logging
|
||||
var gpuTempStr = "N/A"
|
||||
gpuTempStr = GameBarGpuInfo.getGpuTemp()
|
||||
if (showGpuTemp) {
|
||||
statViews.add(createStatLine("GPU Temp", if (gpuTempStr == "N/A") "N/A" else "${gpuTempStr}°C"))
|
||||
}
|
||||
|
||||
if (splitMode == "side_by_side") {
|
||||
layout.orientation = LinearLayout.HORIZONTAL
|
||||
if (overlayFormat == "minimal") {
|
||||
for (i in statViews.indices) {
|
||||
layout.addView(statViews[i])
|
||||
if (i < statViews.size - 1) {
|
||||
layout.addView(createDotView())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (view in statViews) {
|
||||
layout.addView(view)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
layout.orientation = LinearLayout.VERTICAL
|
||||
for (view in statViews) {
|
||||
layout.addView(view)
|
||||
}
|
||||
}
|
||||
|
||||
if (GameDataExport.getInstance().isCapturing()) {
|
||||
val dateTime = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())
|
||||
val pkgName = ForegroundAppDetector.getForegroundPackageName(context)
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
// Check logging parameters and use N/A if disabled
|
||||
val logFps = if (prefs.getBoolean(GameBarLogFragment.PREF_LOG_FPS, true)) fpsStr else "N/A"
|
||||
val logFrameTime = if (prefs.getBoolean(GameBarLogFragment.PREF_LOG_FRAME_TIME, true)) frameTimeStr else "N/A"
|
||||
val logBatteryTemp = if (prefs.getBoolean(GameBarLogFragment.PREF_LOG_BATTERY_TEMP, true)) batteryTempStr else "N/A"
|
||||
val logCpuUsage = if (prefs.getBoolean(GameBarLogFragment.PREF_LOG_CPU_USAGE, true)) cpuUsageStr else "N/A"
|
||||
val logCpuClock = if (prefs.getBoolean(GameBarLogFragment.PREF_LOG_CPU_CLOCK, true)) cpuClockStr else "N/A"
|
||||
val logCpuTemp = if (prefs.getBoolean(GameBarLogFragment.PREF_LOG_CPU_TEMP, true)) cpuTempStr else "N/A"
|
||||
val logRam = if (prefs.getBoolean(GameBarLogFragment.PREF_LOG_RAM, true)) ramStr else "N/A"
|
||||
val logRamSpeed = if (prefs.getBoolean(GameBarLogFragment.PREF_LOG_RAM_SPEED, true)) ramSpeedStr else "N/A"
|
||||
val logRamTemp = if (prefs.getBoolean(GameBarLogFragment.PREF_LOG_RAM_TEMP, true)) ramTempStr else "N/A"
|
||||
val logGpuUsage = if (prefs.getBoolean(GameBarLogFragment.PREF_LOG_GPU_USAGE, true)) gpuUsageStr else "N/A"
|
||||
val logGpuClock = if (prefs.getBoolean(GameBarLogFragment.PREF_LOG_GPU_CLOCK, true)) gpuClockStr else "N/A"
|
||||
val logGpuTemp = if (prefs.getBoolean(GameBarLogFragment.PREF_LOG_GPU_TEMP, true)) gpuTempStr else "N/A"
|
||||
|
||||
GameDataExport.getInstance().addOverlayData(
|
||||
dateTime,
|
||||
pkgName,
|
||||
logFps,
|
||||
logFrameTime,
|
||||
logBatteryTemp,
|
||||
logCpuUsage,
|
||||
logCpuClock,
|
||||
logCpuTemp,
|
||||
logRam,
|
||||
logRamSpeed,
|
||||
logRamTemp,
|
||||
logGpuUsage,
|
||||
logGpuClock,
|
||||
logGpuTemp
|
||||
)
|
||||
}
|
||||
|
||||
layoutParams?.let { lp ->
|
||||
overlayView?.let { view ->
|
||||
try {
|
||||
windowManager.updateViewLayout(view, lp)
|
||||
} catch (e: Exception) {
|
||||
// View might be in invalid state, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Log error but continue operation to prevent crashes
|
||||
android.util.Log.e("GameBar", "Error updating overlay stats: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildCpuFreqView(freqs: List<String>): View {
|
||||
val freqContainer = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
}
|
||||
|
||||
val spacingPx = dpToPx(context, itemSpacingDp)
|
||||
val outerLp = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
setMargins(spacingPx, spacingPx / 2, spacingPx, spacingPx / 2)
|
||||
}
|
||||
freqContainer.layoutParams = outerLp
|
||||
|
||||
if (overlayFormat == "full") {
|
||||
val labelTv = TextView(context).apply {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, textSizeSp.toFloat())
|
||||
try {
|
||||
setTextColor(Color.parseColor(titleColorHex))
|
||||
} catch (e: Exception) {
|
||||
setTextColor(Color.WHITE)
|
||||
}
|
||||
text = "CPU Freq "
|
||||
}
|
||||
freqContainer.addView(labelTv)
|
||||
}
|
||||
|
||||
val verticalFreqs = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
}
|
||||
|
||||
for (freqLine in freqs) {
|
||||
val lineLayout = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
}
|
||||
|
||||
val freqTv = TextView(context).apply {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, textSizeSp.toFloat())
|
||||
try {
|
||||
setTextColor(Color.parseColor(valueColorHex))
|
||||
} catch (e: Exception) {
|
||||
setTextColor(Color.WHITE)
|
||||
}
|
||||
text = freqLine
|
||||
}
|
||||
|
||||
lineLayout.addView(freqTv)
|
||||
|
||||
val lineLp = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
setMargins(spacingPx, spacingPx / 4, spacingPx, spacingPx / 4)
|
||||
}
|
||||
lineLayout.layoutParams = lineLp
|
||||
|
||||
verticalFreqs.addView(lineLayout)
|
||||
}
|
||||
|
||||
freqContainer.addView(verticalFreqs)
|
||||
return freqContainer
|
||||
}
|
||||
|
||||
private fun createStatLine(title: String, rawValue: String): LinearLayout {
|
||||
val lineLayout = LinearLayout(context).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
}
|
||||
|
||||
if (overlayFormat == "full") {
|
||||
val tvTitle = TextView(context).apply {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, textSizeSp.toFloat())
|
||||
try {
|
||||
setTextColor(Color.parseColor(titleColorHex))
|
||||
} catch (e: Exception) {
|
||||
setTextColor(Color.WHITE)
|
||||
}
|
||||
text = if (title.isEmpty()) "" else "$title "
|
||||
}
|
||||
|
||||
val tvValue = TextView(context).apply {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, textSizeSp.toFloat())
|
||||
try {
|
||||
setTextColor(Color.parseColor(valueColorHex))
|
||||
} catch (e: Exception) {
|
||||
setTextColor(Color.WHITE)
|
||||
}
|
||||
text = rawValue
|
||||
}
|
||||
|
||||
lineLayout.addView(tvTitle)
|
||||
lineLayout.addView(tvValue)
|
||||
} else {
|
||||
val tvMinimal = TextView(context).apply {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, textSizeSp.toFloat())
|
||||
try {
|
||||
setTextColor(Color.parseColor(valueColorHex))
|
||||
} catch (e: Exception) {
|
||||
setTextColor(Color.WHITE)
|
||||
}
|
||||
text = rawValue
|
||||
}
|
||||
lineLayout.addView(tvMinimal)
|
||||
}
|
||||
|
||||
val spacingPx = dpToPx(context, itemSpacingDp)
|
||||
val lp = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
setMargins(spacingPx, spacingPx / 2, spacingPx, spacingPx / 2)
|
||||
}
|
||||
lineLayout.layoutParams = lp
|
||||
|
||||
return lineLayout
|
||||
}
|
||||
|
||||
private fun createDotView(): View {
|
||||
return TextView(context).apply {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, textSizeSp.toFloat())
|
||||
try {
|
||||
setTextColor(Color.parseColor(valueColorHex))
|
||||
} catch (e: Exception) {
|
||||
setTextColor(Color.WHITE)
|
||||
}
|
||||
text = " . "
|
||||
}
|
||||
}
|
||||
|
||||
// Public setter methods for feature toggles
|
||||
fun setShowBatteryTemp(show: Boolean) { showBatteryTemp = show }
|
||||
fun setShowCpuUsage(show: Boolean) { showCpuUsage = show }
|
||||
fun setShowCpuClock(show: Boolean) { showCpuClock = show }
|
||||
fun setShowCpuTemp(show: Boolean) { showCpuTemp = show }
|
||||
fun setShowRam(show: Boolean) { showRam = show }
|
||||
fun setShowFps(show: Boolean) { showFps = show }
|
||||
fun setShowFrameTime(show: Boolean) { showFrameTime = show }
|
||||
fun setShowGpuUsage(show: Boolean) { showGpuUsage = show }
|
||||
fun setShowGpuClock(show: Boolean) { showGpuClock = show }
|
||||
fun setShowGpuTemp(show: Boolean) { showGpuTemp = show }
|
||||
fun setShowRamSpeed(show: Boolean) { showRamSpeed = show }
|
||||
fun setShowRamTemp(show: Boolean) { showRamTemp = show }
|
||||
|
||||
fun updateTextSize(sp: Int) {
|
||||
textSizeSp = sp
|
||||
}
|
||||
|
||||
fun updateCornerRadius(radius: Int) {
|
||||
cornerRadius = radius
|
||||
applyBackgroundStyle()
|
||||
}
|
||||
|
||||
fun updateBackgroundAlpha(alpha: Int) {
|
||||
backgroundAlpha = alpha
|
||||
applyBackgroundStyle()
|
||||
}
|
||||
|
||||
fun updatePadding(dp: Int) {
|
||||
paddingDp = dp
|
||||
applyPadding()
|
||||
}
|
||||
|
||||
fun updateTitleColor(hex: String) {
|
||||
titleColorHex = hex
|
||||
}
|
||||
|
||||
fun updateValueColor(hex: String) {
|
||||
valueColorHex = hex
|
||||
}
|
||||
|
||||
fun updateOverlayFormat(format: String) {
|
||||
overlayFormat = format
|
||||
if (isShowing) {
|
||||
updateStats()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateItemSpacing(dp: Int) {
|
||||
itemSpacingDp = dp
|
||||
if (isShowing) {
|
||||
updateStats()
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePosition(pos: String) {
|
||||
position = pos
|
||||
if (isShowing && overlayView != null && layoutParams != null) {
|
||||
if ("draggable" == position) {
|
||||
draggable = true
|
||||
loadSavedPosition(layoutParams!!)
|
||||
if (layoutParams!!.x == 0 && layoutParams!!.y == 0) {
|
||||
layoutParams!!.gravity = Gravity.TOP or Gravity.START
|
||||
layoutParams!!.x = 0
|
||||
layoutParams!!.y = 100
|
||||
}
|
||||
} else {
|
||||
draggable = false
|
||||
applyPosition(layoutParams!!, position)
|
||||
}
|
||||
windowManager.updateViewLayout(overlayView, layoutParams)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSplitMode(mode: String) {
|
||||
splitMode = mode
|
||||
if (isShowing && overlayView != null) {
|
||||
applySplitMode()
|
||||
updateStats()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateUpdateInterval(intervalStr: String) {
|
||||
try {
|
||||
updateIntervalMs = intervalStr.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
updateIntervalMs = 1000
|
||||
}
|
||||
if (isShowing) {
|
||||
startUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
fun setLongPressEnabled(enabled: Boolean) {
|
||||
longPressEnabled = enabled
|
||||
}
|
||||
|
||||
fun setLongPressThresholdMs(ms: Long) {
|
||||
longPressThresholdMs = ms
|
||||
}
|
||||
|
||||
fun setDoubleTapCaptureEnabled(enabled: Boolean) {
|
||||
doubleTapCaptureEnabled = enabled
|
||||
}
|
||||
|
||||
fun setSingleTapToggleEnabled(enabled: Boolean) {
|
||||
singleTapToggleEnabled = enabled
|
||||
}
|
||||
|
||||
fun isCurrentlyShowing(): Boolean {
|
||||
return isShowing
|
||||
}
|
||||
|
||||
private fun applyBackgroundStyle() {
|
||||
// Ensure we have a valid bgDrawable
|
||||
if (bgDrawable == null) {
|
||||
bgDrawable = GradientDrawable()
|
||||
}
|
||||
|
||||
// Apply background color with proper alpha
|
||||
val color = Color.argb(
|
||||
Math.max(backgroundAlpha, 16), // Minimum alpha of 16 to prevent invisible overlays
|
||||
0, 0, 0
|
||||
)
|
||||
bgDrawable?.setColor(color)
|
||||
bgDrawable?.cornerRadius = cornerRadius.toFloat()
|
||||
|
||||
// Only apply background if overlay view exists
|
||||
overlayView?.let { view ->
|
||||
view.background = bgDrawable
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyPadding() {
|
||||
rootLayout?.let {
|
||||
val px = dpToPx(context, paddingDp)
|
||||
it.setPadding(px, px, px, px)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applySplitMode() {
|
||||
rootLayout?.let {
|
||||
it.orientation = if (splitMode == "side_by_side") {
|
||||
LinearLayout.HORIZONTAL
|
||||
} else {
|
||||
LinearLayout.VERTICAL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSavedPosition(lp: WindowManager.LayoutParams) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val savedX = prefs.getInt(PREF_KEY_X, Int.MIN_VALUE)
|
||||
val savedY = prefs.getInt(PREF_KEY_Y, Int.MIN_VALUE)
|
||||
if (savedX != Int.MIN_VALUE && savedY != Int.MIN_VALUE) {
|
||||
lp.gravity = Gravity.TOP or Gravity.START
|
||||
lp.x = savedX
|
||||
lp.y = savedY
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyPosition(lp: WindowManager.LayoutParams, pos: String) {
|
||||
when (pos) {
|
||||
"top_left" -> {
|
||||
lp.gravity = Gravity.TOP or Gravity.START
|
||||
lp.x = 0
|
||||
lp.y = 100
|
||||
}
|
||||
"top_center" -> {
|
||||
lp.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
|
||||
lp.y = 100
|
||||
}
|
||||
"top_right" -> {
|
||||
lp.gravity = Gravity.TOP or Gravity.END
|
||||
lp.x = 0
|
||||
lp.y = 100
|
||||
}
|
||||
"bottom_left" -> {
|
||||
lp.gravity = Gravity.BOTTOM or Gravity.START
|
||||
lp.x = 0
|
||||
lp.y = 100
|
||||
}
|
||||
"bottom_center" -> {
|
||||
lp.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
|
||||
lp.y = 100
|
||||
}
|
||||
"bottom_right" -> {
|
||||
lp.gravity = Gravity.BOTTOM or Gravity.END
|
||||
lp.x = 0
|
||||
lp.y = 100
|
||||
}
|
||||
else -> {
|
||||
lp.gravity = Gravity.TOP or Gravity.START
|
||||
lp.x = 0
|
||||
lp.y = 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readLine(path: String): String? {
|
||||
return try {
|
||||
BufferedReader(FileReader(path)).use { it.readLine() }
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun openOverlaySettings() {
|
||||
try {
|
||||
val intent = Intent(context, GameBarSettingsActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
// Exception ignored
|
||||
}
|
||||
}
|
||||
|
||||
private fun dpToPx(context: Context, dp: Int): Int {
|
||||
val scale = context.resources.displayMetrics.density
|
||||
return Math.round(dp * scale)
|
||||
}
|
||||
}
|
||||
39
src/com/android/gamebar/GameBarBootReceiver.kt
Normal file
39
src/com/android/gamebar/GameBarBootReceiver.kt
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.preference.PreferenceManager
|
||||
|
||||
class GameBarBootReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action
|
||||
if (Intent.ACTION_BOOT_COMPLETED == action || Intent.ACTION_LOCKED_BOOT_COMPLETED == action) {
|
||||
restoreOverlayState(context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreOverlayState(context: Context) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val mainEnabled = prefs.getBoolean("game_bar_enable", false)
|
||||
val autoEnabled = prefs.getBoolean("game_bar_auto_enable", false)
|
||||
|
||||
if (mainEnabled) {
|
||||
val gameBar = GameBar.getInstance(context)
|
||||
gameBar.applyPreferences()
|
||||
gameBar.show()
|
||||
}
|
||||
|
||||
if (autoEnabled) {
|
||||
val monitorIntent = Intent(context, GameBarMonitorService::class.java)
|
||||
context.startService(monitorIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/com/android/gamebar/GameBarCpuInfo.kt
Normal file
121
src/com/android/gamebar/GameBarCpuInfo.kt
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import java.io.BufferedReader
|
||||
import java.io.FileReader
|
||||
import java.io.IOException
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
object GameBarCpuInfo {
|
||||
|
||||
private var prevIdle = -1L
|
||||
private var prevTotal = -1L
|
||||
|
||||
private const val CPU_TEMP_PATH = "/sys/class/thermal/thermal_zone48/temp"
|
||||
|
||||
fun getCpuUsage(): String {
|
||||
val line = readLine("/proc/stat")
|
||||
if (line == null || !line.startsWith("cpu ")) return "N/A"
|
||||
val parts = line.split("\\s+".toRegex())
|
||||
if (parts.size < 8) return "N/A"
|
||||
|
||||
return try {
|
||||
val user = parts[1].toLong()
|
||||
val nice = parts[2].toLong()
|
||||
val system = parts[3].toLong()
|
||||
val idle = parts[4].toLong()
|
||||
val iowait = parts[5].toLong()
|
||||
val irq = parts[6].toLong()
|
||||
val softirq = parts[7].toLong()
|
||||
val steal = if (parts.size > 8) parts[8].toLong() else 0L
|
||||
|
||||
val total = user + nice + system + idle + iowait + irq + softirq + steal
|
||||
|
||||
if (prevTotal != -1L && total != prevTotal) {
|
||||
val diffTotal = total - prevTotal
|
||||
val diffIdle = idle - prevIdle
|
||||
val usage = 100 * (diffTotal - diffIdle) / diffTotal
|
||||
prevTotal = total
|
||||
prevIdle = idle
|
||||
usage.toString()
|
||||
} else {
|
||||
prevTotal = total
|
||||
prevIdle = idle
|
||||
"N/A"
|
||||
}
|
||||
} catch (e: NumberFormatException) {
|
||||
"N/A"
|
||||
}
|
||||
}
|
||||
|
||||
fun getCpuFrequencies(): List<String> {
|
||||
val result = mutableListOf<String>()
|
||||
val cpuDirPath = "/sys/devices/system/cpu/"
|
||||
val cpuDir = File(cpuDirPath)
|
||||
val files = cpuDir.listFiles { _, name -> name.matches(Regex("cpu\\d+")) }
|
||||
if (files.isNullOrEmpty()) {
|
||||
return result
|
||||
}
|
||||
|
||||
val cpuFolders = files.toMutableList()
|
||||
cpuFolders.sortBy { extractCpuNumber(it) }
|
||||
|
||||
for (cpu in cpuFolders) {
|
||||
val freqPath = "${cpu.absolutePath}/cpufreq/scaling_cur_freq"
|
||||
val freqStr = readLine(freqPath)
|
||||
if (!freqStr.isNullOrEmpty()) {
|
||||
try {
|
||||
val khz = freqStr.trim().toInt()
|
||||
val mhz = khz / 1000
|
||||
result.add("${cpu.name}: $mhz MHz")
|
||||
} catch (e: NumberFormatException) {
|
||||
result.add("${cpu.name}: N/A")
|
||||
}
|
||||
} else {
|
||||
result.add("${cpu.name}: offline or frequency not available")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun getCpuTemp(): String {
|
||||
val line = readLine(CPU_TEMP_PATH) ?: return "N/A"
|
||||
val cleanLine = line.trim()
|
||||
return try {
|
||||
val raw = cleanLine.toFloat()
|
||||
// Device reports in millidegrees (e.g., 33849 = 33.849°C)
|
||||
val celsius = raw / 1000f
|
||||
// Sanity check: CPU temp should be between 0 and 150°C
|
||||
if (celsius > 0f && celsius < 150f) {
|
||||
String.format(Locale.getDefault(), "%.1f", celsius)
|
||||
} else {
|
||||
"N/A"
|
||||
}
|
||||
} catch (e: NumberFormatException) {
|
||||
"N/A"
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractCpuNumber(cpuFolder: File): Int {
|
||||
val name = cpuFolder.name.replace("cpu", "")
|
||||
return try {
|
||||
name.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
private fun readLine(path: String): String? {
|
||||
return try {
|
||||
BufferedReader(FileReader(path)).use { it.readLine() }
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
186
src/com/android/gamebar/GameBarFpsMeter.kt
Normal file
186
src/com/android/gamebar/GameBarFpsMeter.kt
Normal file
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.view.WindowManager
|
||||
import android.window.TaskFpsCallback
|
||||
import androidx.preference.PreferenceManager
|
||||
import java.io.BufferedReader
|
||||
import java.io.FileReader
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.Method
|
||||
|
||||
class GameBarFpsMeter private constructor(context: Context) {
|
||||
|
||||
companion object {
|
||||
private const val TOLERANCE = 0.1f
|
||||
private const val STALENESS_THRESHOLD_MS = 2000L
|
||||
private const val TASK_CHECK_INTERVAL_MS = 1000L
|
||||
|
||||
@Volatile
|
||||
private var INSTANCE: GameBarFpsMeter? = null
|
||||
|
||||
@JvmStatic
|
||||
@Synchronized
|
||||
fun getInstance(context: Context): GameBarFpsMeter {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
INSTANCE ?: GameBarFpsMeter(context.applicationContext).also { INSTANCE = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val context: Context = context.applicationContext
|
||||
private val windowManager: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
private val prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private var currentFps = 0f
|
||||
private var taskFpsCallback: TaskFpsCallback? = null
|
||||
private var callbackRegistered = false
|
||||
private var currentTaskId = -1
|
||||
private var lastFpsUpdateTime = System.currentTimeMillis()
|
||||
private val handler = Handler()
|
||||
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
taskFpsCallback = object : TaskFpsCallback() {
|
||||
override fun onFpsReported(fps: Float) {
|
||||
if (fps > 0) {
|
||||
currentFps = fps
|
||||
lastFpsUpdateTime = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
val method = prefs.getString("game_bar_fps_method", "new")
|
||||
if (method != "new") return
|
||||
|
||||
stop()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val taskId = getFocusedTaskId()
|
||||
if (taskId <= 0) {
|
||||
return
|
||||
}
|
||||
currentTaskId = taskId
|
||||
try {
|
||||
taskFpsCallback?.let {
|
||||
windowManager.registerTaskFpsCallback(currentTaskId, Runnable::run, it)
|
||||
callbackRegistered = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Ignore registration errors
|
||||
}
|
||||
lastFpsUpdateTime = System.currentTimeMillis()
|
||||
handler.postDelayed(taskCheckRunnable, TASK_CHECK_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
val method = prefs.getString("game_bar_fps_method", "new")
|
||||
if (method == "new" && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (callbackRegistered) {
|
||||
try {
|
||||
taskFpsCallback?.let {
|
||||
windowManager.unregisterTaskFpsCallback(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Ignore unregistration errors
|
||||
}
|
||||
callbackRegistered = false
|
||||
}
|
||||
handler.removeCallbacks(taskCheckRunnable)
|
||||
}
|
||||
}
|
||||
|
||||
fun getFps(): Float {
|
||||
val method = prefs.getString("game_bar_fps_method", "new")
|
||||
return if (method == "legacy") {
|
||||
readLegacyFps()
|
||||
} else {
|
||||
currentFps
|
||||
}
|
||||
}
|
||||
|
||||
private fun readLegacyFps(): Float {
|
||||
try {
|
||||
BufferedReader(FileReader("/sys/class/drm/sde-crtc-0/measured_fps")).use { br ->
|
||||
val line = br.readLine()
|
||||
if (line != null && line.startsWith("fps:")) {
|
||||
val parts = line.split("\\s+".toRegex())
|
||||
if (parts.size >= 2) {
|
||||
return parts[1].trim().toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
// Ignore errors
|
||||
} catch (e: NumberFormatException) {
|
||||
// Ignore errors
|
||||
}
|
||||
return -1f
|
||||
}
|
||||
|
||||
private val taskCheckRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val newTaskId = getFocusedTaskId()
|
||||
if (newTaskId > 0 && newTaskId != currentTaskId) {
|
||||
reinitCallback()
|
||||
} else {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastFpsUpdateTime > STALENESS_THRESHOLD_MS) {
|
||||
reinitCallback()
|
||||
}
|
||||
}
|
||||
handler.postDelayed(this, TASK_CHECK_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFocusedTaskId(): Int {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
return -1
|
||||
}
|
||||
try {
|
||||
val atmClass = Class.forName("android.app.ActivityTaskManager")
|
||||
val getServiceMethod = atmClass.getDeclaredMethod("getService")
|
||||
val atmService = getServiceMethod.invoke(null)
|
||||
val getFocusedRootTaskInfoMethod = atmService.javaClass.getMethod("getFocusedRootTaskInfo")
|
||||
val taskInfo = getFocusedRootTaskInfoMethod.invoke(atmService)
|
||||
if (taskInfo != null) {
|
||||
try {
|
||||
val taskIdField = taskInfo.javaClass.getField("taskId")
|
||||
return taskIdField.getInt(taskInfo)
|
||||
} catch (nsfe: NoSuchFieldException) {
|
||||
try {
|
||||
val taskIdField = taskInfo.javaClass.getField("mTaskId")
|
||||
return taskIdField.getInt(taskInfo)
|
||||
} catch (nsfe2: NoSuchFieldException) {
|
||||
// Both field names failed
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Ignore reflection errors
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun reinitCallback() {
|
||||
stop()
|
||||
handler.postDelayed({
|
||||
start()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
354
src/com/android/gamebar/GameBarFragment.kt
Normal file
354
src/com/android/gamebar/GameBarFragment.kt
Normal file
@@ -0,0 +1,354 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.android.gamebar.utils.PartsCustomSeekBarPreference
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import com.android.settingslib.widget.MainSwitchPreference
|
||||
import com.android.gamebar.R
|
||||
|
||||
class GameBarFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private var gameBar: GameBar? = null
|
||||
private var masterSwitch: MainSwitchPreference? = null
|
||||
private var autoEnableSwitch: SwitchPreferenceCompat? = null
|
||||
private var fpsSwitch: SwitchPreferenceCompat? = null
|
||||
private var frameTimeSwitch: SwitchPreferenceCompat? = null
|
||||
private var batteryTempSwitch: SwitchPreferenceCompat? = null
|
||||
private var cpuUsageSwitch: SwitchPreferenceCompat? = null
|
||||
private var cpuClockSwitch: SwitchPreferenceCompat? = null
|
||||
private var cpuTempSwitch: SwitchPreferenceCompat? = null
|
||||
private var ramSwitch: SwitchPreferenceCompat? = null
|
||||
private var gpuUsageSwitch: SwitchPreferenceCompat? = null
|
||||
private var gpuClockSwitch: SwitchPreferenceCompat? = null
|
||||
private var gpuTempSwitch: SwitchPreferenceCompat? = null
|
||||
private var doubleTapCapturePref: SwitchPreferenceCompat? = null
|
||||
private var singleTapTogglePref: SwitchPreferenceCompat? = null
|
||||
private var longPressEnablePref: SwitchPreferenceCompat? = null
|
||||
private var longPressTimeoutPref: ListPreference? = null
|
||||
private var textSizePref: PartsCustomSeekBarPreference? = null
|
||||
private var bgAlphaPref: PartsCustomSeekBarPreference? = null
|
||||
private var cornerRadiusPref: PartsCustomSeekBarPreference? = null
|
||||
private var paddingPref: PartsCustomSeekBarPreference? = null
|
||||
private var itemSpacingPref: PartsCustomSeekBarPreference? = null
|
||||
private var updateIntervalPref: ListPreference? = null
|
||||
private var textColorPref: ListPreference? = null
|
||||
private var titleColorPref: ListPreference? = null
|
||||
private var valueColorPref: ListPreference? = null
|
||||
private var positionPref: ListPreference? = null
|
||||
private var splitModePref: ListPreference? = null
|
||||
private var overlayFormatPref: ListPreference? = null
|
||||
private var ramSpeedSwitch: SwitchPreferenceCompat? = null
|
||||
private var ramTempSwitch: SwitchPreferenceCompat? = null
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.game_bar_preferences, rootKey)
|
||||
|
||||
gameBar = GameBar.getInstance(requireContext())
|
||||
|
||||
// Initialize all preferences
|
||||
masterSwitch = findPreference("game_bar_enable")
|
||||
autoEnableSwitch = findPreference("game_bar_auto_enable")
|
||||
fpsSwitch = findPreference("game_bar_fps_enable")
|
||||
frameTimeSwitch = findPreference("game_bar_frame_time_enable")
|
||||
batteryTempSwitch = findPreference("game_bar_temp_enable")
|
||||
cpuUsageSwitch = findPreference("game_bar_cpu_usage_enable")
|
||||
cpuClockSwitch = findPreference("game_bar_cpu_clock_enable")
|
||||
cpuTempSwitch = findPreference("game_bar_cpu_temp_enable")
|
||||
ramSwitch = findPreference("game_bar_ram_enable")
|
||||
gpuUsageSwitch = findPreference("game_bar_gpu_usage_enable")
|
||||
gpuClockSwitch = findPreference("game_bar_gpu_clock_enable")
|
||||
gpuTempSwitch = findPreference("game_bar_gpu_temp_enable")
|
||||
ramSpeedSwitch = findPreference("game_bar_ram_speed_enable")
|
||||
ramTempSwitch = findPreference("game_bar_ram_temp_enable")
|
||||
|
||||
doubleTapCapturePref = findPreference("game_bar_doubletap_capture")
|
||||
singleTapTogglePref = findPreference("game_bar_single_tap_toggle")
|
||||
longPressEnablePref = findPreference("game_bar_longpress_enable")
|
||||
longPressTimeoutPref = findPreference("game_bar_longpress_timeout")
|
||||
|
||||
textSizePref = findPreference("game_bar_text_size")
|
||||
bgAlphaPref = findPreference("game_bar_background_alpha")
|
||||
cornerRadiusPref = findPreference("game_bar_corner_radius")
|
||||
paddingPref = findPreference("game_bar_padding")
|
||||
itemSpacingPref = findPreference("game_bar_item_spacing")
|
||||
|
||||
updateIntervalPref = findPreference("game_bar_update_interval")
|
||||
textColorPref = findPreference("game_bar_text_color")
|
||||
titleColorPref = findPreference("game_bar_title_color")
|
||||
valueColorPref = findPreference("game_bar_value_color")
|
||||
positionPref = findPreference("game_bar_position")
|
||||
splitModePref = findPreference("game_bar_split_mode")
|
||||
overlayFormatPref = findPreference("game_bar_format")
|
||||
|
||||
setupPerAppConfig()
|
||||
setupMasterSwitchListener()
|
||||
setupAutoEnableSwitchListener()
|
||||
setupFeatureSwitchListeners()
|
||||
setupGesturePrefListeners()
|
||||
setupStylePrefListeners()
|
||||
setupExpandableCategories()
|
||||
}
|
||||
|
||||
private fun setupExpandableCategories() {
|
||||
// Get all expandable categories by their keys
|
||||
val overlayFeaturesCategory: ExpandablePreferenceCategory? = findPreference("category_overlay_features")
|
||||
val fpsMethodCategory: ExpandablePreferenceCategory? = findPreference("category_fps_method")
|
||||
val customizationCategory: ExpandablePreferenceCategory? = findPreference("category_customization")
|
||||
val splitConfigCategory: ExpandablePreferenceCategory? = findPreference("category_split_config")
|
||||
val gestureControlsCategory: ExpandablePreferenceCategory? = findPreference("category_gesture_controls")
|
||||
val perAppCategory: ExpandablePreferenceCategory? = findPreference("category_per_app")
|
||||
|
||||
// Categories are collapsed by default
|
||||
// You can expand some by default if needed, e.g.:
|
||||
// overlayFeaturesCategory?.setExpanded(true)
|
||||
}
|
||||
|
||||
private fun setupPerAppConfig() {
|
||||
val perAppConfigPref: Preference? = findPreference("game_bar_per_app_config")
|
||||
perAppConfigPref?.setOnPreferenceClickListener {
|
||||
startActivity(Intent(requireContext(), GameBarPerAppConfigActivity::class.java))
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMasterSwitchListener() {
|
||||
masterSwitch?.setOnPreferenceChangeListener { _, newValue ->
|
||||
val enabled = newValue as Boolean
|
||||
if (enabled) {
|
||||
if (Settings.canDrawOverlays(requireContext())) {
|
||||
android.util.Log.d("GameBarFragment", "Enabling GameBar from settings")
|
||||
|
||||
// Ensure we have a fresh GameBar instance
|
||||
if (!GameBar.isInstanceCreated()) {
|
||||
gameBar = GameBar.getInstance(requireContext())
|
||||
}
|
||||
|
||||
// Clean up any existing instance before creating new one
|
||||
gameBar?.hide()
|
||||
gameBar?.applyPreferences()
|
||||
gameBar?.show()
|
||||
requireContext().startService(Intent(requireContext(), GameBarMonitorService::class.java))
|
||||
true
|
||||
} else {
|
||||
Toast.makeText(requireContext(), R.string.overlay_permission_required, Toast.LENGTH_SHORT).show()
|
||||
false
|
||||
}
|
||||
} else {
|
||||
android.util.Log.d("GameBarFragment", "Disabling GameBar from settings")
|
||||
|
||||
gameBar?.hide()
|
||||
|
||||
// Destroy singleton to ensure clean state
|
||||
GameBar.destroyInstance()
|
||||
|
||||
// Only stop monitor service if auto-enable is also disabled
|
||||
if (autoEnableSwitch?.isChecked != true) {
|
||||
requireContext().stopService(Intent(requireContext(), GameBarMonitorService::class.java))
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAutoEnableSwitchListener() {
|
||||
autoEnableSwitch?.setOnPreferenceChangeListener { _, newValue ->
|
||||
val autoEnabled = newValue as Boolean
|
||||
if (autoEnabled) {
|
||||
requireContext().startService(Intent(requireContext(), GameBarMonitorService::class.java))
|
||||
} else {
|
||||
if (masterSwitch?.isChecked != true) {
|
||||
requireContext().stopService(Intent(requireContext(), GameBarMonitorService::class.java))
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupFeatureSwitchListeners() {
|
||||
fpsSwitch?.setOnPreferenceChangeListener { _, newValue ->
|
||||
gameBar?.setShowFps(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
frameTimeSwitch?.setOnPreferenceChangeListener { _, newValue ->
|
||||
gameBar?.setShowFrameTime(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
batteryTempSwitch?.setOnPreferenceChangeListener { _, newValue ->
|
||||
gameBar?.setShowBatteryTemp(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
cpuUsageSwitch?.setOnPreferenceChangeListener { _, newValue ->
|
||||
gameBar?.setShowCpuUsage(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
cpuClockSwitch?.setOnPreferenceChangeListener { _, newValue ->
|
||||
gameBar?.setShowCpuClock(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
cpuTempSwitch?.setOnPreferenceChangeListener { _, newValue ->
|
||||
gameBar?.setShowCpuTemp(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
ramSwitch?.setOnPreferenceChangeListener { _, newValue ->
|
||||
gameBar?.setShowRam(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
gpuUsageSwitch?.setOnPreferenceChangeListener { _, newValue ->
|
||||
gameBar?.setShowGpuUsage(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
gpuClockSwitch?.setOnPreferenceChangeListener { _, newValue ->
|
||||
gameBar?.setShowGpuClock(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
gpuTempSwitch?.setOnPreferenceChangeListener { _, newValue ->
|
||||
gameBar?.setShowGpuTemp(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
ramSpeedSwitch?.setOnPreferenceChangeListener { _, newValue ->
|
||||
gameBar?.setShowRamSpeed(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
ramTempSwitch?.setOnPreferenceChangeListener { _, newValue ->
|
||||
gameBar?.setShowRamTemp(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupGesturePrefListeners() {
|
||||
doubleTapCapturePref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
gameBar?.setDoubleTapCaptureEnabled(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
singleTapTogglePref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
gameBar?.setSingleTapToggleEnabled(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
longPressEnablePref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
gameBar?.setLongPressEnabled(newValue as Boolean)
|
||||
true
|
||||
}
|
||||
longPressTimeoutPref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue is String) {
|
||||
val ms = newValue.toLong()
|
||||
gameBar?.setLongPressThresholdMs(ms)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupStylePrefListeners() {
|
||||
textSizePref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue is Int) {
|
||||
gameBar?.updateTextSize(newValue)
|
||||
}
|
||||
true
|
||||
}
|
||||
bgAlphaPref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue is Int) {
|
||||
gameBar?.updateBackgroundAlpha(newValue)
|
||||
}
|
||||
true
|
||||
}
|
||||
cornerRadiusPref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue is Int) {
|
||||
gameBar?.updateCornerRadius(newValue)
|
||||
}
|
||||
true
|
||||
}
|
||||
paddingPref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue is Int) {
|
||||
gameBar?.updatePadding(newValue)
|
||||
}
|
||||
true
|
||||
}
|
||||
itemSpacingPref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue is Int) {
|
||||
gameBar?.updateItemSpacing(newValue)
|
||||
}
|
||||
true
|
||||
}
|
||||
updateIntervalPref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue is String) {
|
||||
gameBar?.updateUpdateInterval(newValue)
|
||||
}
|
||||
true
|
||||
}
|
||||
textColorPref?.setOnPreferenceChangeListener { _, _ ->
|
||||
true
|
||||
}
|
||||
titleColorPref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue is String) {
|
||||
gameBar?.updateTitleColor(newValue)
|
||||
}
|
||||
true
|
||||
}
|
||||
valueColorPref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue is String) {
|
||||
gameBar?.updateValueColor(newValue)
|
||||
}
|
||||
true
|
||||
}
|
||||
positionPref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue is String) {
|
||||
gameBar?.updatePosition(newValue)
|
||||
}
|
||||
true
|
||||
}
|
||||
splitModePref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue is String) {
|
||||
gameBar?.updateSplitMode(newValue)
|
||||
}
|
||||
true
|
||||
}
|
||||
overlayFormatPref?.setOnPreferenceChangeListener { _, newValue ->
|
||||
if (newValue is String) {
|
||||
gameBar?.updateOverlayFormat(newValue)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (!hasUsageStatsPermission(requireContext())) {
|
||||
requestUsageStatsPermission()
|
||||
}
|
||||
requireContext().let { context ->
|
||||
if ((masterSwitch?.isChecked == true) || (autoEnableSwitch?.isChecked == true)) {
|
||||
context.startService(Intent(context, GameBarMonitorService::class.java))
|
||||
} else {
|
||||
context.stopService(Intent(context, GameBarMonitorService::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasUsageStatsPermission(context: Context): Boolean {
|
||||
val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as? android.app.AppOpsManager
|
||||
?: return false
|
||||
val mode = appOps.checkOpNoThrow(
|
||||
android.app.AppOpsManager.OPSTR_GET_USAGE_STATS,
|
||||
android.os.Process.myUid(),
|
||||
context.packageName
|
||||
)
|
||||
return mode == android.app.AppOpsManager.MODE_ALLOWED
|
||||
}
|
||||
|
||||
private fun requestUsageStatsPermission() {
|
||||
val intent = Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
61
src/com/android/gamebar/GameBarGpuInfo.kt
Normal file
61
src/com/android/gamebar/GameBarGpuInfo.kt
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import java.io.BufferedReader
|
||||
import java.io.FileReader
|
||||
import java.io.IOException
|
||||
|
||||
object GameBarGpuInfo {
|
||||
|
||||
private const val GPU_USAGE_PATH = "/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage"
|
||||
private const val GPU_CLOCK_PATH = "/sys/class/kgsl/kgsl-3d0/gpuclk"
|
||||
private const val GPU_TEMP_PATH = "/sys/class/kgsl/kgsl-3d0/temp"
|
||||
|
||||
fun getGpuUsage(): String {
|
||||
val line = readLine(GPU_USAGE_PATH) ?: return "N/A"
|
||||
val cleanLine = line.replace("%", "").trim()
|
||||
return try {
|
||||
val value = cleanLine.toInt()
|
||||
value.toString()
|
||||
} catch (e: NumberFormatException) {
|
||||
"N/A"
|
||||
}
|
||||
}
|
||||
|
||||
fun getGpuClock(): String {
|
||||
val line = readLine(GPU_CLOCK_PATH) ?: return "N/A"
|
||||
val cleanLine = line.trim()
|
||||
return try {
|
||||
val hz = cleanLine.toLong()
|
||||
val mhz = hz / 1_000_000
|
||||
mhz.toString()
|
||||
} catch (e: NumberFormatException) {
|
||||
"N/A"
|
||||
}
|
||||
}
|
||||
|
||||
fun getGpuTemp(): String {
|
||||
val line = readLine(GPU_TEMP_PATH) ?: return "N/A"
|
||||
val cleanLine = line.trim()
|
||||
return try {
|
||||
val raw = cleanLine.toFloat()
|
||||
val celsius = raw / 1000f
|
||||
String.format("%.1f", celsius)
|
||||
} catch (e: NumberFormatException) {
|
||||
"N/A"
|
||||
}
|
||||
}
|
||||
|
||||
private fun readLine(path: String): String? {
|
||||
return try {
|
||||
BufferedReader(FileReader(path)).use { it.readLine() }
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/com/android/gamebar/GameBarLogActivity.kt
Normal file
98
src/com/android/gamebar/GameBarLogActivity.kt
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.CheckBox
|
||||
import android.widget.LinearLayout
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity
|
||||
import com.android.gamebar.R
|
||||
|
||||
class GameBarLogActivity : CollapsingToolbarBaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_gamebar_log)
|
||||
title = "GameBar Log Monitor"
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.content_frame, GameBarLogFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.gamebar_log_menu, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_logging_parameters -> {
|
||||
showLoggingParametersDialog()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLoggingParametersDialog() {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
// Create checkboxes for each parameter
|
||||
val container = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(48, 24, 48, 24)
|
||||
}
|
||||
|
||||
val checkboxes = mutableMapOf<String, CheckBox>()
|
||||
|
||||
val parameters = listOf(
|
||||
Pair(GameBarLogFragment.PREF_LOG_FPS, "FPS"),
|
||||
Pair(GameBarLogFragment.PREF_LOG_FRAME_TIME, "Frame Time"),
|
||||
Pair(GameBarLogFragment.PREF_LOG_BATTERY_TEMP, "Battery Temperature"),
|
||||
Pair(GameBarLogFragment.PREF_LOG_CPU_USAGE, "CPU Usage"),
|
||||
Pair(GameBarLogFragment.PREF_LOG_CPU_CLOCK, "CPU Clock"),
|
||||
Pair(GameBarLogFragment.PREF_LOG_CPU_TEMP, "CPU Temperature"),
|
||||
Pair(GameBarLogFragment.PREF_LOG_RAM, "RAM Usage"),
|
||||
Pair(GameBarLogFragment.PREF_LOG_RAM_SPEED, "RAM Frequency"),
|
||||
Pair(GameBarLogFragment.PREF_LOG_RAM_TEMP, "RAM Temperature"),
|
||||
Pair(GameBarLogFragment.PREF_LOG_GPU_USAGE, "GPU Usage"),
|
||||
Pair(GameBarLogFragment.PREF_LOG_GPU_CLOCK, "GPU Clock"),
|
||||
Pair(GameBarLogFragment.PREF_LOG_GPU_TEMP, "GPU Temperature")
|
||||
)
|
||||
|
||||
parameters.forEach { (key, label) ->
|
||||
val cb = CheckBox(this).apply {
|
||||
text = label
|
||||
isChecked = prefs.getBoolean(key, true)
|
||||
setPadding(16, 16, 16, 16)
|
||||
}
|
||||
checkboxes[key] = cb
|
||||
container.addView(cb)
|
||||
}
|
||||
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle("Logging Parameters")
|
||||
.setMessage("Select which parameters to include in logs:")
|
||||
.setView(container)
|
||||
.setPositiveButton("Apply") { _, _ ->
|
||||
val editor = prefs.edit()
|
||||
checkboxes.forEach { (key, checkbox) ->
|
||||
editor.putBoolean(key, checkbox.isChecked)
|
||||
}
|
||||
editor.apply()
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
644
src/com/android/gamebar/GameBarLogFragment.kt
Normal file
644
src/com/android/gamebar/GameBarLogFragment.kt
Normal file
@@ -0,0 +1,644 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.RadioButton
|
||||
import android.widget.RadioGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.gamebar.R
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class GameBarLogFragment : Fragment(), GameDataExport.CaptureStateListener, PerAppLogManager.PerAppStateListener {
|
||||
|
||||
private lateinit var searchBar: EditText
|
||||
private lateinit var startCaptureButton: Button
|
||||
private lateinit var stopCaptureButton: Button
|
||||
private lateinit var manualLogsButton: Button
|
||||
private lateinit var logTypeRadioGroup: RadioGroup
|
||||
private lateinit var globalLoggingRadio: RadioButton
|
||||
private lateinit var perAppLoggingRadio: RadioButton
|
||||
private lateinit var logHistoryRecyclerView: RecyclerView
|
||||
private lateinit var logHistoryAdapter: LogHistoryAdapter
|
||||
private lateinit var perAppLogAdapter: PerAppLogAdapter
|
||||
|
||||
private val gameDataExport = GameDataExport.getInstance()
|
||||
private val perAppLogManager = PerAppLogManager.getInstance()
|
||||
private val logFiles = mutableListOf<LogFile>()
|
||||
private val allLogFiles = mutableListOf<LogFile>()
|
||||
private val installedApps = mutableListOf<ApplicationInfo>()
|
||||
private val filteredApps = mutableListOf<ApplicationInfo>()
|
||||
|
||||
private var currentLoggingMode = GameDataExport.LoggingMode.GLOBAL
|
||||
|
||||
companion object {
|
||||
private const val PREF_LOGGING_MODE = "gamebar_logging_mode"
|
||||
|
||||
// Logging parameter preference keys
|
||||
const val PREF_LOG_FPS = "gamebar_log_fps"
|
||||
const val PREF_LOG_FRAME_TIME = "gamebar_log_frame_time"
|
||||
const val PREF_LOG_BATTERY_TEMP = "gamebar_log_battery_temp"
|
||||
const val PREF_LOG_CPU_USAGE = "gamebar_log_cpu_usage"
|
||||
const val PREF_LOG_CPU_CLOCK = "gamebar_log_cpu_clock"
|
||||
const val PREF_LOG_CPU_TEMP = "gamebar_log_cpu_temp"
|
||||
const val PREF_LOG_RAM = "gamebar_log_ram"
|
||||
const val PREF_LOG_RAM_SPEED = "gamebar_log_ram_speed"
|
||||
const val PREF_LOG_RAM_TEMP = "gamebar_log_ram_temp"
|
||||
const val PREF_LOG_GPU_USAGE = "gamebar_log_gpu_usage"
|
||||
const val PREF_LOG_GPU_CLOCK = "gamebar_log_gpu_clock"
|
||||
const val PREF_LOG_GPU_TEMP = "gamebar_log_gpu_temp"
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_gamebar_log, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
try {
|
||||
initViews(view)
|
||||
loadSavedLoggingMode() // Load saved mode first
|
||||
setupRecyclerView()
|
||||
setupButtonListeners()
|
||||
loadInstalledApps()
|
||||
initializeUIState() // Initialize UI based on saved mode
|
||||
updateButtonStates()
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "Error initializing log monitor: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initViews(view: View) {
|
||||
searchBar = view.findViewById(R.id.search_bar)
|
||||
startCaptureButton = view.findViewById(R.id.btn_start_capture)
|
||||
stopCaptureButton = view.findViewById(R.id.btn_stop_capture)
|
||||
manualLogsButton = view.findViewById(R.id.btn_manual_logs)
|
||||
logTypeRadioGroup = view.findViewById(R.id.rg_log_type)
|
||||
globalLoggingRadio = view.findViewById(R.id.rb_global_logging)
|
||||
perAppLoggingRadio = view.findViewById(R.id.rb_per_app_logging)
|
||||
logHistoryRecyclerView = view.findViewById(R.id.rv_log_history)
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
// Setup global log history adapter
|
||||
logHistoryAdapter = LogHistoryAdapter(logFiles) { logFile, view ->
|
||||
showLogFilePopupMenu(logFile, view)
|
||||
}
|
||||
|
||||
// Setup per-app log adapter
|
||||
perAppLogAdapter = PerAppLogAdapter(
|
||||
requireContext(),
|
||||
filteredApps,
|
||||
onSwitchChanged = { packageName, enabled ->
|
||||
perAppLogManager.setAppLoggingEnabled(requireContext(), packageName, enabled)
|
||||
updatePerAppAdapterStates()
|
||||
},
|
||||
onViewLogsClicked = { packageName, appName ->
|
||||
val intent = Intent(requireContext(), PerAppLogViewActivity::class.java).apply {
|
||||
putExtra(PerAppLogViewActivity.EXTRA_PACKAGE_NAME, packageName)
|
||||
putExtra(PerAppLogViewActivity.EXTRA_APP_NAME, appName)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
)
|
||||
|
||||
logHistoryRecyclerView.apply {
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
adapter = logHistoryAdapter // Start with global adapter
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupButtonListeners() {
|
||||
startCaptureButton.setOnClickListener {
|
||||
gameDataExport.startCapture()
|
||||
|
||||
if (currentLoggingMode == GameDataExport.LoggingMode.PER_APP) {
|
||||
// Start dedicated PerAppLogService for logging-specific foreground monitoring
|
||||
requireContext().startService(Intent(requireContext(), PerAppLogService::class.java))
|
||||
}
|
||||
|
||||
val message = if (currentLoggingMode == GameDataExport.LoggingMode.GLOBAL) {
|
||||
"Started global logging"
|
||||
} else {
|
||||
"Per-app logging enabled - logs will start automatically when enabled apps become active"
|
||||
}
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||
updateButtonStates()
|
||||
}
|
||||
|
||||
stopCaptureButton.setOnClickListener {
|
||||
if (gameDataExport.isCapturing()) {
|
||||
gameDataExport.stopCapture()
|
||||
if (currentLoggingMode == GameDataExport.LoggingMode.GLOBAL) {
|
||||
gameDataExport.exportDataToCsv()
|
||||
Toast.makeText(requireContext(), "Stopped logging and exported data", Toast.LENGTH_SHORT).show()
|
||||
loadLogHistory()
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Stopped per-app logging", Toast.LENGTH_SHORT).show()
|
||||
updatePerAppAdapterStates()
|
||||
}
|
||||
}
|
||||
updateButtonStates()
|
||||
}
|
||||
|
||||
manualLogsButton.setOnClickListener {
|
||||
showManualLogsDialog()
|
||||
}
|
||||
|
||||
logTypeRadioGroup.setOnCheckedChangeListener { _, checkedId ->
|
||||
when (checkedId) {
|
||||
R.id.rb_global_logging -> switchToGlobalMode()
|
||||
R.id.rb_per_app_logging -> switchToPerAppMode()
|
||||
}
|
||||
}
|
||||
|
||||
searchBar.addTextChangedListener(object : android.text.TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
when (currentLoggingMode) {
|
||||
GameDataExport.LoggingMode.GLOBAL -> filterLogs(s.toString().lowercase())
|
||||
GameDataExport.LoggingMode.PER_APP -> filterApps(s.toString().lowercase())
|
||||
}
|
||||
}
|
||||
override fun afterTextChanged(s: android.text.Editable?) {}
|
||||
})
|
||||
}
|
||||
|
||||
private fun filterLogs(query: String) {
|
||||
logFiles.clear()
|
||||
if (query.isEmpty()) {
|
||||
logFiles.addAll(allLogFiles)
|
||||
} else {
|
||||
allLogFiles.forEach { logFile ->
|
||||
if (logFile.name.lowercase().contains(query) ||
|
||||
logFile.lastModified.lowercase().contains(query)) {
|
||||
logFiles.add(logFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
logHistoryAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun updateButtonStates() {
|
||||
val isCapturing = gameDataExport.isCapturing()
|
||||
|
||||
when (currentLoggingMode) {
|
||||
GameDataExport.LoggingMode.GLOBAL -> {
|
||||
startCaptureButton.isEnabled = !isCapturing
|
||||
stopCaptureButton.isEnabled = isCapturing
|
||||
manualLogsButton.visibility = View.GONE
|
||||
|
||||
if (isCapturing) {
|
||||
startCaptureButton.text = "Capturing..."
|
||||
} else {
|
||||
startCaptureButton.text = "Start Capture"
|
||||
}
|
||||
}
|
||||
GameDataExport.LoggingMode.PER_APP -> {
|
||||
val hasEnabledApps = perAppLogManager.getEnabledApps(requireContext()).isNotEmpty()
|
||||
val hasManualLogs = getManualLogPackages().isNotEmpty()
|
||||
|
||||
startCaptureButton.isEnabled = !isCapturing && hasEnabledApps
|
||||
stopCaptureButton.isEnabled = isCapturing
|
||||
manualLogsButton.visibility = if (hasManualLogs) View.VISIBLE else View.GONE
|
||||
|
||||
if (isCapturing) {
|
||||
startCaptureButton.text = "Per-app Logging Active"
|
||||
} else {
|
||||
startCaptureButton.text = if (hasEnabledApps) "Enable Per-app Logging" else "No Apps Enabled"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getManualLogPackages(): List<String> {
|
||||
val allLogFiles = perAppLogManager.getAllPerAppLogFiles()
|
||||
val enabledApps = perAppLogManager.getEnabledApps(requireContext())
|
||||
|
||||
// Return packages that have log files but aren't in the enabled apps list
|
||||
return allLogFiles.keys.filter { packageName ->
|
||||
!enabledApps.contains(packageName) && packageName != "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private fun showManualLogsDialog() {
|
||||
val manualLogPackages = getManualLogPackages()
|
||||
|
||||
if (manualLogPackages.isEmpty()) {
|
||||
Toast.makeText(requireContext(), "No manual logs found", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val pm = requireContext().packageManager
|
||||
val items = manualLogPackages.map { packageName ->
|
||||
try {
|
||||
val appInfo = pm.getApplicationInfo(packageName, 0)
|
||||
pm.getApplicationLabel(appInfo).toString()
|
||||
} catch (e: Exception) {
|
||||
packageName
|
||||
}
|
||||
}.toTypedArray()
|
||||
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle("Manual Logs")
|
||||
.setItems(items) { _, which ->
|
||||
val packageName = manualLogPackages[which]
|
||||
val appName = items[which]
|
||||
|
||||
// Open log view for this package
|
||||
val intent = Intent(requireContext(), PerAppLogViewActivity::class.java).apply {
|
||||
putExtra(PerAppLogViewActivity.EXTRA_PACKAGE_NAME, packageName)
|
||||
putExtra(PerAppLogViewActivity.EXTRA_APP_NAME, appName)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
.setNegativeButton("Close", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun loadLogHistory() {
|
||||
logFiles.clear()
|
||||
allLogFiles.clear()
|
||||
|
||||
val externalStorageDir = Environment.getExternalStorageDirectory()
|
||||
val files = externalStorageDir.listFiles { file ->
|
||||
file.name.startsWith("GameBar_log_") && file.name.endsWith(".csv")
|
||||
}
|
||||
|
||||
files?.let { fileArray ->
|
||||
fileArray.sortByDescending { it.lastModified() }
|
||||
for (file in fileArray) {
|
||||
val logFile = LogFile(
|
||||
name = file.name,
|
||||
path = file.absolutePath,
|
||||
size = formatFileSize(file.length()),
|
||||
lastModified = formatDate(file.lastModified())
|
||||
)
|
||||
allLogFiles.add(logFile)
|
||||
logFiles.add(logFile)
|
||||
}
|
||||
}
|
||||
|
||||
logHistoryAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun showLogFilePopupMenu(logFile: LogFile, anchorView: View) {
|
||||
val popupMenu = PopupMenu(requireContext(), anchorView)
|
||||
popupMenu.menuInflater.inflate(R.menu.log_file_popup_menu, popupMenu.menu)
|
||||
|
||||
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.menu_open -> {
|
||||
openLogFile(logFile)
|
||||
true
|
||||
}
|
||||
R.id.menu_share -> {
|
||||
shareLogFile(logFile)
|
||||
true
|
||||
}
|
||||
R.id.menu_export -> {
|
||||
exportLogFile(logFile)
|
||||
true
|
||||
}
|
||||
R.id.menu_delete -> {
|
||||
deleteLogFile(logFile)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
popupMenu.show()
|
||||
}
|
||||
|
||||
private fun openLogFile(logFile: LogFile) {
|
||||
// Show analytics dialog for log file
|
||||
showLogAnalyticsDialog(logFile)
|
||||
}
|
||||
|
||||
private fun showLogAnalyticsDialog(logFile: LogFile) {
|
||||
// Show loading message
|
||||
val loadingDialog = AlertDialog.Builder(requireContext())
|
||||
.setTitle("Analyzing Log...")
|
||||
.setMessage("Please wait while we analyze the session data.")
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
loadingDialog.show()
|
||||
|
||||
// Analyze log file in background thread
|
||||
Thread {
|
||||
val logReader = PerAppLogReader()
|
||||
val analytics = logReader.analyzeLogFile(logFile.path)
|
||||
|
||||
// Update UI on main thread
|
||||
activity?.runOnUiThread {
|
||||
loadingDialog.dismiss()
|
||||
|
||||
if (analytics != null) {
|
||||
showAnalyticsInDialog(logFile, analytics)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Failed to analyze log file", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun showAnalyticsInDialog(logFile: LogFile, analytics: LogAnalytics) {
|
||||
// Open LogAnalyticsActivity with the file
|
||||
val intent = Intent(requireContext(), LogAnalyticsActivity::class.java).apply {
|
||||
putExtra(LogAnalyticsActivity.EXTRA_LOG_FILE_PATH, logFile.path)
|
||||
putExtra(LogAnalyticsActivity.EXTRA_LOG_FILE_NAME, logFile.name)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun shareLogFile(logFile: LogFile) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
val file = File(logFile.path)
|
||||
|
||||
// Use FileProvider to create content:// URI
|
||||
val uri = FileProvider.getUriForFile(
|
||||
requireContext(),
|
||||
"${requireContext().packageName}.fileprovider",
|
||||
file
|
||||
)
|
||||
|
||||
intent.type = "text/csv"
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, "GameBar Performance Log")
|
||||
intent.putExtra(Intent.EXTRA_TEXT, "GameBar performance log file: ${logFile.name}")
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
val chooser = Intent.createChooser(intent, "Share log file")
|
||||
startActivity(chooser)
|
||||
} catch (e: Exception) {
|
||||
// Fallback with generic type if CSV doesn't work
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
val file = File(logFile.path)
|
||||
|
||||
val uri = FileProvider.getUriForFile(
|
||||
requireContext(),
|
||||
"${requireContext().packageName}.fileprovider",
|
||||
file
|
||||
)
|
||||
|
||||
intent.type = "text/plain"
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, "GameBar Performance Log")
|
||||
intent.putExtra(Intent.EXTRA_TEXT, "GameBar performance log file: ${logFile.name}")
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
val chooser = Intent.createChooser(intent, "Share log file")
|
||||
startActivity(chooser)
|
||||
} catch (e2: Exception) {
|
||||
Toast.makeText(requireContext(), "File location: ${logFile.path}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportLogFile(logFile: LogFile) {
|
||||
Toast.makeText(requireContext(), "File saved at: ${logFile.path}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun deleteLogFile(logFile: LogFile) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle("Delete Log File")
|
||||
.setMessage("Are you sure you want to delete this log file?\n\n${logFile.name}")
|
||||
.setPositiveButton("Delete") { _, _ ->
|
||||
try {
|
||||
val file = File(logFile.path)
|
||||
if (file.delete()) {
|
||||
Toast.makeText(requireContext(), "Log file deleted", Toast.LENGTH_SHORT).show()
|
||||
loadLogHistory() // Refresh the list
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Failed to delete log file", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "Error deleting file: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun formatFileSize(bytes: Long): String {
|
||||
val kb = bytes / 1024.0
|
||||
val mb = kb / 1024.0
|
||||
|
||||
return when {
|
||||
mb >= 1 -> String.format("%.1f MB", mb)
|
||||
kb >= 1 -> String.format("%.1f KB", kb)
|
||||
else -> "$bytes bytes"
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDate(timestamp: Long): String {
|
||||
val dateFormat = SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault())
|
||||
return dateFormat.format(Date(timestamp))
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
gameDataExport.setCaptureStateListener(this)
|
||||
perAppLogManager.setPerAppStateListener(this)
|
||||
loadLogHistory()
|
||||
if (currentLoggingMode == GameDataExport.LoggingMode.PER_APP) {
|
||||
updatePerAppAdapterStates()
|
||||
}
|
||||
// Update button states last to ensure manual logs button visibility is correct
|
||||
updateButtonStates()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
gameDataExport.setCaptureStateListener(null)
|
||||
perAppLogManager.setPerAppStateListener(null)
|
||||
}
|
||||
|
||||
// CaptureStateListener implementation
|
||||
override fun onCaptureStarted() {
|
||||
activity?.runOnUiThread {
|
||||
updateButtonStates()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCaptureStopped() {
|
||||
activity?.runOnUiThread {
|
||||
updateButtonStates()
|
||||
// Small delay to ensure file is written before refreshing
|
||||
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
||||
if (currentLoggingMode == GameDataExport.LoggingMode.GLOBAL) {
|
||||
loadLogHistory()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
// PerAppStateListener implementation
|
||||
override fun onAppLoggingStarted(packageName: String) {
|
||||
activity?.runOnUiThread {
|
||||
updatePerAppAdapterStates()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAppLoggingStopped(packageName: String) {
|
||||
activity?.runOnUiThread {
|
||||
updatePerAppAdapterStates()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadInstalledApps() {
|
||||
try {
|
||||
val pm = requireContext().packageManager
|
||||
val apps = pm.getInstalledApplications(PackageManager.GET_META_DATA)
|
||||
|
||||
installedApps.clear()
|
||||
installedApps.addAll(apps.filter { app ->
|
||||
// Filter out system apps and our own app
|
||||
(app.flags and ApplicationInfo.FLAG_SYSTEM) == 0 &&
|
||||
app.packageName != requireContext().packageName
|
||||
}.sortedBy { app ->
|
||||
app.loadLabel(pm).toString().lowercase()
|
||||
})
|
||||
|
||||
filteredApps.clear()
|
||||
filteredApps.addAll(installedApps)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "Error loading apps: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSavedLoggingMode() {
|
||||
val prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val savedMode = prefs.getString(PREF_LOGGING_MODE, "GLOBAL")
|
||||
|
||||
currentLoggingMode = if (savedMode == "PER_APP") {
|
||||
GameDataExport.LoggingMode.PER_APP
|
||||
} else {
|
||||
GameDataExport.LoggingMode.GLOBAL
|
||||
}
|
||||
|
||||
// Set the mode in GameDataExport
|
||||
gameDataExport.setLoggingMode(currentLoggingMode)
|
||||
}
|
||||
|
||||
private fun saveLoggingMode() {
|
||||
val prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
prefs.edit().putString(PREF_LOGGING_MODE, currentLoggingMode.name).apply()
|
||||
}
|
||||
|
||||
private fun initializeUIState() {
|
||||
// Set radio button state based on loaded mode
|
||||
when (currentLoggingMode) {
|
||||
GameDataExport.LoggingMode.GLOBAL -> {
|
||||
globalLoggingRadio.isChecked = true
|
||||
logHistoryRecyclerView.adapter = logHistoryAdapter
|
||||
searchBar.hint = "Search logs..."
|
||||
loadLogHistory()
|
||||
}
|
||||
GameDataExport.LoggingMode.PER_APP -> {
|
||||
perAppLoggingRadio.isChecked = true
|
||||
logHistoryRecyclerView.adapter = perAppLogAdapter
|
||||
searchBar.hint = "Search apps..."
|
||||
updatePerAppAdapterStates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun switchToGlobalMode() {
|
||||
if (currentLoggingMode == GameDataExport.LoggingMode.GLOBAL) return
|
||||
|
||||
currentLoggingMode = GameDataExport.LoggingMode.GLOBAL
|
||||
gameDataExport.setLoggingMode(GameDataExport.LoggingMode.GLOBAL)
|
||||
saveLoggingMode() // Save the mode change
|
||||
|
||||
// Switch to global log history adapter
|
||||
logHistoryRecyclerView.adapter = logHistoryAdapter
|
||||
searchBar.hint = "Search logs..."
|
||||
searchBar.setText("") // Clear search when switching modes
|
||||
|
||||
loadLogHistory()
|
||||
updateButtonStates()
|
||||
}
|
||||
|
||||
private fun switchToPerAppMode() {
|
||||
if (currentLoggingMode == GameDataExport.LoggingMode.PER_APP) return
|
||||
|
||||
currentLoggingMode = GameDataExport.LoggingMode.PER_APP
|
||||
gameDataExport.setLoggingMode(GameDataExport.LoggingMode.PER_APP)
|
||||
saveLoggingMode() // Save the mode change
|
||||
|
||||
// Switch to per-app adapter
|
||||
logHistoryRecyclerView.adapter = perAppLogAdapter
|
||||
searchBar.hint = "Search apps..."
|
||||
searchBar.setText("") // Clear search when switching modes
|
||||
|
||||
updatePerAppAdapterStates()
|
||||
updateButtonStates()
|
||||
}
|
||||
|
||||
private fun filterApps(query: String) {
|
||||
filteredApps.clear()
|
||||
if (query.isEmpty()) {
|
||||
filteredApps.addAll(installedApps)
|
||||
} else {
|
||||
installedApps.forEach { app ->
|
||||
val pm = requireContext().packageManager
|
||||
val label = app.loadLabel(pm).toString().lowercase()
|
||||
val pkg = app.packageName.lowercase()
|
||||
if (label.contains(query) || pkg.contains(query)) {
|
||||
filteredApps.add(app)
|
||||
}
|
||||
}
|
||||
}
|
||||
perAppLogAdapter.updateApps(filteredApps)
|
||||
}
|
||||
|
||||
private fun updatePerAppAdapterStates() {
|
||||
val enabledApps = perAppLogManager.getEnabledApps(requireContext())
|
||||
val currentlyLoggingApps = perAppLogManager.getCurrentlyLoggingApps()
|
||||
|
||||
perAppLogAdapter.updateEnabledApps(enabledApps)
|
||||
perAppLogAdapter.updateCurrentlyLoggingApps(currentlyLoggingApps)
|
||||
perAppLogAdapter.refreshLogFileStates() // Refresh to update document icons
|
||||
updateButtonStates() // Update button states when per-app states change
|
||||
}
|
||||
|
||||
data class LogFile(
|
||||
val name: String,
|
||||
val path: String,
|
||||
val size: String,
|
||||
val lastModified: String
|
||||
)
|
||||
}
|
||||
102
src/com/android/gamebar/GameBarMemInfo.kt
Normal file
102
src/com/android/gamebar/GameBarMemInfo.kt
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import java.io.BufferedReader
|
||||
import java.io.FileReader
|
||||
import java.io.IOException
|
||||
|
||||
object GameBarMemInfo {
|
||||
|
||||
fun getRamUsage(): String {
|
||||
var memTotal = 0L
|
||||
var memAvailable = 0L
|
||||
|
||||
try {
|
||||
BufferedReader(FileReader("/proc/meminfo")).use { br ->
|
||||
var line: String?
|
||||
while (br.readLine().also { line = it } != null) {
|
||||
line?.let {
|
||||
when {
|
||||
it.startsWith("MemTotal:") -> memTotal = parseMemValue(it)
|
||||
it.startsWith("MemAvailable:") -> memAvailable = parseMemValue(it)
|
||||
}
|
||||
if (memTotal > 0 && memAvailable > 0) {
|
||||
return@use
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
if (memTotal == 0L) {
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
val usedKb = memTotal - memAvailable
|
||||
val usedMb = usedKb / 1024
|
||||
return usedMb.toString()
|
||||
}
|
||||
|
||||
private fun parseMemValue(line: String): Long {
|
||||
val parts = line.split("\\s+".toRegex())
|
||||
if (parts.size < 3) {
|
||||
return 0L
|
||||
}
|
||||
return try {
|
||||
parts[1].toLong()
|
||||
} catch (e: NumberFormatException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
fun getRamSpeed(): String {
|
||||
val path = "/sys/devices/system/cpu/bus_dcvs/DDR/cur_freq"
|
||||
try {
|
||||
BufferedReader(FileReader(path)).use { br ->
|
||||
val line = br.readLine()
|
||||
if (!line.isNullOrEmpty()) {
|
||||
try {
|
||||
val khz = line.trim().toInt()
|
||||
val mhz = khz / 1000f
|
||||
return if (mhz >= 1000) {
|
||||
String.format("%.3f GHz", mhz / 1000f)
|
||||
} else {
|
||||
String.format("%.0f MHz", mhz)
|
||||
}
|
||||
} catch (ignored: NumberFormatException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
// ignore
|
||||
}
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
fun getRamTemp(): String {
|
||||
val path = "/sys/class/thermal/thermal_zone27/temp"
|
||||
try {
|
||||
BufferedReader(FileReader(path)).use { br ->
|
||||
val line = br.readLine()
|
||||
if (!line.isNullOrEmpty()) {
|
||||
try {
|
||||
val raw = line.trim().toInt()
|
||||
val celsius = raw / 1000f
|
||||
return String.format("%.1f°C", celsius)
|
||||
} catch (ignored: NumberFormatException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
// ignore
|
||||
}
|
||||
return "N/A"
|
||||
}
|
||||
}
|
||||
150
src/com/android/gamebar/GameBarMonitorService.kt
Normal file
150
src/com/android/gamebar/GameBarMonitorService.kt
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import androidx.preference.PreferenceManager
|
||||
|
||||
class GameBarMonitorService : Service() {
|
||||
|
||||
private var handler: Handler? = null
|
||||
private var monitorRunnable: Runnable? = null
|
||||
@Volatile
|
||||
private var isRunning = false
|
||||
|
||||
private var lastForegroundApp = ""
|
||||
private var lastGameBarState = false
|
||||
private val perAppLogManager = PerAppLogManager.getInstance()
|
||||
|
||||
companion object {
|
||||
private const val MONITOR_INTERVAL = 2000L // 2 seconds
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
handler = Handler(Looper.getMainLooper())
|
||||
monitorRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (isRunning) {
|
||||
monitorForegroundApp()
|
||||
if (isRunning && handler != null) {
|
||||
handler!!.postDelayed(this, MONITOR_INTERVAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (!isRunning) {
|
||||
isRunning = true
|
||||
handler?.let { h ->
|
||||
monitorRunnable?.let { r ->
|
||||
h.post(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun monitorForegroundApp() {
|
||||
try {
|
||||
if (!isRunning) return
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val masterEnabled = prefs.getBoolean("game_bar_enable", false)
|
||||
|
||||
if (masterEnabled) {
|
||||
if (!lastGameBarState) {
|
||||
val gameBar = GameBar.getInstance(this)
|
||||
gameBar.applyPreferences()
|
||||
gameBar.show()
|
||||
lastGameBarState = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val autoEnabled = prefs.getBoolean("game_bar_auto_enable", false)
|
||||
if (!autoEnabled) {
|
||||
if (lastGameBarState) {
|
||||
GameBar.getInstance(this).hide()
|
||||
lastGameBarState = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val foreground = ForegroundAppDetector.getForegroundPackageName(this)
|
||||
|
||||
// Only update if foreground app changed
|
||||
if (foreground != lastForegroundApp) {
|
||||
val autoApps = prefs.getStringSet(
|
||||
GameBarPerAppConfigFragment.PREF_AUTO_APPS,
|
||||
emptySet()
|
||||
) ?: emptySet()
|
||||
|
||||
val shouldShow = autoApps.contains(foreground)
|
||||
|
||||
if (shouldShow && !lastGameBarState) {
|
||||
val gameBar = GameBar.getInstance(this)
|
||||
gameBar.applyPreferences()
|
||||
gameBar.show()
|
||||
lastGameBarState = true
|
||||
} else if (!shouldShow && lastGameBarState) {
|
||||
GameBar.getInstance(this).hide()
|
||||
lastGameBarState = false
|
||||
}
|
||||
|
||||
// Handle per-app logging state changes
|
||||
if (lastForegroundApp.isNotEmpty() && lastForegroundApp != "Unknown") {
|
||||
perAppLogManager.onAppWentToBackground(this, lastForegroundApp)
|
||||
}
|
||||
|
||||
if (foreground.isNotEmpty() && foreground != "Unknown") {
|
||||
perAppLogManager.onAppBecameForeground(this, foreground)
|
||||
}
|
||||
|
||||
lastForegroundApp = foreground
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Prevent crashes from propagating
|
||||
android.util.Log.e("GameBarMonitorService", "Error in monitorForegroundApp", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
isRunning = false
|
||||
|
||||
handler?.let {
|
||||
monitorRunnable?.let { runnable ->
|
||||
it.removeCallbacks(runnable)
|
||||
}
|
||||
it.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
// Clean up GameBar instance
|
||||
try {
|
||||
GameBar.destroyInstance()
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("GameBarMonitorService", "Error destroying GameBar instance", e)
|
||||
}
|
||||
|
||||
// Clear state variables to prevent lingering references
|
||||
lastForegroundApp = ""
|
||||
lastGameBarState = false
|
||||
handler = null
|
||||
monitorRunnable = null
|
||||
}
|
||||
}
|
||||
26
src/com/android/gamebar/GameBarPerAppConfigActivity.kt
Normal file
26
src/com/android/gamebar/GameBarPerAppConfigActivity.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.os.Bundle
|
||||
import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity
|
||||
import com.android.gamebar.R
|
||||
|
||||
class GameBarPerAppConfigActivity : CollapsingToolbarBaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_game_bar_app_selector)
|
||||
title = "Configure Per-App GameBar"
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.content_frame, GameBarPerAppConfigFragment())
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
117
src/com/android/gamebar/GameBarPerAppConfigFragment.kt
Normal file
117
src/com/android/gamebar/GameBarPerAppConfigFragment.kt
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import com.android.gamebar.R
|
||||
|
||||
class GameBarPerAppConfigFragment : PreferenceFragmentCompat() {
|
||||
|
||||
companion object {
|
||||
const val PREF_AUTO_APPS = "game_bar_auto_apps"
|
||||
}
|
||||
|
||||
private var searchBar: EditText? = null
|
||||
private var category: PreferenceCategory? = null
|
||||
private var allApps: List<ApplicationInfo>? = null
|
||||
private var pm: PackageManager? = null
|
||||
private var autoApps: Set<String>? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val root = super.onCreateView(inflater, container, savedInstanceState)
|
||||
val context = requireContext()
|
||||
val layout = LinearLayout(context)
|
||||
layout.orientation = LinearLayout.VERTICAL
|
||||
searchBar = EditText(context).apply {
|
||||
id = View.generateViewId()
|
||||
hint = "Search apps..."
|
||||
inputType = android.text.InputType.TYPE_CLASS_TEXT
|
||||
setBackgroundResource(R.drawable.bg_search_rounded)
|
||||
setPadding(24, 24, 24, 24)
|
||||
setTextColor(context.getColor(R.color.app_name_text_selector))
|
||||
setHintTextColor(context.getColor(R.color.app_package_text_selector))
|
||||
}
|
||||
val params = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
val margin = (context.resources.displayMetrics.density * 16).toInt() // 16dp
|
||||
params.setMargins(margin, 0, margin, 0)
|
||||
layout.addView(searchBar, params)
|
||||
root?.let { layout.addView(it) }
|
||||
|
||||
searchBar?.addTextChangedListener(object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
populateAppList(s.toString().lowercase())
|
||||
}
|
||||
override fun afterTextChanged(s: Editable?) {}
|
||||
})
|
||||
return layout
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
preferenceScreen = preferenceManager.createPreferenceScreen(requireContext())
|
||||
category = PreferenceCategory(requireContext()).apply {
|
||||
title = "Configure Per-App GameBar"
|
||||
}
|
||||
preferenceScreen.addPreference(category!!)
|
||||
pm = requireContext().packageManager
|
||||
allApps = pm!!.getInstalledApplications(PackageManager.GET_META_DATA)
|
||||
autoApps = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.getStringSet(PREF_AUTO_APPS, emptySet())
|
||||
populateAppList("")
|
||||
}
|
||||
|
||||
private fun populateAppList(filter: String) {
|
||||
category?.removeAll()
|
||||
allApps?.forEach { app ->
|
||||
if ((app.flags and ApplicationInfo.FLAG_SYSTEM) != 0) return@forEach
|
||||
if (app.packageName == requireContext().packageName) return@forEach
|
||||
val label = app.loadLabel(pm!!).toString().lowercase()
|
||||
val pkg = app.packageName.lowercase()
|
||||
if (filter.isNotEmpty() && !(label.contains(filter) || pkg.contains(filter))) return@forEach
|
||||
|
||||
val pref = SwitchPreferenceCompat(requireContext()).apply {
|
||||
title = app.loadLabel(pm!!)
|
||||
summary = app.packageName
|
||||
key = "gamebar_${app.packageName}"
|
||||
isChecked = autoApps?.contains(app.packageName) == true
|
||||
icon = app.loadIcon(pm!!)
|
||||
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
val updated = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.getStringSet(PREF_AUTO_APPS, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
if (newValue as Boolean) {
|
||||
updated.add(app.packageName)
|
||||
} else {
|
||||
updated.remove(app.packageName)
|
||||
}
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.edit().putStringSet(PREF_AUTO_APPS, updated).apply()
|
||||
true
|
||||
}
|
||||
}
|
||||
category?.addPreference(pref)
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/com/android/gamebar/GameBarSettingsActivity.kt
Normal file
133
src/com/android/gamebar/GameBarSettingsActivity.kt
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity
|
||||
import com.android.gamebar.R
|
||||
|
||||
class GameBarSettingsActivity : CollapsingToolbarBaseActivity() {
|
||||
|
||||
companion object {
|
||||
private const val OVERLAY_PERMISSION_REQUEST_CODE = 1234
|
||||
private const val REQUEST_CODE_OPEN_CSV = 1001
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_game_bar)
|
||||
title = getString(R.string.game_bar_title)
|
||||
|
||||
if (!Settings.canDrawOverlays(this)) {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
Uri.parse("package:$packageName")
|
||||
)
|
||||
startActivityForResult(intent, OVERLAY_PERMISSION_REQUEST_CODE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
when (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()
|
||||
}
|
||||
}
|
||||
REQUEST_CODE_OPEN_CSV -> {
|
||||
if (resultCode == RESULT_OK) {
|
||||
data?.data?.also { uri ->
|
||||
handleExternalLogFile(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.gamebar_settings_menu, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_log_monitor -> {
|
||||
try {
|
||||
startActivity(Intent(this, GameBarLogActivity::class.java))
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, "Error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
e.printStackTrace()
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.menu_open_external_log -> {
|
||||
openExternalLogFile()
|
||||
true
|
||||
}
|
||||
R.id.menu_user_guide -> {
|
||||
try {
|
||||
val url = getString(R.string.game_bar_user_guide_url)
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, "Unable to open user guide: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
e.printStackTrace()
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openExternalLogFile() {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "text/*"
|
||||
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("text/csv", "text/comma-separated-values", "text/plain"))
|
||||
}
|
||||
|
||||
try {
|
||||
startActivityForResult(intent, REQUEST_CODE_OPEN_CSV)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, "File picker not available: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleExternalLogFile(uri: Uri) {
|
||||
try {
|
||||
// Copy file content to temporary location
|
||||
val inputStream = contentResolver.openInputStream(uri)
|
||||
val tempFile = java.io.File(cacheDir, "temp_external_log.csv")
|
||||
|
||||
inputStream?.use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
// Open analytics activity with this file
|
||||
val intent = Intent(this, LogAnalyticsActivity::class.java).apply {
|
||||
putExtra(LogAnalyticsActivity.EXTRA_LOG_FILE_PATH, tempFile.absolutePath)
|
||||
putExtra(LogAnalyticsActivity.EXTRA_LOG_FILE_NAME, uri.lastPathSegment ?: "External Log")
|
||||
}
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(this, "Error opening file: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
107
src/com/android/gamebar/GameBarTileService.kt
Normal file
107
src/com/android/gamebar/GameBarTileService.kt
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.android.gamebar.R
|
||||
|
||||
class GameBarTileService : TileService() {
|
||||
|
||||
private lateinit var gameBar: GameBar
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
gameBar = GameBar.getInstance(this)
|
||||
}
|
||||
|
||||
private fun ensureGameBarInstance() {
|
||||
if (!GameBar.isInstanceCreated()) {
|
||||
gameBar = GameBar.getInstance(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
val enabled = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getBoolean("game_bar_enable", false)
|
||||
updateTileState(enabled)
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val currentlyEnabled = prefs.getBoolean("game_bar_enable", false)
|
||||
val newState = !currentlyEnabled
|
||||
|
||||
// Update preference with apply() instead of commit() for better performance
|
||||
prefs.edit()
|
||||
.putBoolean("game_bar_enable", newState)
|
||||
.apply()
|
||||
|
||||
updateTileState(newState)
|
||||
|
||||
if (newState) {
|
||||
// Ensure we have overlay permission before showing
|
||||
if (android.provider.Settings.canDrawOverlays(this)) {
|
||||
android.util.Log.d("GameBarTileService", "Enabling GameBar from tile")
|
||||
|
||||
// Ensure we have a GameBar instance
|
||||
ensureGameBarInstance()
|
||||
|
||||
// Force cleanup any existing overlay first
|
||||
gameBar.hide()
|
||||
|
||||
gameBar.applyPreferences()
|
||||
gameBar.show()
|
||||
|
||||
// Start monitor service if needed
|
||||
val autoEnabled = prefs.getBoolean("game_bar_auto_enable", false)
|
||||
if (autoEnabled) {
|
||||
startService(android.content.Intent(this, GameBarMonitorService::class.java))
|
||||
}
|
||||
} else {
|
||||
// Revert the preference if we don't have permission
|
||||
prefs.edit()
|
||||
.putBoolean("game_bar_enable", false)
|
||||
.apply()
|
||||
updateTileState(false)
|
||||
|
||||
// Show permission request intent
|
||||
val intent = android.content.Intent(android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
android.net.Uri.parse("package:$packageName"))
|
||||
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
android.util.Log.d("GameBarTileService", "Disabling GameBar from tile")
|
||||
|
||||
// Ensure we have a GameBar instance to hide
|
||||
ensureGameBarInstance()
|
||||
|
||||
// Force hide the overlay
|
||||
gameBar.hide()
|
||||
|
||||
// Also destroy the singleton to ensure clean state
|
||||
GameBar.destroyInstance()
|
||||
|
||||
// Stop monitor service if auto-enable is also disabled
|
||||
val autoEnabled = prefs.getBoolean("game_bar_auto_enable", false)
|
||||
if (!autoEnabled) {
|
||||
stopService(android.content.Intent(this, GameBarMonitorService::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTileState(enabled: Boolean) {
|
||||
val tile = qsTile ?: return
|
||||
|
||||
tile.state = if (enabled) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
|
||||
tile.label = getString(R.string.game_bar_tile_label)
|
||||
tile.contentDescription = getString(R.string.game_bar_tile_description)
|
||||
tile.updateTile()
|
||||
}
|
||||
}
|
||||
216
src/com/android/gamebar/GameDataExport.kt
Normal file
216
src/com/android/gamebar/GameDataExport.kt
Normal file
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.os.Environment
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class GameDataExport private constructor() {
|
||||
|
||||
interface CaptureStateListener {
|
||||
fun onCaptureStarted()
|
||||
fun onCaptureStopped()
|
||||
}
|
||||
|
||||
enum class LoggingMode {
|
||||
GLOBAL, PER_APP
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: GameDataExport? = null
|
||||
|
||||
@JvmStatic
|
||||
@Synchronized
|
||||
fun getInstance(): GameDataExport {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
INSTANCE ?: GameDataExport().also { INSTANCE = it }
|
||||
}
|
||||
}
|
||||
|
||||
private const val MAX_ROWS = 10000 // Prevent unlimited memory growth
|
||||
|
||||
private val CSV_HEADER = arrayOf(
|
||||
"DateTime",
|
||||
"PackageName",
|
||||
"FPS",
|
||||
"Frame_Time",
|
||||
"Battery_Temp",
|
||||
"CPU_Usage",
|
||||
"CPU_Clock",
|
||||
"CPU_Temp",
|
||||
"RAM_Usage",
|
||||
"RAM_Speed",
|
||||
"RAM_Temp",
|
||||
"GPU_Usage",
|
||||
"GPU_Clock",
|
||||
"GPU_Temp"
|
||||
)
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private var capturing = false
|
||||
private var currentLoggingMode = LoggingMode.PER_APP // Default to per-app logging
|
||||
|
||||
private val statsRows = mutableListOf<Array<String>>()
|
||||
private var listener: CaptureStateListener? = null
|
||||
private val perAppLogManager = PerAppLogManager.getInstance()
|
||||
|
||||
fun setCaptureStateListener(listener: CaptureStateListener?) {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
fun startCapture() {
|
||||
capturing = true
|
||||
if (currentLoggingMode == LoggingMode.GLOBAL) {
|
||||
statsRows.clear()
|
||||
statsRows.add(CSV_HEADER)
|
||||
}
|
||||
listener?.onCaptureStarted()
|
||||
}
|
||||
|
||||
fun stopCapture() {
|
||||
capturing = false
|
||||
if (currentLoggingMode == LoggingMode.PER_APP) {
|
||||
perAppLogManager.stopAllPerAppLogging()
|
||||
}
|
||||
listener?.onCaptureStopped()
|
||||
}
|
||||
|
||||
fun clearData() {
|
||||
statsRows.clear()
|
||||
statsRows.add(CSV_HEADER)
|
||||
}
|
||||
|
||||
fun getDataSize(): Int {
|
||||
return statsRows.size
|
||||
}
|
||||
|
||||
fun isCapturing(): Boolean {
|
||||
return capturing
|
||||
}
|
||||
|
||||
fun addOverlayData(
|
||||
dateTime: String,
|
||||
packageName: String,
|
||||
fps: String,
|
||||
frameTime: String,
|
||||
batteryTemp: String,
|
||||
cpuUsage: String,
|
||||
cpuClock: String,
|
||||
cpuTemp: String,
|
||||
ramUsage: String,
|
||||
ramSpeed: String,
|
||||
ramTemp: String,
|
||||
gpuUsage: String,
|
||||
gpuClock: String,
|
||||
gpuTemp: String
|
||||
) {
|
||||
if (!capturing) return
|
||||
|
||||
when (currentLoggingMode) {
|
||||
LoggingMode.GLOBAL -> {
|
||||
// Prevent unlimited memory growth
|
||||
if (statsRows.size >= MAX_ROWS) {
|
||||
// Remove oldest entries but keep header
|
||||
if (statsRows.size > 1) {
|
||||
val toRemove = statsRows.size / 2
|
||||
repeat(toRemove) {
|
||||
if (statsRows.size > 1) {
|
||||
statsRows.removeAt(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val row = arrayOf(
|
||||
dateTime,
|
||||
packageName,
|
||||
fps,
|
||||
frameTime,
|
||||
batteryTemp,
|
||||
cpuUsage,
|
||||
cpuClock,
|
||||
cpuTemp,
|
||||
ramUsage,
|
||||
ramSpeed,
|
||||
ramTemp,
|
||||
gpuUsage,
|
||||
gpuClock,
|
||||
gpuTemp
|
||||
)
|
||||
statsRows.add(row)
|
||||
}
|
||||
LoggingMode.PER_APP -> {
|
||||
// Add data to per-app logging system
|
||||
perAppLogManager.addPerAppData(
|
||||
packageName,
|
||||
dateTime,
|
||||
fps,
|
||||
frameTime,
|
||||
batteryTemp,
|
||||
cpuUsage,
|
||||
cpuClock,
|
||||
cpuTemp,
|
||||
ramUsage,
|
||||
ramSpeed,
|
||||
ramTemp,
|
||||
gpuUsage,
|
||||
gpuClock,
|
||||
gpuTemp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun exportDataToCsv() {
|
||||
if (currentLoggingMode == LoggingMode.GLOBAL) {
|
||||
if (statsRows.size <= 1) {
|
||||
return
|
||||
}
|
||||
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||
val outFile = File(Environment.getExternalStorageDirectory(), "GameBar_log_$timeStamp.csv")
|
||||
|
||||
var bw: BufferedWriter? = null
|
||||
try {
|
||||
bw = BufferedWriter(FileWriter(outFile, true))
|
||||
for (row in statsRows) {
|
||||
bw.write(toCsvLine(row))
|
||||
bw.newLine()
|
||||
}
|
||||
bw.flush()
|
||||
} catch (ignored: IOException) {
|
||||
} finally {
|
||||
bw?.let {
|
||||
try { it.close() } catch (ignored: IOException) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Per-app logs are exported automatically when sessions end
|
||||
}
|
||||
|
||||
private fun toCsvLine(columns: Array<String>): String {
|
||||
return columns.joinToString(",")
|
||||
}
|
||||
|
||||
fun setLoggingMode(mode: LoggingMode) {
|
||||
currentLoggingMode = mode
|
||||
}
|
||||
|
||||
fun getLoggingMode(): LoggingMode {
|
||||
return currentLoggingMode
|
||||
}
|
||||
|
||||
fun getPerAppLogManager(): PerAppLogManager {
|
||||
return perAppLogManager
|
||||
}
|
||||
}
|
||||
212
src/com/android/gamebar/GpuClockGraphView.kt
Normal file
212
src/com/android/gamebar/GpuClockGraphView.kt
Normal file
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import kotlin.math.max
|
||||
|
||||
class GpuClockGraphView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#00BCD4") // Cyan for GPU clock
|
||||
strokeWidth = 3f
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
}
|
||||
|
||||
private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
shader = LinearGradient(
|
||||
0f, 0f, 0f, 600f,
|
||||
Color.parseColor("#8000BCD4"),
|
||||
Color.parseColor("#00000000"),
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#30FFFFFF")
|
||||
strokeWidth = 1f
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
|
||||
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FFFFFF")
|
||||
textSize = 28f
|
||||
typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
|
||||
}
|
||||
|
||||
private val avgLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FF9800")
|
||||
strokeWidth = 2f
|
||||
style = Paint.Style.STROKE
|
||||
pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||
}
|
||||
|
||||
private var clockData: List<Pair<Long, Double>> = emptyList()
|
||||
private var avgClock: Double = 0.0
|
||||
private var maxClock = 1000.0 // Dynamic max based on data
|
||||
private var minClock = 0.0
|
||||
|
||||
private val padding = 80f
|
||||
private val topPadding = 40f
|
||||
private val bottomPadding = 80f
|
||||
|
||||
fun setData(data: List<Pair<Long, Double>>, avg: Double) {
|
||||
this.clockData = data
|
||||
this.avgClock = avg
|
||||
|
||||
// Calculate dynamic max (round up to nearest 100)
|
||||
if (data.isNotEmpty()) {
|
||||
val dataMax = data.maxOf { it.second }
|
||||
maxClock = ((dataMax / 100).toInt() + 1) * 100.0
|
||||
}
|
||||
|
||||
post {
|
||||
fillPaint.shader = LinearGradient(
|
||||
0f, topPadding, 0f, height - bottomPadding,
|
||||
Color.parseColor("#8000BCD4"),
|
||||
Color.parseColor("#1000BCD4"),
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
if (clockData.isEmpty()) {
|
||||
drawEmptyState(canvas)
|
||||
return
|
||||
}
|
||||
|
||||
val graphWidth = width - 2 * padding
|
||||
val graphHeight = height - topPadding - bottomPadding
|
||||
|
||||
drawGrid(canvas, graphWidth, graphHeight)
|
||||
drawAverageLine(canvas, graphWidth, graphHeight)
|
||||
drawGraph(canvas, graphWidth, graphHeight)
|
||||
drawTitle(canvas)
|
||||
}
|
||||
|
||||
private fun drawEmptyState(canvas: Canvas) {
|
||||
val message = "No GPU clock data available"
|
||||
val textWidth = textPaint.measureText(message)
|
||||
canvas.drawText(
|
||||
message,
|
||||
(width - textWidth) / 2,
|
||||
height / 2f,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
|
||||
private fun drawGrid(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
// Calculate clock steps based on maxClock
|
||||
val step = (maxClock / 4).toInt()
|
||||
val clockSteps = (0..4).map { it * step }
|
||||
|
||||
for (clock in clockSteps) {
|
||||
val y = (topPadding + graphHeight * (1 - clock / maxClock)).toFloat()
|
||||
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, gridPaint)
|
||||
val label = "${clock}MHz"
|
||||
canvas.drawText(label, padding - 75f, y + 10f, textPaint)
|
||||
}
|
||||
|
||||
// Y-axis label
|
||||
canvas.save()
|
||||
canvas.rotate(-90f, 15f, height / 2f)
|
||||
val yLabel = "GPU Clock (MHz)"
|
||||
val yLabelWidth = textPaint.measureText(yLabel)
|
||||
canvas.drawText(yLabel, (width - yLabelWidth) / 2, 30f, textPaint)
|
||||
canvas.restore()
|
||||
|
||||
// Time labels
|
||||
if (clockData.isNotEmpty()) {
|
||||
val startTime = clockData.first().first
|
||||
val endTime = clockData.last().first
|
||||
val duration = endTime - startTime
|
||||
|
||||
canvas.drawText("0s", padding, height - bottomPadding + 25f, textPaint)
|
||||
|
||||
val middleTime = formatDuration(duration / 2)
|
||||
canvas.drawText(middleTime, padding + graphWidth / 2 - 30f, height - bottomPadding + 25f, textPaint)
|
||||
|
||||
val endTimeStr = formatDuration(duration)
|
||||
val endX = padding + graphWidth - textPaint.measureText(endTimeStr)
|
||||
canvas.drawText(endTimeStr, endX, height - bottomPadding + 25f, textPaint)
|
||||
}
|
||||
|
||||
val xLabel = "Time"
|
||||
val xLabelWidth = textPaint.measureText(xLabel)
|
||||
canvas.drawText(xLabel, (width - xLabelWidth) / 2, height - bottomPadding + 55f, textPaint)
|
||||
}
|
||||
|
||||
private fun drawAverageLine(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
val y = (topPadding + graphHeight * (1 - avgClock / maxClock)).toFloat()
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, avgLinePaint)
|
||||
}
|
||||
|
||||
private fun drawGraph(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
if (clockData.size < 2) return
|
||||
|
||||
val startTime = clockData.first().first
|
||||
val endTime = clockData.last().first
|
||||
val timeDuration = max(endTime - startTime, 1L)
|
||||
|
||||
val path = Path()
|
||||
val fillPath = Path()
|
||||
|
||||
clockData.forEachIndexed { index, (timestamp, clock) ->
|
||||
val x = padding + ((timestamp - startTime).toFloat() / timeDuration) * graphWidth
|
||||
val y = (topPadding + graphHeight * (1 - clock / maxClock)).toFloat()
|
||||
|
||||
if (index == 0) {
|
||||
path.moveTo(x, y)
|
||||
fillPath.moveTo(x, height - bottomPadding)
|
||||
fillPath.lineTo(x, y)
|
||||
} else {
|
||||
path.lineTo(x, y)
|
||||
fillPath.lineTo(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
fillPath.lineTo(padding + graphWidth, height - bottomPadding)
|
||||
fillPath.close()
|
||||
|
||||
canvas.drawPath(fillPath, fillPaint)
|
||||
canvas.drawPath(path, linePaint)
|
||||
}
|
||||
|
||||
private fun drawTitle(canvas: Canvas) {
|
||||
val title = "GPU Clock Speed vs Time"
|
||||
val titleWidth = textPaint.measureText(title)
|
||||
canvas.drawText(title, (width - titleWidth) / 2, topPadding - 10f, textPaint)
|
||||
}
|
||||
|
||||
private fun formatDuration(durationMs: Long): String {
|
||||
val seconds = durationMs / 1000
|
||||
return if (seconds < 60) {
|
||||
"${seconds}s"
|
||||
} else {
|
||||
val minutes = seconds / 60
|
||||
val remainingSeconds = seconds % 60
|
||||
"${minutes}m ${remainingSeconds}s"
|
||||
}
|
||||
}
|
||||
}
|
||||
212
src/com/android/gamebar/GpuTempGraphView.kt
Normal file
212
src/com/android/gamebar/GpuTempGraphView.kt
Normal file
@@ -0,0 +1,212 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import kotlin.math.max
|
||||
|
||||
class GpuTempGraphView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#E91E63") // Pink for GPU temperature
|
||||
strokeWidth = 3f
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
}
|
||||
|
||||
private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
shader = LinearGradient(
|
||||
0f, 0f, 0f, 600f,
|
||||
Color.parseColor("#80E91E63"),
|
||||
Color.parseColor("#00000000"),
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#30FFFFFF")
|
||||
strokeWidth = 1f
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
|
||||
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FFFFFF")
|
||||
textSize = 28f
|
||||
typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
|
||||
}
|
||||
|
||||
private val avgLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FF9800")
|
||||
strokeWidth = 2f
|
||||
style = Paint.Style.STROKE
|
||||
pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||
}
|
||||
|
||||
private var tempData: List<Pair<Long, Double>> = emptyList()
|
||||
private var avgTemp: Double = 0.0
|
||||
private var maxTemp = 100.0 // Dynamic max based on data
|
||||
private var minTemp = 0.0
|
||||
|
||||
private val padding = 80f
|
||||
private val topPadding = 40f
|
||||
private val bottomPadding = 80f
|
||||
|
||||
fun setData(data: List<Pair<Long, Double>>, avg: Double) {
|
||||
this.tempData = data
|
||||
this.avgTemp = avg
|
||||
|
||||
// Calculate dynamic max (round up to nearest 10)
|
||||
if (data.isNotEmpty()) {
|
||||
val dataMax = data.maxOf { it.second }
|
||||
maxTemp = ((dataMax / 10).toInt() + 1) * 10.0
|
||||
}
|
||||
|
||||
post {
|
||||
fillPaint.shader = LinearGradient(
|
||||
0f, topPadding, 0f, height - bottomPadding,
|
||||
Color.parseColor("#80E91E63"),
|
||||
Color.parseColor("#10E91E63"),
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
if (tempData.isEmpty()) {
|
||||
drawEmptyState(canvas)
|
||||
return
|
||||
}
|
||||
|
||||
val graphWidth = width - 2 * padding
|
||||
val graphHeight = height - topPadding - bottomPadding
|
||||
|
||||
drawGrid(canvas, graphWidth, graphHeight)
|
||||
drawAverageLine(canvas, graphWidth, graphHeight)
|
||||
drawGraph(canvas, graphWidth, graphHeight)
|
||||
drawTitle(canvas)
|
||||
}
|
||||
|
||||
private fun drawEmptyState(canvas: Canvas) {
|
||||
val message = "No GPU temperature data available"
|
||||
val textWidth = textPaint.measureText(message)
|
||||
canvas.drawText(
|
||||
message,
|
||||
(width - textWidth) / 2,
|
||||
height / 2f,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
|
||||
private fun drawGrid(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
// Calculate temp steps based on maxTemp
|
||||
val step = (maxTemp / 4).toInt()
|
||||
val tempSteps = (0..4).map { it * step }
|
||||
|
||||
for (temp in tempSteps) {
|
||||
val y = (topPadding + graphHeight * (1 - temp / maxTemp)).toFloat()
|
||||
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, gridPaint)
|
||||
val label = "${temp}°C"
|
||||
canvas.drawText(label, padding - 70f, y + 10f, textPaint)
|
||||
}
|
||||
|
||||
// Y-axis label
|
||||
canvas.save()
|
||||
canvas.rotate(-90f, 15f, height / 2f)
|
||||
val yLabel = "GPU Temperature (°C)"
|
||||
val yLabelWidth = textPaint.measureText(yLabel)
|
||||
canvas.drawText(yLabel, (width - yLabelWidth) / 2, 30f, textPaint)
|
||||
canvas.restore()
|
||||
|
||||
// Time labels
|
||||
if (tempData.isNotEmpty()) {
|
||||
val startTime = tempData.first().first
|
||||
val endTime = tempData.last().first
|
||||
val duration = endTime - startTime
|
||||
|
||||
canvas.drawText("0s", padding, height - bottomPadding + 25f, textPaint)
|
||||
|
||||
val middleTime = formatDuration(duration / 2)
|
||||
canvas.drawText(middleTime, padding + graphWidth / 2 - 30f, height - bottomPadding + 25f, textPaint)
|
||||
|
||||
val endTimeStr = formatDuration(duration)
|
||||
val endX = padding + graphWidth - textPaint.measureText(endTimeStr)
|
||||
canvas.drawText(endTimeStr, endX, height - bottomPadding + 25f, textPaint)
|
||||
}
|
||||
|
||||
val xLabel = "Time"
|
||||
val xLabelWidth = textPaint.measureText(xLabel)
|
||||
canvas.drawText(xLabel, (width - xLabelWidth) / 2, height - bottomPadding + 55f, textPaint)
|
||||
}
|
||||
|
||||
private fun drawAverageLine(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
val y = (topPadding + graphHeight * (1 - avgTemp / maxTemp)).toFloat()
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, avgLinePaint)
|
||||
}
|
||||
|
||||
private fun drawGraph(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
if (tempData.size < 2) return
|
||||
|
||||
val startTime = tempData.first().first
|
||||
val endTime = tempData.last().first
|
||||
val timeDuration = max(endTime - startTime, 1L)
|
||||
|
||||
val path = Path()
|
||||
val fillPath = Path()
|
||||
|
||||
tempData.forEachIndexed { index, (timestamp, temp) ->
|
||||
val x = padding + ((timestamp - startTime).toFloat() / timeDuration) * graphWidth
|
||||
val y = (topPadding + graphHeight * (1 - temp / maxTemp)).toFloat()
|
||||
|
||||
if (index == 0) {
|
||||
path.moveTo(x, y)
|
||||
fillPath.moveTo(x, height - bottomPadding)
|
||||
fillPath.lineTo(x, y)
|
||||
} else {
|
||||
path.lineTo(x, y)
|
||||
fillPath.lineTo(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
fillPath.lineTo(padding + graphWidth, height - bottomPadding)
|
||||
fillPath.close()
|
||||
|
||||
canvas.drawPath(fillPath, fillPaint)
|
||||
canvas.drawPath(path, linePaint)
|
||||
}
|
||||
|
||||
private fun drawTitle(canvas: Canvas) {
|
||||
val title = "GPU Temperature vs Time"
|
||||
val titleWidth = textPaint.measureText(title)
|
||||
canvas.drawText(title, (width - titleWidth) / 2, topPadding - 10f, textPaint)
|
||||
}
|
||||
|
||||
private fun formatDuration(durationMs: Long): String {
|
||||
val seconds = durationMs / 1000
|
||||
return if (seconds < 60) {
|
||||
"${seconds}s"
|
||||
} else {
|
||||
val minutes = seconds / 60
|
||||
val remainingSeconds = seconds % 60
|
||||
"${minutes}m ${remainingSeconds}s"
|
||||
}
|
||||
}
|
||||
}
|
||||
204
src/com/android/gamebar/GpuUsageGraphView.kt
Normal file
204
src/com/android/gamebar/GpuUsageGraphView.kt
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import kotlin.math.max
|
||||
|
||||
class GpuUsageGraphView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#9C27B0") // Purple for GPU
|
||||
strokeWidth = 3f
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
}
|
||||
|
||||
private val fillPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
shader = LinearGradient(
|
||||
0f, 0f, 0f, 600f,
|
||||
Color.parseColor("#809C27B0"),
|
||||
Color.parseColor("#00000000"),
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#30FFFFFF")
|
||||
strokeWidth = 1f
|
||||
style = Paint.Style.STROKE
|
||||
}
|
||||
|
||||
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FFFFFF")
|
||||
textSize = 28f
|
||||
typeface = Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
|
||||
}
|
||||
|
||||
private val avgLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#FF9800")
|
||||
strokeWidth = 2f
|
||||
style = Paint.Style.STROKE
|
||||
pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||
}
|
||||
|
||||
private var gpuData: List<Pair<Long, Double>> = emptyList()
|
||||
private var avgGpu: Double = 0.0
|
||||
private val maxGpu = 100.0
|
||||
private val minGpu = 0.0
|
||||
|
||||
private val padding = 80f
|
||||
private val topPadding = 40f
|
||||
private val bottomPadding = 80f
|
||||
|
||||
fun setData(data: List<Pair<Long, Double>>, avg: Double) {
|
||||
this.gpuData = data
|
||||
this.avgGpu = avg
|
||||
|
||||
post {
|
||||
fillPaint.shader = LinearGradient(
|
||||
0f, topPadding, 0f, height - bottomPadding,
|
||||
Color.parseColor("#809C27B0"),
|
||||
Color.parseColor("#109C27B0"),
|
||||
Shader.TileMode.CLAMP
|
||||
)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
if (gpuData.isEmpty()) {
|
||||
drawEmptyState(canvas)
|
||||
return
|
||||
}
|
||||
|
||||
val graphWidth = width - 2 * padding
|
||||
val graphHeight = height - topPadding - bottomPadding
|
||||
|
||||
drawGrid(canvas, graphWidth, graphHeight)
|
||||
drawAverageLine(canvas, graphWidth, graphHeight)
|
||||
drawGraph(canvas, graphWidth, graphHeight)
|
||||
drawTitle(canvas)
|
||||
}
|
||||
|
||||
private fun drawEmptyState(canvas: Canvas) {
|
||||
val message = "No GPU usage data available"
|
||||
val textWidth = textPaint.measureText(message)
|
||||
canvas.drawText(
|
||||
message,
|
||||
(width - textWidth) / 2,
|
||||
height / 2f,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
|
||||
private fun drawGrid(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
val gpuSteps = listOf(0, 25, 50, 75, 100)
|
||||
|
||||
for (gpu in gpuSteps) {
|
||||
val y = (topPadding + graphHeight * (1 - gpu / maxGpu.toFloat())).toFloat()
|
||||
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, gridPaint)
|
||||
val label = "$gpu%"
|
||||
canvas.drawText(label, padding - 70f, y + 10f, textPaint)
|
||||
}
|
||||
|
||||
// Y-axis label
|
||||
canvas.save()
|
||||
canvas.rotate(-90f, 15f, height / 2f)
|
||||
val yLabel = "GPU Usage (%)"
|
||||
val yLabelWidth = textPaint.measureText(yLabel)
|
||||
canvas.drawText(yLabel, (width - yLabelWidth) / 2, 30f, textPaint)
|
||||
canvas.restore()
|
||||
|
||||
// Time labels
|
||||
if (gpuData.isNotEmpty()) {
|
||||
val startTime = gpuData.first().first
|
||||
val endTime = gpuData.last().first
|
||||
val duration = endTime - startTime
|
||||
|
||||
canvas.drawText("0s", padding, height - bottomPadding + 25f, textPaint)
|
||||
|
||||
val middleTime = formatDuration(duration / 2)
|
||||
canvas.drawText(middleTime, padding + graphWidth / 2 - 30f, height - bottomPadding + 25f, textPaint)
|
||||
|
||||
val endTimeStr = formatDuration(duration)
|
||||
val endX = padding + graphWidth - textPaint.measureText(endTimeStr)
|
||||
canvas.drawText(endTimeStr, endX, height - bottomPadding + 25f, textPaint)
|
||||
}
|
||||
|
||||
val xLabel = "Time"
|
||||
val xLabelWidth = textPaint.measureText(xLabel)
|
||||
canvas.drawText(xLabel, (width - xLabelWidth) / 2, height - bottomPadding + 55f, textPaint)
|
||||
}
|
||||
|
||||
private fun drawAverageLine(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
val y = (topPadding + graphHeight * (1 - avgGpu / maxGpu)).toFloat()
|
||||
canvas.drawLine(padding, y, padding + graphWidth, y, avgLinePaint)
|
||||
}
|
||||
|
||||
private fun drawGraph(canvas: Canvas, graphWidth: Float, graphHeight: Float) {
|
||||
if (gpuData.size < 2) return
|
||||
|
||||
val startTime = gpuData.first().first
|
||||
val endTime = gpuData.last().first
|
||||
val timeDuration = max(endTime - startTime, 1L)
|
||||
|
||||
val path = Path()
|
||||
val fillPath = Path()
|
||||
|
||||
gpuData.forEachIndexed { index, (timestamp, gpu) ->
|
||||
val x = padding + ((timestamp - startTime).toFloat() / timeDuration) * graphWidth
|
||||
val y = (topPadding + graphHeight * (1 - gpu / maxGpu)).toFloat()
|
||||
|
||||
if (index == 0) {
|
||||
path.moveTo(x, y)
|
||||
fillPath.moveTo(x, height - bottomPadding)
|
||||
fillPath.lineTo(x, y)
|
||||
} else {
|
||||
path.lineTo(x, y)
|
||||
fillPath.lineTo(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
fillPath.lineTo(padding + graphWidth, height - bottomPadding)
|
||||
fillPath.close()
|
||||
|
||||
canvas.drawPath(fillPath, fillPaint)
|
||||
canvas.drawPath(path, linePaint)
|
||||
}
|
||||
|
||||
private fun drawTitle(canvas: Canvas) {
|
||||
val title = "GPU Usage vs Time"
|
||||
val titleWidth = textPaint.measureText(title)
|
||||
canvas.drawText(title, (width - titleWidth) / 2, topPadding - 10f, textPaint)
|
||||
}
|
||||
|
||||
private fun formatDuration(durationMs: Long): String {
|
||||
val seconds = durationMs / 1000
|
||||
return if (seconds < 60) {
|
||||
"${seconds}s"
|
||||
} else {
|
||||
val minutes = seconds / 60
|
||||
val remainingSeconds = seconds % 60
|
||||
"${minutes}m ${remainingSeconds}s"
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/com/android/gamebar/LogAnalyticsActivity.kt
Normal file
73
src/com/android/gamebar/LogAnalyticsActivity.kt
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity
|
||||
import com.android.gamebar.R
|
||||
|
||||
class LogAnalyticsActivity : CollapsingToolbarBaseActivity() {
|
||||
|
||||
companion object {
|
||||
const val EXTRA_LOG_FILE_PATH = "log_file_path"
|
||||
const val EXTRA_LOG_FILE_NAME = "log_file_name"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_gamebar_log)
|
||||
|
||||
val logFilePath = intent.getStringExtra(EXTRA_LOG_FILE_PATH) ?: ""
|
||||
val logFileName = intent.getStringExtra(EXTRA_LOG_FILE_NAME) ?: "Log Analytics"
|
||||
|
||||
title = logFileName
|
||||
|
||||
if (savedInstanceState == null && logFilePath.isNotEmpty()) {
|
||||
showAnalytics(logFilePath, logFileName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAnalytics(filePath: String, fileName: String) {
|
||||
// Show loading message
|
||||
val loadingDialog = AlertDialog.Builder(this)
|
||||
.setTitle("Analyzing Log...")
|
||||
.setMessage("Please wait while we analyze the session data.")
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
loadingDialog.show()
|
||||
|
||||
// Analyze log file in background thread
|
||||
Thread {
|
||||
val logReader = PerAppLogReader()
|
||||
val analytics = logReader.analyzeLogFile(filePath)
|
||||
|
||||
// Update UI on main thread
|
||||
runOnUiThread {
|
||||
loadingDialog.dismiss()
|
||||
|
||||
if (analytics != null) {
|
||||
// Create and show fragment with analytics
|
||||
val fragment = LogAnalyticsFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString("LOG_FILE_PATH", filePath)
|
||||
putString("LOG_FILE_NAME", fileName)
|
||||
putSerializable("ANALYTICS_DATA", analytics)
|
||||
}
|
||||
}
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.content_frame, fragment)
|
||||
.commit()
|
||||
} else {
|
||||
Toast.makeText(this, "Failed to analyze log file", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
148
src/com/android/gamebar/LogAnalyticsFragment.kt
Normal file
148
src/com/android/gamebar/LogAnalyticsFragment.kt
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.android.gamebar.R
|
||||
|
||||
class LogAnalyticsFragment : Fragment() {
|
||||
|
||||
private var analytics: LogAnalytics? = null
|
||||
private var logFileName: String = "Log"
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val logFilePath = arguments?.getString("LOG_FILE_PATH") ?: ""
|
||||
logFileName = arguments?.getString("LOG_FILE_NAME") ?: "Log"
|
||||
analytics = arguments?.getSerializable("ANALYTICS_DATA") as? LogAnalytics
|
||||
|
||||
if (analytics == null) {
|
||||
Toast.makeText(requireContext(), "Failed to load analytics data", Toast.LENGTH_SHORT).show()
|
||||
return inflater.inflate(R.layout.dialog_log_analytics, container, false)
|
||||
}
|
||||
|
||||
return inflater.inflate(R.layout.dialog_log_analytics, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val analyticsData = analytics ?: return
|
||||
|
||||
// Get all views from the layout
|
||||
val sessionInfoText = view.findViewById<TextView>(R.id.tv_session_info)
|
||||
val fpsGraphView = view.findViewById<FpsGraphView>(R.id.fps_graph_view)
|
||||
val frameTimeGraphView = view.findViewById<FrameTimeGraphView>(R.id.frame_time_graph_view)
|
||||
val maxFpsText = view.findViewById<TextView>(R.id.tv_max_fps)
|
||||
val minFpsText = view.findViewById<TextView>(R.id.tv_min_fps)
|
||||
val avgFpsText = view.findViewById<TextView>(R.id.tv_avg_fps)
|
||||
val varianceText = view.findViewById<TextView>(R.id.tv_variance)
|
||||
val stdDevText = view.findViewById<TextView>(R.id.tv_std_dev)
|
||||
val smoothnessText = view.findViewById<TextView>(R.id.tv_smoothness)
|
||||
val fps1PercentText = view.findViewById<TextView>(R.id.tv_1percent_low)
|
||||
val fps01PercentText = view.findViewById<TextView>(R.id.tv_01percent_low)
|
||||
|
||||
// Set session info
|
||||
sessionInfoText.text = buildString {
|
||||
appendLine("📅 ${analyticsData.sessionDate}")
|
||||
appendLine("⏱️ ${analyticsData.sessionDuration}")
|
||||
appendLine("📊 ${analyticsData.totalSamples} samples")
|
||||
append("📁 $logFileName")
|
||||
}
|
||||
|
||||
// Set FPS statistics
|
||||
maxFpsText.text = String.format("Max FPS: %.1f", analyticsData.fpsStats.maxFps)
|
||||
minFpsText.text = String.format("Min FPS: %.1f", analyticsData.fpsStats.minFps)
|
||||
avgFpsText.text = String.format("Avg FPS: %.1f", analyticsData.fpsStats.avgFps)
|
||||
varianceText.text = String.format("Variance: %.2f", analyticsData.fpsStats.variance)
|
||||
stdDevText.text = String.format("Std Dev: %.2f", analyticsData.fpsStats.standardDeviation)
|
||||
smoothnessText.text = String.format("Smoothness: %.1f%%", analyticsData.fpsStats.smoothnessPercentage)
|
||||
fps1PercentText.text = String.format("1%% Low: %.1f FPS", analyticsData.fpsStats.fps1PercentLow)
|
||||
fps01PercentText.text = String.format("0.1%% Low: %.1f FPS", analyticsData.fpsStats.fps0_1PercentLow)
|
||||
|
||||
// Set FPS graph data
|
||||
fpsGraphView.setData(
|
||||
analyticsData.fpsTimeData,
|
||||
analyticsData.fpsStats.avgFps,
|
||||
analyticsData.fpsStats.fps1PercentLow
|
||||
)
|
||||
|
||||
// Set Frame Time graph data
|
||||
val avgFrameTime = if (analyticsData.fpsStats.avgFps > 0) {
|
||||
1000.0 / analyticsData.fpsStats.avgFps
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
frameTimeGraphView.setData(analyticsData.frameTimeData, avgFrameTime)
|
||||
|
||||
// Get CPU graph views
|
||||
val cpuUsageGraphView = view.findViewById<CpuGraphView>(R.id.cpu_usage_graph_view)
|
||||
val cpuTempGraphView = view.findViewById<CpuTempGraphView>(R.id.cpu_temp_graph_view)
|
||||
val cpuClockGraphView = view.findViewById<CpuClockGraphView>(R.id.cpu_clock_graph_view)
|
||||
|
||||
// Get CPU statistics views
|
||||
val maxCpuUsageText = view.findViewById<TextView>(R.id.tv_max_cpu_usage)
|
||||
val minCpuUsageText = view.findViewById<TextView>(R.id.tv_min_cpu_usage)
|
||||
val avgCpuUsageText = view.findViewById<TextView>(R.id.tv_avg_cpu_usage)
|
||||
val maxCpuTempText = view.findViewById<TextView>(R.id.tv_max_cpu_temp)
|
||||
val minCpuTempText = view.findViewById<TextView>(R.id.tv_min_cpu_temp)
|
||||
val avgCpuTempText = view.findViewById<TextView>(R.id.tv_avg_cpu_temp)
|
||||
|
||||
// Set CPU graph data
|
||||
cpuUsageGraphView.setData(analyticsData.cpuUsageTimeData, analyticsData.cpuStats.avgUsage)
|
||||
cpuTempGraphView.setData(analyticsData.cpuTempTimeData, analyticsData.cpuStats.avgTemp)
|
||||
cpuClockGraphView.setData(analyticsData.cpuClockTimeData)
|
||||
|
||||
// Set CPU statistics
|
||||
maxCpuUsageText.text = String.format("Max Usage: %.0f%%", analyticsData.cpuStats.maxUsage)
|
||||
minCpuUsageText.text = String.format("Min Usage: %.0f%%", analyticsData.cpuStats.minUsage)
|
||||
avgCpuUsageText.text = String.format("Avg Usage: %.1f%%", analyticsData.cpuStats.avgUsage)
|
||||
maxCpuTempText.text = String.format("Max Temp: %.1f°C", analyticsData.cpuStats.maxTemp)
|
||||
minCpuTempText.text = String.format("Min Temp: %.1f°C", analyticsData.cpuStats.minTemp)
|
||||
avgCpuTempText.text = String.format("Avg Temp: %.1f°C", analyticsData.cpuStats.avgTemp)
|
||||
|
||||
// Get GPU graph views
|
||||
val gpuUsageGraphView = view.findViewById<GpuUsageGraphView>(R.id.gpu_usage_graph_view)
|
||||
val gpuTempGraphView = view.findViewById<GpuTempGraphView>(R.id.gpu_temp_graph_view)
|
||||
val gpuClockGraphView = view.findViewById<GpuClockGraphView>(R.id.gpu_clock_graph_view)
|
||||
|
||||
// Get GPU statistics views
|
||||
val maxGpuUsageText = view.findViewById<TextView>(R.id.tv_max_gpu_usage)
|
||||
val minGpuUsageText = view.findViewById<TextView>(R.id.tv_min_gpu_usage)
|
||||
val avgGpuUsageText = view.findViewById<TextView>(R.id.tv_avg_gpu_usage)
|
||||
val maxGpuClockText = view.findViewById<TextView>(R.id.tv_max_gpu_clock)
|
||||
val minGpuClockText = view.findViewById<TextView>(R.id.tv_min_gpu_clock)
|
||||
val avgGpuClockText = view.findViewById<TextView>(R.id.tv_avg_gpu_clock)
|
||||
val maxGpuTempText = view.findViewById<TextView>(R.id.tv_max_gpu_temp)
|
||||
val minGpuTempText = view.findViewById<TextView>(R.id.tv_min_gpu_temp)
|
||||
val avgGpuTempText = view.findViewById<TextView>(R.id.tv_avg_gpu_temp)
|
||||
|
||||
// Set GPU graph data
|
||||
gpuUsageGraphView.setData(analyticsData.gpuUsageTimeData, analyticsData.gpuStats.avgUsage)
|
||||
gpuTempGraphView.setData(analyticsData.gpuTempTimeData, analyticsData.gpuStats.avgTemp)
|
||||
gpuClockGraphView.setData(analyticsData.gpuClockTimeData, analyticsData.gpuStats.avgClock)
|
||||
|
||||
// Set GPU statistics
|
||||
maxGpuUsageText.text = String.format("Max Usage: %.0f%%", analyticsData.gpuStats.maxUsage)
|
||||
minGpuUsageText.text = String.format("Min Usage: %.0f%%", analyticsData.gpuStats.minUsage)
|
||||
avgGpuUsageText.text = String.format("Avg Usage: %.1f%%", analyticsData.gpuStats.avgUsage)
|
||||
maxGpuClockText.text = String.format("Max Clock: %.0f MHz", analyticsData.gpuStats.maxClock)
|
||||
minGpuClockText.text = String.format("Min Clock: %.0f MHz", analyticsData.gpuStats.minClock)
|
||||
avgGpuClockText.text = String.format("Avg Clock: %.0f MHz", analyticsData.gpuStats.avgClock)
|
||||
maxGpuTempText.text = String.format("Max Temp: %.1f°C", analyticsData.gpuStats.maxTemp)
|
||||
minGpuTempText.text = String.format("Min Temp: %.1f°C", analyticsData.gpuStats.minTemp)
|
||||
avgGpuTempText.text = String.format("Avg Temp: %.1f°C", analyticsData.gpuStats.avgTemp)
|
||||
}
|
||||
}
|
||||
46
src/com/android/gamebar/LogHistoryAdapter.kt
Normal file
46
src/com/android/gamebar/LogHistoryAdapter.kt
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.gamebar.R
|
||||
|
||||
class LogHistoryAdapter(
|
||||
private val logFiles: List<GameBarLogFragment.LogFile>,
|
||||
private val onItemClick: (GameBarLogFragment.LogFile, View) -> Unit
|
||||
) : RecyclerView.Adapter<LogHistoryAdapter.LogFileViewHolder>() {
|
||||
|
||||
inner class LogFileViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val fileName: TextView = itemView.findViewById(R.id.tv_file_name)
|
||||
private val fileInfo: TextView = itemView.findViewById(R.id.tv_file_info)
|
||||
|
||||
fun bind(logFile: GameBarLogFragment.LogFile) {
|
||||
fileName.text = logFile.name
|
||||
fileInfo.text = "${logFile.size} • ${logFile.lastModified}"
|
||||
|
||||
itemView.setOnClickListener {
|
||||
onItemClick(logFile, itemView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LogFileViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_log_file, parent, false)
|
||||
return LogFileViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: LogFileViewHolder, position: Int) {
|
||||
holder.bind(logFiles[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = logFiles.size
|
||||
}
|
||||
120
src/com/android/gamebar/PerAppLogAdapter.kt
Normal file
120
src/com/android/gamebar/PerAppLogAdapter.kt
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.Switch
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.gamebar.R
|
||||
|
||||
class PerAppLogAdapter(
|
||||
private val context: Context,
|
||||
private var apps: List<ApplicationInfo>,
|
||||
private val onSwitchChanged: (String, Boolean) -> Unit,
|
||||
private val onViewLogsClicked: (String, String) -> Unit // packageName, appName
|
||||
) : RecyclerView.Adapter<PerAppLogAdapter.PerAppLogViewHolder>() {
|
||||
|
||||
private val packageManager = context.packageManager
|
||||
private val perAppLogManager = PerAppLogManager.getInstance()
|
||||
private var enabledApps = setOf<String>()
|
||||
private var currentlyLoggingApps = setOf<String>()
|
||||
|
||||
fun updateApps(newApps: List<ApplicationInfo>) {
|
||||
apps = newApps
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun updateEnabledApps(enabled: Set<String>) {
|
||||
enabledApps = enabled
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun updateCurrentlyLoggingApps(logging: Set<String>) {
|
||||
currentlyLoggingApps = logging
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun refreshLogFileStates() {
|
||||
// Force refresh of the adapter to update log file states
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
inner class PerAppLogViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val appIcon: ImageView = itemView.findViewById(R.id.iv_app_icon)
|
||||
private val appName: TextView = itemView.findViewById(R.id.tv_app_name)
|
||||
private val packageName: TextView = itemView.findViewById(R.id.tv_package_name)
|
||||
private val viewLogsIcon: ImageView = itemView.findViewById(R.id.iv_view_logs)
|
||||
private val loggingSwitch: Switch = itemView.findViewById(R.id.switch_per_app_logging)
|
||||
|
||||
fun bind(app: ApplicationInfo) {
|
||||
val appLabel = app.loadLabel(packageManager).toString()
|
||||
|
||||
appIcon.setImageDrawable(app.loadIcon(packageManager))
|
||||
appName.text = appLabel
|
||||
packageName.text = app.packageName
|
||||
|
||||
// Update switch state
|
||||
loggingSwitch.setOnCheckedChangeListener(null) // Prevent recursive calls
|
||||
loggingSwitch.isChecked = enabledApps.contains(app.packageName)
|
||||
|
||||
// Add visual indicator if currently logging
|
||||
if (currentlyLoggingApps.contains(app.packageName)) {
|
||||
appName.setTextColor(context.getColor(R.color.gamebar_green))
|
||||
packageName.text = "${app.packageName} • LOGGING"
|
||||
} else {
|
||||
appName.setTextColor(context.getColor(R.color.app_name_text_selector))
|
||||
packageName.text = app.packageName
|
||||
}
|
||||
|
||||
// Set switch listener
|
||||
loggingSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
onSwitchChanged(app.packageName, isChecked)
|
||||
}
|
||||
|
||||
// Always enable view logs icon - the log view will handle empty state
|
||||
viewLogsIcon.alpha = 1.0f
|
||||
viewLogsIcon.isEnabled = true
|
||||
|
||||
// Set view logs click listener - always navigate to log view
|
||||
viewLogsIcon.setOnClickListener {
|
||||
onViewLogsClicked(app.packageName, appLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PerAppLogViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_per_app_log, parent, false)
|
||||
return PerAppLogViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PerAppLogViewHolder, position: Int) {
|
||||
holder.bind(apps[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = apps.size
|
||||
|
||||
fun filter(query: String): List<ApplicationInfo> {
|
||||
return if (query.isEmpty()) {
|
||||
apps
|
||||
} else {
|
||||
apps.filter { app ->
|
||||
val label = app.loadLabel(packageManager).toString().lowercase()
|
||||
val pkg = app.packageName.lowercase()
|
||||
label.contains(query.lowercase()) || pkg.contains(query.lowercase())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
407
src/com/android/gamebar/PerAppLogManager.kt
Normal file
407
src/com/android/gamebar/PerAppLogManager.kt
Normal file
@@ -0,0 +1,407 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.preference.PreferenceManager
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class PerAppLogManager private constructor() {
|
||||
|
||||
interface PerAppStateListener {
|
||||
fun onAppLoggingStarted(packageName: String)
|
||||
fun onAppLoggingStopped(packageName: String)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: PerAppLogManager? = null
|
||||
private const val TAG = "PerAppLogManager"
|
||||
private const val PREF_PER_APP_ENABLED_APPS = "per_app_logging_enabled_apps"
|
||||
private const val MAX_ROWS_PER_APP = 5000
|
||||
|
||||
@JvmStatic
|
||||
@Synchronized
|
||||
fun getInstance(): PerAppLogManager {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
INSTANCE ?: PerAppLogManager().also { INSTANCE = it }
|
||||
}
|
||||
}
|
||||
|
||||
private val CSV_HEADER = arrayOf(
|
||||
"DateTime",
|
||||
"PackageName",
|
||||
"FPS",
|
||||
"Frame_Time",
|
||||
"Battery_Temp",
|
||||
"CPU_Usage",
|
||||
"CPU_Clock",
|
||||
"CPU_Temp",
|
||||
"RAM_Usage",
|
||||
"RAM_Speed",
|
||||
"RAM_Temp",
|
||||
"GPU_Usage",
|
||||
"GPU_Clock",
|
||||
"GPU_Temp"
|
||||
)
|
||||
}
|
||||
|
||||
private val activeLogSessions = ConcurrentHashMap<String, MutableList<Array<String>>>()
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var listener: PerAppStateListener? = null
|
||||
private var currentForegroundApp: String? = null
|
||||
|
||||
fun setPerAppStateListener(listener: PerAppStateListener?) {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
fun getEnabledApps(context: Context): Set<String> {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getStringSet(PREF_PER_APP_ENABLED_APPS, emptySet()) ?: emptySet()
|
||||
}
|
||||
|
||||
fun setAppLoggingEnabled(context: Context, packageName: String, enabled: Boolean) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val current = prefs.getStringSet(PREF_PER_APP_ENABLED_APPS, emptySet())?.toMutableSet() ?: mutableSetOf()
|
||||
|
||||
if (enabled) {
|
||||
current.add(packageName)
|
||||
} else {
|
||||
current.remove(packageName)
|
||||
// Stop logging for this app if it's currently active
|
||||
stopLoggingForApp(packageName)
|
||||
}
|
||||
|
||||
prefs.edit().putStringSet(PREF_PER_APP_ENABLED_APPS, current).apply()
|
||||
}
|
||||
|
||||
fun isAppLoggingEnabled(context: Context, packageName: String): Boolean {
|
||||
return getEnabledApps(context).contains(packageName)
|
||||
}
|
||||
|
||||
fun onAppBecameForeground(context: Context, packageName: String) {
|
||||
if (currentForegroundApp == packageName) return
|
||||
|
||||
// Stop logging for previous app if it was being logged
|
||||
currentForegroundApp?.let { previousApp ->
|
||||
if (isAppLoggingActive(previousApp)) {
|
||||
stopLoggingForApp(previousApp)
|
||||
}
|
||||
}
|
||||
|
||||
currentForegroundApp = packageName
|
||||
|
||||
// Start logging for new app if enabled AND per-app logging mode is active
|
||||
// Note: Per-app logging works independently of GameBar overlay visibility
|
||||
val gameDataExport = GameDataExport.getInstance()
|
||||
if (isAppLoggingEnabled(context, packageName) &&
|
||||
gameDataExport.getLoggingMode() == GameDataExport.LoggingMode.PER_APP &&
|
||||
gameDataExport.isCapturing()) {
|
||||
startLoggingForApp(packageName)
|
||||
}
|
||||
}
|
||||
|
||||
fun onAppWentToBackground(context: Context, packageName: String) {
|
||||
if (currentForegroundApp == packageName) {
|
||||
currentForegroundApp = null
|
||||
if (isAppLoggingActive(packageName)) {
|
||||
stopLoggingForApp(packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startLoggingForApp(packageName: String) {
|
||||
if (activeLogSessions.containsKey(packageName)) return
|
||||
|
||||
// Check if GameBar overlay is actually showing
|
||||
val isGameBarShowing = GameBar.isShowing()
|
||||
|
||||
if (!isGameBarShowing) {
|
||||
// GameBar overlay is OFF - cannot collect data
|
||||
Log.w(TAG, "Cannot start logging for $packageName - GameBar overlay is OFF")
|
||||
handler.post {
|
||||
try {
|
||||
val context = android.app.ActivityThread.currentApplication()
|
||||
context?.let {
|
||||
val pm = it.packageManager
|
||||
try {
|
||||
val appInfo = pm.getApplicationInfo(packageName, 0)
|
||||
val appName = pm.getApplicationLabel(appInfo).toString()
|
||||
android.widget.Toast.makeText(it,
|
||||
"$appName: GameBar logging enabled but GameBar overlay is OFF. Turn ON GameBar to collect logs.",
|
||||
android.widget.Toast.LENGTH_LONG).show()
|
||||
} catch (e: Exception) {
|
||||
android.widget.Toast.makeText(it,
|
||||
"GameBar logging enabled but GameBar overlay is OFF. Turn ON GameBar to collect logs.",
|
||||
android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to show warning toast for $packageName", e)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Starting logging for app: $packageName")
|
||||
val logData = mutableListOf<Array<String>>()
|
||||
logData.add(CSV_HEADER)
|
||||
activeLogSessions[packageName] = logData
|
||||
|
||||
listener?.onAppLoggingStarted(packageName)
|
||||
|
||||
// Show toast notification
|
||||
handler.post {
|
||||
try {
|
||||
val context = android.app.ActivityThread.currentApplication()
|
||||
context?.let {
|
||||
val pm = it.packageManager
|
||||
try {
|
||||
val appInfo = pm.getApplicationInfo(packageName, 0)
|
||||
val appName = pm.getApplicationLabel(appInfo).toString()
|
||||
android.widget.Toast.makeText(it, "$appName GameBar log started", android.widget.Toast.LENGTH_SHORT).show()
|
||||
} catch (e: Exception) {
|
||||
android.widget.Toast.makeText(it, "$packageName GameBar log started", android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to show start toast for $packageName", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopLoggingForApp(packageName: String) {
|
||||
val logData = activeLogSessions.remove(packageName)
|
||||
if (logData != null && logData.size > 1) {
|
||||
Log.d(TAG, "Stopping logging for app: $packageName")
|
||||
exportPerAppDataToCsv(packageName, logData)
|
||||
listener?.onAppLoggingStopped(packageName)
|
||||
|
||||
// Show toast notification
|
||||
handler.post {
|
||||
try {
|
||||
val context = android.app.ActivityThread.currentApplication()
|
||||
context?.let {
|
||||
val pm = it.packageManager
|
||||
try {
|
||||
val appInfo = pm.getApplicationInfo(packageName, 0)
|
||||
val appName = pm.getApplicationLabel(appInfo).toString()
|
||||
android.widget.Toast.makeText(it, "$appName GameBar log ended", android.widget.Toast.LENGTH_SHORT).show()
|
||||
} catch (e: Exception) {
|
||||
android.widget.Toast.makeText(it, "$packageName GameBar log ended", android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to show stop toast for $packageName", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isAppLoggingActive(packageName: String): Boolean {
|
||||
return activeLogSessions.containsKey(packageName)
|
||||
}
|
||||
|
||||
fun addPerAppData(
|
||||
packageName: String,
|
||||
dateTime: String,
|
||||
fps: String,
|
||||
frameTime: String,
|
||||
batteryTemp: String,
|
||||
cpuUsage: String,
|
||||
cpuClock: String,
|
||||
cpuTemp: String,
|
||||
ramUsage: String,
|
||||
ramSpeed: String,
|
||||
ramTemp: String,
|
||||
gpuUsage: String,
|
||||
gpuClock: String,
|
||||
gpuTemp: String
|
||||
) {
|
||||
val logData = activeLogSessions[packageName] ?: return
|
||||
|
||||
// Prevent unlimited memory growth
|
||||
if (logData.size >= MAX_ROWS_PER_APP) {
|
||||
// Remove oldest entries but keep header
|
||||
if (logData.size > 1) {
|
||||
val toRemove = logData.size / 2
|
||||
repeat(toRemove) {
|
||||
if (logData.size > 1) {
|
||||
logData.removeAt(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val row = arrayOf(
|
||||
dateTime,
|
||||
packageName,
|
||||
fps,
|
||||
frameTime,
|
||||
batteryTemp,
|
||||
cpuUsage,
|
||||
cpuClock,
|
||||
cpuTemp,
|
||||
ramUsage,
|
||||
ramSpeed,
|
||||
ramTemp,
|
||||
gpuUsage,
|
||||
gpuClock,
|
||||
gpuTemp
|
||||
)
|
||||
logData.add(row)
|
||||
}
|
||||
|
||||
private fun exportPerAppDataToCsv(packageName: String, logData: List<Array<String>>) {
|
||||
if (logData.size <= 1) return
|
||||
|
||||
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||
val fileName = "${packageName}_GameBar_log_$timeStamp.csv"
|
||||
val outFile = File(Environment.getExternalStorageDirectory(), fileName)
|
||||
|
||||
var bw: BufferedWriter? = null
|
||||
try {
|
||||
bw = BufferedWriter(FileWriter(outFile, false))
|
||||
for (row in logData) {
|
||||
bw.write(toCsvLine(row))
|
||||
bw.newLine()
|
||||
}
|
||||
bw.flush()
|
||||
Log.d(TAG, "Exported per-app log for $packageName to ${outFile.absolutePath}")
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to export per-app log for $packageName", e)
|
||||
} finally {
|
||||
bw?.let {
|
||||
try { it.close() } catch (ignored: IOException) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toCsvLine(columns: Array<String>): String {
|
||||
return columns.joinToString(",")
|
||||
}
|
||||
|
||||
fun getPerAppLogFiles(packageName: String): List<File> {
|
||||
val externalStorageDir = Environment.getExternalStorageDirectory()
|
||||
val files = externalStorageDir.listFiles { file ->
|
||||
file.name.startsWith("${packageName}_GameBar_log_") && file.name.endsWith(".csv")
|
||||
}
|
||||
return files?.sortedByDescending { it.lastModified() } ?: emptyList()
|
||||
}
|
||||
|
||||
fun getAllPerAppLogFiles(): Map<String, List<File>> {
|
||||
val externalStorageDir = Environment.getExternalStorageDirectory()
|
||||
val files = externalStorageDir.listFiles { file ->
|
||||
file.name.contains("_GameBar_log_") &&
|
||||
file.name.endsWith(".csv") &&
|
||||
!file.name.startsWith("GameBar_log_") // Exclude global logs
|
||||
} ?: emptyArray()
|
||||
|
||||
return files.groupBy { file ->
|
||||
// Extract package name from filename: packageName_GameBar_log_timestamp.csv
|
||||
val fileName = file.name
|
||||
val endIndex = fileName.indexOf("_GameBar_log_")
|
||||
if (endIndex > 0) fileName.substring(0, endIndex) else "unknown"
|
||||
}.mapValues { (_, fileList) ->
|
||||
fileList.sortedByDescending { it.lastModified() }
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentlyLoggingApps(): Set<String> {
|
||||
return activeLogSessions.keys.toSet()
|
||||
}
|
||||
|
||||
/**
|
||||
* Start manual logging for an app via double-tap (even if not in enabled apps list)
|
||||
*/
|
||||
fun startManualLoggingForApp(packageName: String) {
|
||||
if (activeLogSessions.containsKey(packageName)) {
|
||||
// Already logging, ignore
|
||||
return
|
||||
}
|
||||
|
||||
// Check if GameBar overlay is showing
|
||||
val isGameBarShowing = GameBar.isShowing()
|
||||
if (!isGameBarShowing) {
|
||||
handler.post {
|
||||
try {
|
||||
val context = android.app.ActivityThread.currentApplication()
|
||||
context?.let {
|
||||
android.widget.Toast.makeText(it,
|
||||
"Cannot start logging - GameBar overlay is OFF",
|
||||
android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to show toast", e)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Starting manual logging for app: $packageName")
|
||||
val logData = mutableListOf<Array<String>>()
|
||||
logData.add(CSV_HEADER)
|
||||
activeLogSessions[packageName] = logData
|
||||
|
||||
listener?.onAppLoggingStarted(packageName)
|
||||
|
||||
// Show toast notification
|
||||
handler.post {
|
||||
try {
|
||||
val context = android.app.ActivityThread.currentApplication()
|
||||
context?.let {
|
||||
val pm = it.packageManager
|
||||
try {
|
||||
val appInfo = pm.getApplicationInfo(packageName, 0)
|
||||
val appName = pm.getApplicationLabel(appInfo).toString()
|
||||
android.widget.Toast.makeText(it, "$appName: Manual logging started", android.widget.Toast.LENGTH_SHORT).show()
|
||||
} catch (e: Exception) {
|
||||
android.widget.Toast.makeText(it, "Manual logging started for $packageName", android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to show start toast for $packageName", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop manual logging for an app via double-tap
|
||||
*/
|
||||
fun stopManualLoggingForApp(packageName: String) {
|
||||
stopLoggingForApp(packageName)
|
||||
}
|
||||
|
||||
fun stopAllPerAppLogging() {
|
||||
val appsToStop = activeLogSessions.keys.toList()
|
||||
appsToStop.forEach { packageName ->
|
||||
stopLoggingForApp(packageName)
|
||||
}
|
||||
}
|
||||
|
||||
fun handlePerAppLoggingStarted(context: Context) {
|
||||
// When per-app logging is started, check if current foreground app should be logged
|
||||
val foregroundApp = ForegroundAppDetector.getForegroundPackageName(context)
|
||||
if (foregroundApp != "Unknown" && foregroundApp.isNotEmpty()) {
|
||||
currentForegroundApp = foregroundApp
|
||||
if (isAppLoggingEnabled(context, foregroundApp)) {
|
||||
startLoggingForApp(foregroundApp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
554
src/com/android/gamebar/PerAppLogReader.kt
Normal file
554
src/com/android/gamebar/PerAppLogReader.kt
Normal file
@@ -0,0 +1,554 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.util.Log
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileReader
|
||||
import java.io.Serializable
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
data class LogAnalytics(
|
||||
val fpsStats: FpsStatistics,
|
||||
val cpuStats: CpuStatistics,
|
||||
val gpuStats: GpuStatistics,
|
||||
val sessionDuration: String,
|
||||
val totalSamples: Int,
|
||||
val appName: String,
|
||||
val sessionDate: String,
|
||||
val fpsTimeData: List<Pair<Long, Double>>, // Timestamp in millis, FPS value
|
||||
val frameTimeData: List<Pair<Long, Double>>, // Frame time over time
|
||||
val cpuUsageTimeData: List<Pair<Long, Double>>, // CPU usage over time
|
||||
val cpuTempTimeData: List<Pair<Long, Double>>, // CPU temp over time
|
||||
val cpuClockTimeData: Map<Int, List<Pair<Long, Double>>>, // Per-core clock speeds over time
|
||||
val gpuUsageTimeData: List<Pair<Long, Double>>, // GPU usage over time
|
||||
val gpuTempTimeData: List<Pair<Long, Double>>, // GPU temp over time
|
||||
val gpuClockTimeData: List<Pair<Long, Double>> // GPU clock speed over time
|
||||
) : Serializable
|
||||
|
||||
data class FpsStatistics(
|
||||
val maxFps: Double,
|
||||
val minFps: Double,
|
||||
val avgFps: Double,
|
||||
val variance: Double,
|
||||
val standardDeviation: Double,
|
||||
val fps1PercentLow: Double, // 1% low - worst 1% of frames
|
||||
val fps0_1PercentLow: Double, // 0.1% low - worst 0.1% of frames
|
||||
val smoothnessPercentage: Double // Percentage of frames >= 45 FPS
|
||||
) : Serializable
|
||||
|
||||
data class CpuStatistics(
|
||||
val maxUsage: Double,
|
||||
val minUsage: Double,
|
||||
val avgUsage: Double,
|
||||
val maxTemp: Double,
|
||||
val minTemp: Double,
|
||||
val avgTemp: Double
|
||||
) : Serializable
|
||||
|
||||
data class GpuStatistics(
|
||||
val maxUsage: Double,
|
||||
val minUsage: Double,
|
||||
val avgUsage: Double,
|
||||
val maxClock: Double,
|
||||
val minClock: Double,
|
||||
val avgClock: Double,
|
||||
val maxTemp: Double,
|
||||
val minTemp: Double,
|
||||
val avgTemp: Double
|
||||
) : Serializable
|
||||
|
||||
class PerAppLogReader {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PerAppLogReader"
|
||||
|
||||
// CSV column indices (updated format)
|
||||
private const val COL_DATETIME = 0
|
||||
private const val COL_PACKAGE_NAME = 1
|
||||
private const val COL_FPS = 2
|
||||
private const val COL_FRAME_TIME = 3
|
||||
private const val COL_BATTERY_TEMP = 4
|
||||
private const val COL_CPU_USAGE = 5
|
||||
private const val COL_CPU_CLOCK = 6
|
||||
private const val COL_CPU_TEMP = 7
|
||||
private const val COL_RAM_USAGE = 8
|
||||
private const val COL_RAM_SPEED = 9
|
||||
private const val COL_RAM_TEMP = 10
|
||||
private const val COL_GPU_USAGE = 11
|
||||
private const val COL_GPU_CLOCK = 12
|
||||
private const val COL_GPU_TEMP = 13
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and analyze a log file
|
||||
*/
|
||||
fun analyzeLogFile(logFilePath: String): LogAnalytics? {
|
||||
return try {
|
||||
val file = File(logFilePath)
|
||||
if (!file.exists() || !file.canRead()) {
|
||||
Log.e(TAG, "Log file does not exist or cannot be read: $logFilePath")
|
||||
return null
|
||||
}
|
||||
|
||||
val fpsValues = mutableListOf<Double>()
|
||||
val fpsTimeData = mutableListOf<Pair<Long, Double>>()
|
||||
val frameTimeValues = mutableListOf<Double>()
|
||||
val frameTimeData = mutableListOf<Pair<Long, Double>>()
|
||||
val cpuUsageValues = mutableListOf<Double>()
|
||||
val cpuUsageTimeData = mutableListOf<Pair<Long, Double>>()
|
||||
val cpuTempValues = mutableListOf<Double>()
|
||||
val cpuTempTimeData = mutableListOf<Pair<Long, Double>>()
|
||||
val cpuClockTimeData = mutableMapOf<Int, MutableList<Pair<Long, Double>>>()
|
||||
val gpuUsageValues = mutableListOf<Double>()
|
||||
val gpuUsageTimeData = mutableListOf<Pair<Long, Double>>()
|
||||
val gpuClockValues = mutableListOf<Double>()
|
||||
val gpuClockTimeData = mutableListOf<Pair<Long, Double>>()
|
||||
val gpuTempValues = mutableListOf<Double>()
|
||||
val gpuTempTimeData = mutableListOf<Pair<Long, Double>>()
|
||||
var firstTimestamp: String? = null
|
||||
var lastTimestamp: String? = null
|
||||
var packageName = ""
|
||||
var lineCount = 0
|
||||
var sessionStartTimeMs: Long = 0
|
||||
|
||||
BufferedReader(FileReader(file)).use { reader ->
|
||||
var line = reader.readLine()
|
||||
|
||||
// Skip header
|
||||
if (line != null && line.contains("DateTime")) {
|
||||
line = reader.readLine()
|
||||
}
|
||||
|
||||
// Read data lines
|
||||
while (line != null) {
|
||||
lineCount++
|
||||
val columns = line.split(",")
|
||||
|
||||
if (columns.size >= COL_FPS + 1) {
|
||||
try {
|
||||
// Extract timestamp
|
||||
val timestampStr = columns[COL_DATETIME].trim()
|
||||
|
||||
// Extract FPS
|
||||
val fpsStr = columns[COL_FPS].trim()
|
||||
if (fpsStr.isNotEmpty() && fpsStr != "N/A" && fpsStr != "-") {
|
||||
val fps = fpsStr.toDoubleOrNull()
|
||||
if (fps != null && fps > 0) {
|
||||
fpsValues.add(fps)
|
||||
|
||||
// Parse timestamp and calculate relative time
|
||||
try {
|
||||
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
||||
val timestamp = dateFormat.parse(timestampStr)?.time ?: 0L
|
||||
|
||||
if (sessionStartTimeMs == 0L) {
|
||||
sessionStartTimeMs = timestamp
|
||||
}
|
||||
|
||||
// Store relative time in milliseconds
|
||||
val relativeTime = timestamp - sessionStartTimeMs
|
||||
fpsTimeData.add(Pair(relativeTime, fps))
|
||||
} catch (e: Exception) {
|
||||
// If timestamp parsing fails, use sequential time
|
||||
fpsTimeData.add(Pair(lineCount * 1000L, fps))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract Frame Time
|
||||
if (columns.size > COL_FRAME_TIME) {
|
||||
val frameTimeStr = columns[COL_FRAME_TIME].trim()
|
||||
if (frameTimeStr.isNotEmpty() && frameTimeStr != "N/A" && frameTimeStr != "-") {
|
||||
val frameTime = frameTimeStr.toDoubleOrNull()
|
||||
if (frameTime != null && frameTime > 0) {
|
||||
frameTimeValues.add(frameTime)
|
||||
val relativeTime = if (sessionStartTimeMs > 0) {
|
||||
fpsTimeData.lastOrNull()?.first ?: (lineCount * 1000L)
|
||||
} else {
|
||||
lineCount * 1000L
|
||||
}
|
||||
frameTimeData.add(Pair(relativeTime, frameTime))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract CPU Usage
|
||||
if (columns.size > COL_CPU_USAGE) {
|
||||
val cpuUsageStr = columns[COL_CPU_USAGE].trim()
|
||||
if (cpuUsageStr.isNotEmpty() && cpuUsageStr != "N/A") {
|
||||
val cpuUsage = cpuUsageStr.toDoubleOrNull()
|
||||
if (cpuUsage != null && cpuUsage >= 0) {
|
||||
cpuUsageValues.add(cpuUsage)
|
||||
val relativeTime = if (sessionStartTimeMs > 0) {
|
||||
fpsTimeData.lastOrNull()?.first ?: (lineCount * 1000L)
|
||||
} else {
|
||||
lineCount * 1000L
|
||||
}
|
||||
cpuUsageTimeData.add(Pair(relativeTime, cpuUsage))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract CPU Temp
|
||||
if (columns.size > COL_CPU_TEMP) {
|
||||
val cpuTempStr = columns[COL_CPU_TEMP].trim()
|
||||
if (cpuTempStr.isNotEmpty() && cpuTempStr != "N/A") {
|
||||
val cpuTemp = cpuTempStr.toDoubleOrNull()
|
||||
if (cpuTemp != null && cpuTemp > 0) {
|
||||
cpuTempValues.add(cpuTemp)
|
||||
val relativeTime = if (sessionStartTimeMs > 0) {
|
||||
fpsTimeData.lastOrNull()?.first ?: (lineCount * 1000L)
|
||||
} else {
|
||||
lineCount * 1000L
|
||||
}
|
||||
cpuTempTimeData.add(Pair(relativeTime, cpuTemp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract CPU Clock (multi-core data)
|
||||
if (columns.size > COL_CPU_CLOCK) {
|
||||
val cpuClockStr = columns[COL_CPU_CLOCK].trim()
|
||||
if (cpuClockStr.isNotEmpty() && cpuClockStr != "N/A") {
|
||||
// Parse format: "cpu0: 1800 MHz; cpu1: 2000 MHz; ..."
|
||||
val relativeTime = if (sessionStartTimeMs > 0) {
|
||||
fpsTimeData.lastOrNull()?.first ?: (lineCount * 1000L)
|
||||
} else {
|
||||
lineCount * 1000L
|
||||
}
|
||||
|
||||
cpuClockStr.split(";").forEachIndexed { index, coreData ->
|
||||
try {
|
||||
// Extract MHz value from "cpuX: YYYY MHz"
|
||||
val mhzMatch = Regex("(\\d+)\\s*MHz").find(coreData)
|
||||
if (mhzMatch != null) {
|
||||
val mhz = mhzMatch.groupValues[1].toDouble()
|
||||
if (!cpuClockTimeData.containsKey(index)) {
|
||||
cpuClockTimeData[index] = mutableListOf()
|
||||
}
|
||||
cpuClockTimeData[index]?.add(Pair(relativeTime, mhz))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Skip malformed core data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract GPU Usage
|
||||
if (columns.size > COL_GPU_USAGE) {
|
||||
val gpuUsageStr = columns[COL_GPU_USAGE].trim()
|
||||
if (gpuUsageStr.isNotEmpty() && gpuUsageStr != "N/A") {
|
||||
val gpuUsage = gpuUsageStr.toDoubleOrNull()
|
||||
if (gpuUsage != null && gpuUsage >= 0) {
|
||||
gpuUsageValues.add(gpuUsage)
|
||||
val relativeTime = if (sessionStartTimeMs > 0) {
|
||||
fpsTimeData.lastOrNull()?.first ?: (lineCount * 1000L)
|
||||
} else {
|
||||
lineCount * 1000L
|
||||
}
|
||||
gpuUsageTimeData.add(Pair(relativeTime, gpuUsage))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract GPU Clock
|
||||
if (columns.size > COL_GPU_CLOCK) {
|
||||
val gpuClockStr = columns[COL_GPU_CLOCK].trim()
|
||||
if (gpuClockStr.isNotEmpty() && gpuClockStr != "N/A") {
|
||||
val gpuClock = gpuClockStr.toDoubleOrNull()
|
||||
if (gpuClock != null && gpuClock > 0) {
|
||||
gpuClockValues.add(gpuClock)
|
||||
val relativeTime = if (sessionStartTimeMs > 0) {
|
||||
fpsTimeData.lastOrNull()?.first ?: (lineCount * 1000L)
|
||||
} else {
|
||||
lineCount * 1000L
|
||||
}
|
||||
gpuClockTimeData.add(Pair(relativeTime, gpuClock))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract GPU Temp
|
||||
if (columns.size > COL_GPU_TEMP) {
|
||||
val gpuTempStr = columns[COL_GPU_TEMP].trim()
|
||||
if (gpuTempStr.isNotEmpty() && gpuTempStr != "N/A") {
|
||||
val gpuTemp = gpuTempStr.toDoubleOrNull()
|
||||
if (gpuTemp != null && gpuTemp > 0) {
|
||||
gpuTempValues.add(gpuTemp)
|
||||
val relativeTime = if (sessionStartTimeMs > 0) {
|
||||
fpsTimeData.lastOrNull()?.first ?: (lineCount * 1000L)
|
||||
} else {
|
||||
lineCount * 1000L
|
||||
}
|
||||
gpuTempTimeData.add(Pair(relativeTime, gpuTemp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract timestamps for session duration
|
||||
if (firstTimestamp == null) {
|
||||
firstTimestamp = timestampStr
|
||||
packageName = columns[COL_PACKAGE_NAME].trim()
|
||||
}
|
||||
lastTimestamp = timestampStr
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error parsing line $lineCount: $line", e)
|
||||
}
|
||||
}
|
||||
|
||||
line = reader.readLine()
|
||||
}
|
||||
}
|
||||
|
||||
if (fpsValues.isEmpty()) {
|
||||
Log.w(TAG, "No valid FPS data found in log file")
|
||||
return null
|
||||
}
|
||||
|
||||
// Calculate FPS statistics
|
||||
val fpsStats = calculateFpsStatistics(fpsValues)
|
||||
|
||||
// Calculate CPU statistics
|
||||
val cpuStats = calculateCpuStatistics(cpuUsageValues, cpuTempValues)
|
||||
|
||||
// Calculate GPU statistics
|
||||
val gpuStats = calculateGpuStatistics(gpuUsageValues, gpuClockValues, gpuTempValues)
|
||||
|
||||
// Calculate session duration
|
||||
val sessionDuration = calculateSessionDuration(firstTimestamp, lastTimestamp)
|
||||
|
||||
// Extract session date from filename
|
||||
val sessionDate = extractSessionDate(file.name)
|
||||
|
||||
LogAnalytics(
|
||||
fpsStats = fpsStats,
|
||||
cpuStats = cpuStats,
|
||||
gpuStats = gpuStats,
|
||||
sessionDuration = sessionDuration,
|
||||
totalSamples = fpsValues.size,
|
||||
appName = packageName,
|
||||
sessionDate = sessionDate,
|
||||
fpsTimeData = fpsTimeData,
|
||||
frameTimeData = frameTimeData,
|
||||
cpuUsageTimeData = cpuUsageTimeData,
|
||||
cpuTempTimeData = cpuTempTimeData,
|
||||
cpuClockTimeData = cpuClockTimeData,
|
||||
gpuUsageTimeData = gpuUsageTimeData,
|
||||
gpuTempTimeData = gpuTempTimeData,
|
||||
gpuClockTimeData = gpuClockTimeData
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error analyzing log file: $logFilePath", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate comprehensive FPS statistics
|
||||
*/
|
||||
private fun calculateFpsStatistics(fpsValues: List<Double>): FpsStatistics {
|
||||
if (fpsValues.isEmpty()) {
|
||||
return FpsStatistics(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
|
||||
}
|
||||
|
||||
val sortedFps = fpsValues.sorted()
|
||||
|
||||
// Max and Min
|
||||
val maxFps = sortedFps.last()
|
||||
val minFps = sortedFps.first()
|
||||
|
||||
// Average
|
||||
val avgFps = fpsValues.average()
|
||||
|
||||
// Variance and Standard Deviation
|
||||
val variance = if (fpsValues.size > 1) {
|
||||
fpsValues.map { (it - avgFps).pow(2) }.sum() / fpsValues.size
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
val stdDev = sqrt(variance)
|
||||
|
||||
// Calculate 1% low and 0.1% low
|
||||
val fps1PercentLow = calculatePercentileLow(sortedFps, 0.01)
|
||||
val fps0_1PercentLow = calculatePercentileLow(sortedFps, 0.001)
|
||||
|
||||
// Calculate smoothness percentage (frames >= 45 FPS)
|
||||
val smoothFrames = fpsValues.count { it >= 45.0 }
|
||||
val smoothnessPercentage = (smoothFrames.toDouble() / fpsValues.size) * 100.0
|
||||
|
||||
return FpsStatistics(
|
||||
maxFps = maxFps,
|
||||
minFps = minFps,
|
||||
avgFps = avgFps,
|
||||
variance = variance,
|
||||
standardDeviation = stdDev,
|
||||
fps1PercentLow = fps1PercentLow,
|
||||
fps0_1PercentLow = fps0_1PercentLow,
|
||||
smoothnessPercentage = smoothnessPercentage
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate CPU statistics
|
||||
*/
|
||||
private fun calculateCpuStatistics(cpuUsageValues: List<Double>, cpuTempValues: List<Double>): CpuStatistics {
|
||||
val maxUsage = if (cpuUsageValues.isNotEmpty()) cpuUsageValues.maxOrNull() ?: 0.0 else 0.0
|
||||
val minUsage = if (cpuUsageValues.isNotEmpty()) cpuUsageValues.minOrNull() ?: 0.0 else 0.0
|
||||
val avgUsage = if (cpuUsageValues.isNotEmpty()) cpuUsageValues.average() else 0.0
|
||||
|
||||
val maxTemp = if (cpuTempValues.isNotEmpty()) cpuTempValues.maxOrNull() ?: 0.0 else 0.0
|
||||
val minTemp = if (cpuTempValues.isNotEmpty()) cpuTempValues.minOrNull() ?: 0.0 else 0.0
|
||||
val avgTemp = if (cpuTempValues.isNotEmpty()) cpuTempValues.average() else 0.0
|
||||
|
||||
return CpuStatistics(
|
||||
maxUsage = maxUsage,
|
||||
minUsage = minUsage,
|
||||
avgUsage = avgUsage,
|
||||
maxTemp = maxTemp,
|
||||
minTemp = minTemp,
|
||||
avgTemp = avgTemp
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate GPU statistics
|
||||
*/
|
||||
private fun calculateGpuStatistics(
|
||||
gpuUsageValues: List<Double>,
|
||||
gpuClockValues: List<Double>,
|
||||
gpuTempValues: List<Double>
|
||||
): GpuStatistics {
|
||||
val maxUsage = if (gpuUsageValues.isNotEmpty()) gpuUsageValues.maxOrNull() ?: 0.0 else 0.0
|
||||
val minUsage = if (gpuUsageValues.isNotEmpty()) gpuUsageValues.minOrNull() ?: 0.0 else 0.0
|
||||
val avgUsage = if (gpuUsageValues.isNotEmpty()) gpuUsageValues.average() else 0.0
|
||||
|
||||
val maxClock = if (gpuClockValues.isNotEmpty()) gpuClockValues.maxOrNull() ?: 0.0 else 0.0
|
||||
val minClock = if (gpuClockValues.isNotEmpty()) gpuClockValues.minOrNull() ?: 0.0 else 0.0
|
||||
val avgClock = if (gpuClockValues.isNotEmpty()) gpuClockValues.average() else 0.0
|
||||
|
||||
val maxTemp = if (gpuTempValues.isNotEmpty()) gpuTempValues.maxOrNull() ?: 0.0 else 0.0
|
||||
val minTemp = if (gpuTempValues.isNotEmpty()) gpuTempValues.minOrNull() ?: 0.0 else 0.0
|
||||
val avgTemp = if (gpuTempValues.isNotEmpty()) gpuTempValues.average() else 0.0
|
||||
|
||||
return GpuStatistics(
|
||||
maxUsage = maxUsage,
|
||||
minUsage = minUsage,
|
||||
avgUsage = avgUsage,
|
||||
maxClock = maxClock,
|
||||
minClock = minClock,
|
||||
avgClock = avgClock,
|
||||
maxTemp = maxTemp,
|
||||
minTemp = minTemp,
|
||||
avgTemp = avgTemp
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate percentile low (e.g., 1% low = average of worst 1% of frames)
|
||||
*/
|
||||
private fun calculatePercentileLow(sortedFps: List<Double>, percentile: Double): Double {
|
||||
if (sortedFps.isEmpty()) return 0.0
|
||||
|
||||
val count = (sortedFps.size * percentile).toInt().coerceAtLeast(1)
|
||||
val worstFrames = sortedFps.take(count)
|
||||
|
||||
return if (worstFrames.isNotEmpty()) {
|
||||
worstFrames.average()
|
||||
} else {
|
||||
sortedFps.first()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate session duration from timestamps
|
||||
*/
|
||||
private fun calculateSessionDuration(firstTimestamp: String?, lastTimestamp: String?): String {
|
||||
if (firstTimestamp == null || lastTimestamp == null) {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
return try {
|
||||
// Parse timestamps - format example: "2025-01-15 14:32:45"
|
||||
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
||||
val startTime = dateFormat.parse(firstTimestamp)
|
||||
val endTime = dateFormat.parse(lastTimestamp)
|
||||
|
||||
if (startTime != null && endTime != null) {
|
||||
val durationMs = endTime.time - startTime.time
|
||||
val seconds = (durationMs / 1000) % 60
|
||||
val minutes = (durationMs / (1000 * 60)) % 60
|
||||
val hours = (durationMs / (1000 * 60 * 60))
|
||||
|
||||
when {
|
||||
hours > 0 -> String.format("%dh %dm %ds", hours, minutes, seconds)
|
||||
minutes > 0 -> String.format("%dm %ds", minutes, seconds)
|
||||
else -> String.format("%ds", seconds)
|
||||
}
|
||||
} else {
|
||||
"Unknown"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error calculating session duration", e)
|
||||
"Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract session date from filename
|
||||
* Filename format: packageName_GameBar_log_yyyyMMdd_HHmmss.csv
|
||||
*/
|
||||
private fun extractSessionDate(fileName: String): String {
|
||||
return try {
|
||||
val pattern = Regex("_(\\d{8})_(\\d{6})\\.csv$")
|
||||
val match = pattern.find(fileName)
|
||||
|
||||
if (match != null) {
|
||||
val dateStr = match.groupValues[1]
|
||||
val timeStr = match.groupValues[2]
|
||||
|
||||
// Parse: yyyyMMdd_HHmmss
|
||||
val inputFormat = java.text.SimpleDateFormat("yyyyMMddHHmmss", java.util.Locale.getDefault())
|
||||
val outputFormat = java.text.SimpleDateFormat("MMM dd, yyyy HH:mm", java.util.Locale.getDefault())
|
||||
|
||||
val date = inputFormat.parse(dateStr + timeStr)
|
||||
if (date != null) {
|
||||
outputFormat.format(date)
|
||||
} else {
|
||||
"Unknown Date"
|
||||
}
|
||||
} else {
|
||||
"Unknown Date"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error extracting session date from filename: $fileName", e)
|
||||
"Unknown Date"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format FPS statistics for display
|
||||
*/
|
||||
fun formatFpsStats(stats: FpsStatistics): String {
|
||||
return buildString {
|
||||
appendLine("FPS Statistics:")
|
||||
appendLine("─────────────────────────")
|
||||
appendLine(String.format("Max FPS: %.1f", stats.maxFps))
|
||||
appendLine(String.format("Min FPS: %.1f", stats.minFps))
|
||||
appendLine(String.format("Avg FPS: %.1f", stats.avgFps))
|
||||
appendLine(String.format("Variance: %.2f", stats.variance))
|
||||
appendLine(String.format("Std Dev: %.2f", stats.standardDeviation))
|
||||
appendLine(String.format("1%% Low: %.1f", stats.fps1PercentLow))
|
||||
appendLine(String.format("0.1%% Low: %.1f", stats.fps0_1PercentLow))
|
||||
}
|
||||
}
|
||||
}
|
||||
153
src/com/android/gamebar/PerAppLogService.kt
Normal file
153
src/com/android/gamebar/PerAppLogService.kt
Normal file
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.widget.Toast
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Dedicated service for per-app logging that runs independently of GameBar display logic.
|
||||
* This service only monitors foreground app changes for logging purposes and doesn't
|
||||
* interfere with GameBar functionality.
|
||||
*/
|
||||
class PerAppLogService : Service() {
|
||||
|
||||
private var handler: Handler? = null
|
||||
private var monitorRunnable: Runnable? = null
|
||||
@Volatile
|
||||
private var isRunning = false
|
||||
|
||||
private var lastForegroundApp = ""
|
||||
private val perAppLogManager = PerAppLogManager.getInstance()
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PerAppLogService"
|
||||
private const val MONITOR_INTERVAL = 1000L // 1 second for responsive logging
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
handler = Handler(Looper.getMainLooper())
|
||||
monitorRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (isRunning) {
|
||||
monitorForPerAppLogging()
|
||||
if (isRunning && handler != null) {
|
||||
handler!!.postDelayed(this, MONITOR_INTERVAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (!isRunning) {
|
||||
Log.d(TAG, "Starting per-app logging service")
|
||||
isRunning = true
|
||||
handler?.let { h ->
|
||||
monitorRunnable?.let { r ->
|
||||
h.post(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun monitorForPerAppLogging() {
|
||||
try {
|
||||
if (!isRunning) return
|
||||
|
||||
// Only run if per-app logging is active
|
||||
val gameDataExport = GameDataExport.getInstance()
|
||||
if (gameDataExport.getLoggingMode() != GameDataExport.LoggingMode.PER_APP ||
|
||||
!gameDataExport.isCapturing()) {
|
||||
// Per-app logging is not active, stop service
|
||||
Log.d(TAG, "Per-app logging not active, stopping service")
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
|
||||
val foreground = ForegroundAppDetector.getForegroundPackageName(this)
|
||||
|
||||
// Only process if foreground app changed
|
||||
if (foreground != lastForegroundApp) {
|
||||
Log.d(TAG, "Foreground app changed from $lastForegroundApp to $foreground")
|
||||
|
||||
// Handle previous app
|
||||
if (lastForegroundApp.isNotEmpty() && lastForegroundApp != "Unknown") {
|
||||
if (perAppLogManager.isAppLoggingActive(lastForegroundApp)) {
|
||||
Log.d(TAG, "Stopping logging for $lastForegroundApp")
|
||||
perAppLogManager.onAppWentToBackground(this, lastForegroundApp)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle new app
|
||||
if (foreground.isNotEmpty() && foreground != "Unknown") {
|
||||
if (perAppLogManager.isAppLoggingEnabled(this, foreground)) {
|
||||
Log.d(TAG, "Starting logging for $foreground")
|
||||
perAppLogManager.onAppBecameForeground(this, foreground)
|
||||
}
|
||||
}
|
||||
|
||||
lastForegroundApp = foreground
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in monitorForPerAppLogging", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAppName(packageName: String): String {
|
||||
return try {
|
||||
val pm = packageManager
|
||||
val appInfo = pm.getApplicationInfo(packageName, 0)
|
||||
pm.getApplicationLabel(appInfo).toString()
|
||||
} catch (e: Exception) {
|
||||
packageName // Fallback to package name
|
||||
}
|
||||
}
|
||||
|
||||
private fun showToast(message: String) {
|
||||
handler?.post {
|
||||
try {
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to show toast: $message", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Log.d(TAG, "Stopping per-app logging service")
|
||||
isRunning = false
|
||||
|
||||
handler?.let {
|
||||
monitorRunnable?.let { runnable ->
|
||||
it.removeCallbacks(runnable)
|
||||
}
|
||||
it.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
// Stop all active per-app logging sessions
|
||||
perAppLogManager.stopAllPerAppLogging()
|
||||
|
||||
// Clear state variables
|
||||
lastForegroundApp = ""
|
||||
handler = null
|
||||
monitorRunnable = null
|
||||
}
|
||||
}
|
||||
42
src/com/android/gamebar/PerAppLogViewActivity.kt
Normal file
42
src/com/android/gamebar/PerAppLogViewActivity.kt
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.os.Bundle
|
||||
import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity
|
||||
import com.android.gamebar.R
|
||||
|
||||
class PerAppLogViewActivity : CollapsingToolbarBaseActivity() {
|
||||
|
||||
companion object {
|
||||
const val EXTRA_PACKAGE_NAME = "package_name"
|
||||
const val EXTRA_APP_NAME = "app_name"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_gamebar_log)
|
||||
|
||||
val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME) ?: ""
|
||||
val appName = intent.getStringExtra(EXTRA_APP_NAME) ?: "Unknown App"
|
||||
|
||||
title = "Logs for $appName"
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
val fragment = PerAppLogViewFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(EXTRA_PACKAGE_NAME, packageName)
|
||||
putString(EXTRA_APP_NAME, appName)
|
||||
}
|
||||
}
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.content_frame, fragment)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
573
src/com/android/gamebar/PerAppLogViewFragment.kt
Normal file
573
src/com/android/gamebar/PerAppLogViewFragment.kt
Normal file
@@ -0,0 +1,573 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.gamebar.R
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class PerAppLogViewFragment : Fragment() {
|
||||
|
||||
private lateinit var searchBar: EditText
|
||||
private lateinit var logHistoryRecyclerView: RecyclerView
|
||||
private lateinit var logHistoryAdapter: LogHistoryAdapter
|
||||
private lateinit var emptyStateView: View
|
||||
private lateinit var emptyMessageView: TextView
|
||||
private val logFiles = mutableListOf<GameBarLogFragment.LogFile>()
|
||||
private val allLogFiles = mutableListOf<GameBarLogFragment.LogFile>()
|
||||
private val perAppLogManager = PerAppLogManager.getInstance()
|
||||
|
||||
private var packageName: String = ""
|
||||
private var appName: String = ""
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
packageName = arguments?.getString(PerAppLogViewActivity.EXTRA_PACKAGE_NAME) ?: ""
|
||||
appName = arguments?.getString(PerAppLogViewActivity.EXTRA_APP_NAME) ?: "Unknown App"
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_per_app_log_view, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
try {
|
||||
initViews(view)
|
||||
setupRecyclerView()
|
||||
setupSearchBar()
|
||||
loadPerAppLogHistory()
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "Error loading logs: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initViews(view: View) {
|
||||
searchBar = view.findViewById(R.id.search_bar)
|
||||
logHistoryRecyclerView = view.findViewById(R.id.rv_log_history)
|
||||
emptyStateView = view.findViewById(R.id.empty_state)
|
||||
emptyMessageView = view.findViewById(R.id.tv_empty_message)
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
logHistoryAdapter = LogHistoryAdapter(logFiles) { logFile, view ->
|
||||
showLogFilePopupMenu(logFile, view)
|
||||
}
|
||||
logHistoryRecyclerView.apply {
|
||||
layoutManager = LinearLayoutManager(requireContext())
|
||||
adapter = logHistoryAdapter
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSearchBar() {
|
||||
searchBar.hint = "Search $appName logs..."
|
||||
searchBar.addTextChangedListener(object : android.text.TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
filterLogs(s.toString().lowercase())
|
||||
}
|
||||
override fun afterTextChanged(s: android.text.Editable?) {}
|
||||
})
|
||||
}
|
||||
|
||||
private fun filterLogs(query: String) {
|
||||
logFiles.clear()
|
||||
if (query.isEmpty()) {
|
||||
logFiles.addAll(allLogFiles)
|
||||
} else {
|
||||
allLogFiles.forEach { logFile ->
|
||||
if (logFile.name.lowercase().contains(query) ||
|
||||
logFile.lastModified.lowercase().contains(query)) {
|
||||
logFiles.add(logFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
logHistoryAdapter.notifyDataSetChanged()
|
||||
updateEmptyState()
|
||||
}
|
||||
|
||||
private fun updateEmptyState() {
|
||||
if (logFiles.isEmpty()) {
|
||||
logHistoryRecyclerView.visibility = View.GONE
|
||||
emptyStateView.visibility = View.VISIBLE
|
||||
emptyMessageView.text = "No logs available for $appName"
|
||||
} else {
|
||||
logHistoryRecyclerView.visibility = View.VISIBLE
|
||||
emptyStateView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPerAppLogHistory() {
|
||||
logFiles.clear()
|
||||
allLogFiles.clear()
|
||||
|
||||
val files = perAppLogManager.getPerAppLogFiles(packageName)
|
||||
|
||||
for (file in files) {
|
||||
val logFile = GameBarLogFragment.LogFile(
|
||||
name = file.name,
|
||||
path = file.absolutePath,
|
||||
size = formatFileSize(file.length()),
|
||||
lastModified = formatDate(file.lastModified())
|
||||
)
|
||||
allLogFiles.add(logFile)
|
||||
logFiles.add(logFile)
|
||||
}
|
||||
|
||||
logHistoryAdapter.notifyDataSetChanged()
|
||||
updateEmptyState()
|
||||
}
|
||||
|
||||
private fun showLogFilePopupMenu(logFile: GameBarLogFragment.LogFile, anchorView: View) {
|
||||
val popupMenu = PopupMenu(requireContext(), anchorView)
|
||||
popupMenu.menuInflater.inflate(R.menu.log_file_popup_menu, popupMenu.menu)
|
||||
|
||||
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.menu_open -> {
|
||||
openLogFile(logFile)
|
||||
true
|
||||
}
|
||||
R.id.menu_share -> {
|
||||
shareLogFile(logFile)
|
||||
true
|
||||
}
|
||||
R.id.menu_export -> {
|
||||
exportLogFile(logFile)
|
||||
true
|
||||
}
|
||||
R.id.menu_delete -> {
|
||||
deleteLogFile(logFile)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
popupMenu.show()
|
||||
}
|
||||
|
||||
private fun openLogFile(logFile: GameBarLogFragment.LogFile) {
|
||||
// Show analytics popup
|
||||
showLogAnalyticsDialog(logFile)
|
||||
}
|
||||
|
||||
private fun showLogAnalyticsDialog(logFile: GameBarLogFragment.LogFile) {
|
||||
// Show loading message
|
||||
val loadingDialog = AlertDialog.Builder(requireContext())
|
||||
.setTitle("Analyzing Log...")
|
||||
.setMessage("Please wait while we analyze the session data.")
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
loadingDialog.show()
|
||||
|
||||
// Analyze log file in background thread
|
||||
Thread {
|
||||
val logReader = PerAppLogReader()
|
||||
val analytics = logReader.analyzeLogFile(logFile.path)
|
||||
|
||||
// Update UI on main thread
|
||||
requireActivity().runOnUiThread {
|
||||
loadingDialog.dismiss()
|
||||
|
||||
if (analytics != null) {
|
||||
showAnalyticsResult(logFile, analytics)
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Failed to analyze log file", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun showAnalyticsResult(logFile: GameBarLogFragment.LogFile, analytics: LogAnalytics) {
|
||||
// Inflate custom dialog layout
|
||||
val dialogView = layoutInflater.inflate(R.layout.dialog_log_analytics, null)
|
||||
|
||||
// Get views
|
||||
val sessionInfoText = dialogView.findViewById<TextView>(R.id.tv_session_info)
|
||||
val fpsGraphView = dialogView.findViewById<FpsGraphView>(R.id.fps_graph_view)
|
||||
val frameTimeGraphView = dialogView.findViewById<FrameTimeGraphView>(R.id.frame_time_graph_view)
|
||||
val maxFpsText = dialogView.findViewById<TextView>(R.id.tv_max_fps)
|
||||
val minFpsText = dialogView.findViewById<TextView>(R.id.tv_min_fps)
|
||||
val avgFpsText = dialogView.findViewById<TextView>(R.id.tv_avg_fps)
|
||||
val varianceText = dialogView.findViewById<TextView>(R.id.tv_variance)
|
||||
val stdDevText = dialogView.findViewById<TextView>(R.id.tv_std_dev)
|
||||
val smoothnessText = dialogView.findViewById<TextView>(R.id.tv_smoothness)
|
||||
val fps1PercentText = dialogView.findViewById<TextView>(R.id.tv_1percent_low)
|
||||
val fps01PercentText = dialogView.findViewById<TextView>(R.id.tv_01percent_low)
|
||||
|
||||
// Set session info
|
||||
sessionInfoText.text = buildString {
|
||||
appendLine("📅 ${analytics.sessionDate}")
|
||||
appendLine("⏱️ ${analytics.sessionDuration}")
|
||||
appendLine("📊 ${analytics.totalSamples} samples")
|
||||
append("📁 ${logFile.name}")
|
||||
}
|
||||
|
||||
// Set FPS statistics
|
||||
maxFpsText.text = String.format("Max FPS: %.1f", analytics.fpsStats.maxFps)
|
||||
minFpsText.text = String.format("Min FPS: %.1f", analytics.fpsStats.minFps)
|
||||
avgFpsText.text = String.format("Avg FPS: %.1f", analytics.fpsStats.avgFps)
|
||||
varianceText.text = String.format("Variance: %.2f", analytics.fpsStats.variance)
|
||||
stdDevText.text = String.format("Std Dev: %.2f", analytics.fpsStats.standardDeviation)
|
||||
smoothnessText.text = String.format("Smoothness: %.1f%%", analytics.fpsStats.smoothnessPercentage)
|
||||
fps1PercentText.text = String.format("1%% Low: %.1f FPS", analytics.fpsStats.fps1PercentLow)
|
||||
fps01PercentText.text = String.format("0.1%% Low: %.1f FPS", analytics.fpsStats.fps0_1PercentLow)
|
||||
|
||||
// Set FPS graph data
|
||||
fpsGraphView.setData(
|
||||
analytics.fpsTimeData,
|
||||
analytics.fpsStats.avgFps,
|
||||
analytics.fpsStats.fps1PercentLow
|
||||
)
|
||||
|
||||
// Set Frame Time graph data
|
||||
val avgFrameTime = if (analytics.fpsStats.avgFps > 0) {
|
||||
1000.0 / analytics.fpsStats.avgFps
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
frameTimeGraphView.setData(analytics.frameTimeData, avgFrameTime)
|
||||
|
||||
// Get CPU graph views
|
||||
val cpuUsageGraphView = dialogView.findViewById<CpuGraphView>(R.id.cpu_usage_graph_view)
|
||||
val cpuTempGraphView = dialogView.findViewById<CpuTempGraphView>(R.id.cpu_temp_graph_view)
|
||||
val cpuClockGraphView = dialogView.findViewById<CpuClockGraphView>(R.id.cpu_clock_graph_view)
|
||||
|
||||
// Get CPU statistics views
|
||||
val maxCpuUsageText = dialogView.findViewById<TextView>(R.id.tv_max_cpu_usage)
|
||||
val minCpuUsageText = dialogView.findViewById<TextView>(R.id.tv_min_cpu_usage)
|
||||
val avgCpuUsageText = dialogView.findViewById<TextView>(R.id.tv_avg_cpu_usage)
|
||||
val maxCpuTempText = dialogView.findViewById<TextView>(R.id.tv_max_cpu_temp)
|
||||
val minCpuTempText = dialogView.findViewById<TextView>(R.id.tv_min_cpu_temp)
|
||||
val avgCpuTempText = dialogView.findViewById<TextView>(R.id.tv_avg_cpu_temp)
|
||||
|
||||
// Set CPU graph data
|
||||
cpuUsageGraphView.setData(analytics.cpuUsageTimeData, analytics.cpuStats.avgUsage)
|
||||
cpuTempGraphView.setData(analytics.cpuTempTimeData, analytics.cpuStats.avgTemp)
|
||||
cpuClockGraphView.setData(analytics.cpuClockTimeData)
|
||||
|
||||
// Set CPU statistics
|
||||
maxCpuUsageText.text = String.format("Max Usage: %.0f%%", analytics.cpuStats.maxUsage)
|
||||
minCpuUsageText.text = String.format("Min Usage: %.0f%%", analytics.cpuStats.minUsage)
|
||||
avgCpuUsageText.text = String.format("Avg Usage: %.1f%%", analytics.cpuStats.avgUsage)
|
||||
maxCpuTempText.text = String.format("Max Temp: %.1f°C", analytics.cpuStats.maxTemp)
|
||||
minCpuTempText.text = String.format("Min Temp: %.1f°C", analytics.cpuStats.minTemp)
|
||||
avgCpuTempText.text = String.format("Avg Temp: %.1f°C", analytics.cpuStats.avgTemp)
|
||||
|
||||
// Get GPU graph views
|
||||
val gpuUsageGraphView = dialogView.findViewById<GpuUsageGraphView>(R.id.gpu_usage_graph_view)
|
||||
val gpuTempGraphView = dialogView.findViewById<GpuTempGraphView>(R.id.gpu_temp_graph_view)
|
||||
val gpuClockGraphView = dialogView.findViewById<GpuClockGraphView>(R.id.gpu_clock_graph_view)
|
||||
|
||||
// Get GPU statistics views
|
||||
val maxGpuUsageText = dialogView.findViewById<TextView>(R.id.tv_max_gpu_usage)
|
||||
val minGpuUsageText = dialogView.findViewById<TextView>(R.id.tv_min_gpu_usage)
|
||||
val avgGpuUsageText = dialogView.findViewById<TextView>(R.id.tv_avg_gpu_usage)
|
||||
val maxGpuClockText = dialogView.findViewById<TextView>(R.id.tv_max_gpu_clock)
|
||||
val minGpuClockText = dialogView.findViewById<TextView>(R.id.tv_min_gpu_clock)
|
||||
val avgGpuClockText = dialogView.findViewById<TextView>(R.id.tv_avg_gpu_clock)
|
||||
val maxGpuTempText = dialogView.findViewById<TextView>(R.id.tv_max_gpu_temp)
|
||||
val minGpuTempText = dialogView.findViewById<TextView>(R.id.tv_min_gpu_temp)
|
||||
val avgGpuTempText = dialogView.findViewById<TextView>(R.id.tv_avg_gpu_temp)
|
||||
|
||||
// Set GPU graph data
|
||||
gpuUsageGraphView.setData(analytics.gpuUsageTimeData, analytics.gpuStats.avgUsage)
|
||||
gpuTempGraphView.setData(analytics.gpuTempTimeData, analytics.gpuStats.avgTemp)
|
||||
gpuClockGraphView.setData(analytics.gpuClockTimeData, analytics.gpuStats.avgClock)
|
||||
|
||||
// Set GPU statistics
|
||||
maxGpuUsageText.text = String.format("Max Usage: %.0f%%", analytics.gpuStats.maxUsage)
|
||||
minGpuUsageText.text = String.format("Min Usage: %.0f%%", analytics.gpuStats.minUsage)
|
||||
avgGpuUsageText.text = String.format("Avg Usage: %.1f%%", analytics.gpuStats.avgUsage)
|
||||
maxGpuClockText.text = String.format("Max Clock: %.0f MHz", analytics.gpuStats.maxClock)
|
||||
minGpuClockText.text = String.format("Min Clock: %.0f MHz", analytics.gpuStats.minClock)
|
||||
avgGpuClockText.text = String.format("Avg Clock: %.0f MHz", analytics.gpuStats.avgClock)
|
||||
maxGpuTempText.text = String.format("Max Temp: %.1f°C", analytics.gpuStats.maxTemp)
|
||||
minGpuTempText.text = String.format("Min Temp: %.1f°C", analytics.gpuStats.minTemp)
|
||||
avgGpuTempText.text = String.format("Avg Temp: %.1f°C", analytics.gpuStats.avgTemp)
|
||||
|
||||
// Create and show dialog with menu
|
||||
val dialog = AlertDialog.Builder(requireContext())
|
||||
.setTitle("📊 Session Analytics")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("⋮ Actions") { _, _ ->
|
||||
// Will be overridden
|
||||
}
|
||||
.setNegativeButton("Close", null)
|
||||
.create()
|
||||
|
||||
dialog.show()
|
||||
|
||||
// Override the Actions button to show menu
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
showActionsMenu(it, dialogView, analytics, logFile)
|
||||
}
|
||||
|
||||
// Make sure dialog is wide enough
|
||||
dialog.window?.setLayout(
|
||||
(resources.displayMetrics.widthPixels * 0.95).toInt(),
|
||||
android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
private fun showActionsMenu(
|
||||
anchorView: View,
|
||||
dialogView: View,
|
||||
analytics: LogAnalytics,
|
||||
logFile: GameBarLogFragment.LogFile
|
||||
) {
|
||||
val popup = android.widget.PopupMenu(requireContext(), anchorView)
|
||||
popup.menu.apply {
|
||||
add(0, 1, 1, "📊 Export Data (CSV)")
|
||||
add(0, 2, 2, "📸 Save Graphics (PNG)")
|
||||
add(0, 3, 3, "📤 Share Data (CSV)")
|
||||
add(0, 4, 4, "🖼️ Share Graphics (PNG)")
|
||||
}
|
||||
|
||||
popup.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
1 -> {
|
||||
exportLogFile(logFile)
|
||||
true
|
||||
}
|
||||
2 -> {
|
||||
saveGraphicsAsImage(dialogView, analytics, logFile)
|
||||
true
|
||||
}
|
||||
3 -> {
|
||||
shareLogFile(logFile)
|
||||
true
|
||||
}
|
||||
4 -> {
|
||||
shareGraphicsAsImage(dialogView, analytics, logFile)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
popup.show()
|
||||
}
|
||||
|
||||
private fun saveGraphicsAsImage(view: View, analytics: LogAnalytics, logFile: GameBarLogFragment.LogFile) {
|
||||
try {
|
||||
// Ensure view is properly measured and laid out
|
||||
view.post {
|
||||
try {
|
||||
// Create bitmap from view
|
||||
val bitmap = captureViewAsBitmap(view)
|
||||
|
||||
// Save to external storage
|
||||
val timeStamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.getDefault()).format(java.util.Date())
|
||||
val fileName = "GameBar_Stats_${appName}_$timeStamp.png"
|
||||
val file = File(android.os.Environment.getExternalStorageDirectory(), fileName)
|
||||
|
||||
val fos = java.io.FileOutputStream(file)
|
||||
bitmap.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, fos)
|
||||
fos.flush()
|
||||
fos.close()
|
||||
|
||||
Toast.makeText(requireContext(), "Saved: ${file.absolutePath}", Toast.LENGTH_LONG).show()
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "Failed to save: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "Error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareGraphicsAsImage(view: View, analytics: LogAnalytics, logFile: GameBarLogFragment.LogFile) {
|
||||
try {
|
||||
// Ensure view is properly measured and laid out
|
||||
view.post {
|
||||
try {
|
||||
// Create bitmap from view
|
||||
val bitmap = captureViewAsBitmap(view)
|
||||
|
||||
// Save to cache directory
|
||||
val timeStamp = java.text.SimpleDateFormat("yyyyMMdd_HHmmss", java.util.Locale.getDefault()).format(java.util.Date())
|
||||
val fileName = "GameBar_Stats_${appName}_$timeStamp.png"
|
||||
val file = File(requireContext().cacheDir, fileName)
|
||||
|
||||
val fos = java.io.FileOutputStream(file)
|
||||
bitmap.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, fos)
|
||||
fos.flush()
|
||||
fos.close()
|
||||
|
||||
view.isDrawingCacheEnabled = false
|
||||
|
||||
// Share the image
|
||||
val uri = FileProvider.getUriForFile(
|
||||
requireContext(),
|
||||
"${requireContext().packageName}.fileprovider",
|
||||
file
|
||||
)
|
||||
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = "image/png"
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, "GameBar Performance Stats - $appName")
|
||||
intent.putExtra(Intent.EXTRA_TEXT, buildString {
|
||||
appendLine("🎮 GameBar Performance Stats")
|
||||
appendLine("")
|
||||
appendLine("📱 App: $appName")
|
||||
appendLine("📅 Session: ${analytics.sessionDate}")
|
||||
appendLine("⏱️ Duration: ${analytics.sessionDuration}")
|
||||
appendLine("")
|
||||
appendLine("🎯 FPS: Avg ${String.format("%.1f", analytics.fpsStats.avgFps)} | Max ${String.format("%.1f", analytics.fpsStats.maxFps)}")
|
||||
appendLine("✨ Smoothness: ${String.format("%.1f%%", analytics.fpsStats.smoothnessPercentage)}")
|
||||
appendLine("🔥 1% Low: ${String.format("%.1f", analytics.fpsStats.fps1PercentLow)} FPS")
|
||||
})
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
val chooser = Intent.createChooser(intent, "Share Graphics")
|
||||
startActivity(chooser)
|
||||
|
||||
Toast.makeText(requireContext(), "Graphics ready to share!", Toast.LENGTH_SHORT).show()
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "Failed to share: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "Error: ${e.message}", Toast.LENGTH_LONG).show()
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun captureViewAsBitmap(view: View): android.graphics.Bitmap {
|
||||
// The root view of the dialog is a ScrollView, get its content
|
||||
val contentView = if (view is android.widget.ScrollView) {
|
||||
// Get the LinearLayout inside the ScrollView
|
||||
view.getChildAt(0)
|
||||
} else {
|
||||
view
|
||||
}
|
||||
|
||||
// Measure the full content size (not just what's visible)
|
||||
contentView.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(contentView.width, View.MeasureSpec.EXACTLY),
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
||||
)
|
||||
|
||||
// Create bitmap with full content dimensions
|
||||
val bitmap = android.graphics.Bitmap.createBitmap(
|
||||
contentView.width,
|
||||
contentView.measuredHeight,
|
||||
android.graphics.Bitmap.Config.ARGB_8888
|
||||
)
|
||||
|
||||
val canvas = android.graphics.Canvas(bitmap)
|
||||
|
||||
// Draw the entire content including off-screen parts
|
||||
contentView.layout(
|
||||
contentView.left,
|
||||
contentView.top,
|
||||
contentView.right,
|
||||
contentView.bottom + contentView.measuredHeight
|
||||
)
|
||||
contentView.draw(canvas)
|
||||
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private fun shareLogFile(logFile: GameBarLogFragment.LogFile) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
val file = File(logFile.path)
|
||||
|
||||
val uri = FileProvider.getUriForFile(
|
||||
requireContext(),
|
||||
"${requireContext().packageName}.fileprovider",
|
||||
file
|
||||
)
|
||||
|
||||
intent.type = "text/csv"
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, "GameBar Performance Log for $appName")
|
||||
intent.putExtra(Intent.EXTRA_TEXT, "GameBar performance log file for $appName: ${logFile.name}")
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
val chooser = Intent.createChooser(intent, "Share log file")
|
||||
startActivity(chooser)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "File location: ${logFile.path}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportLogFile(logFile: GameBarLogFragment.LogFile) {
|
||||
Toast.makeText(requireContext(), "File saved at: ${logFile.path}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun deleteLogFile(logFile: GameBarLogFragment.LogFile) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle("Delete Log File")
|
||||
.setMessage("Are you sure you want to delete this log file?\\n\\n${logFile.name}")
|
||||
.setPositiveButton("Delete") { _, _ ->
|
||||
try {
|
||||
val file = File(logFile.path)
|
||||
if (file.delete()) {
|
||||
Toast.makeText(requireContext(), "Log file deleted", Toast.LENGTH_SHORT).show()
|
||||
loadPerAppLogHistory() // Refresh the list
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Failed to delete log file", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(requireContext(), "Error deleting file: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun formatFileSize(bytes: Long): String {
|
||||
val kb = bytes / 1024.0
|
||||
val mb = kb / 1024.0
|
||||
|
||||
return when {
|
||||
mb >= 1 -> String.format("%.1f MB", mb)
|
||||
kb >= 1 -> String.format("%.1f KB", kb)
|
||||
else -> "$bytes bytes"
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatDate(timestamp: Long): String {
|
||||
val dateFormat = SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault())
|
||||
return dateFormat.format(Date(timestamp))
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
loadPerAppLogHistory()
|
||||
}
|
||||
}
|
||||
211
src/com/android/gamebar/utils/FileUtils.java
Normal file
211
src/com/android/gamebar/utils/FileUtils.java
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: The LineageOS Project
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar.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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
502
src/com/android/gamebar/utils/PartsCustomSeekBarPreference.java
Normal file
502
src/com/android/gamebar/utils/PartsCustomSeekBarPreference.java
Normal file
@@ -0,0 +1,502 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2017 The Dirty Unicorns Project
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.android.gamebar.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.PorterDuff;
|
||||
import androidx.preference.*;
|
||||
import androidx.core.content.res.TypedArrayUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.gamebar.R;
|
||||
import com.android.settingslib.widget.SettingsThemeHelper;
|
||||
|
||||
import com.google.android.material.slider.LabelFormatter;
|
||||
import com.google.android.material.slider.Slider;
|
||||
|
||||
public class PartsCustomSeekBarPreference extends Preference implements Slider.OnChangeListener,
|
||||
Slider.OnSliderTouchListener, View.OnClickListener, View.OnLongClickListener {
|
||||
protected final String TAG = getClass().getName();
|
||||
private static final String SETTINGS_NS = "http://schemas.android.com/apk/res/com.android.settings";
|
||||
private static final String SETTINGS_NS_ALT = "http://schemas.android.com/apk/res-auto";
|
||||
protected static final String ANDROIDNS = "http://schemas.android.com/apk/res/android";
|
||||
|
||||
protected int mInterval = 1;
|
||||
protected boolean mShowSign = false;
|
||||
protected String mUnits = "";
|
||||
protected boolean mContinuousUpdates = false;
|
||||
|
||||
protected int mMinValue = 0;
|
||||
protected int mMaxValue = 100;
|
||||
protected boolean mDefaultValueExists = false;
|
||||
protected int mDefaultValue;
|
||||
|
||||
protected int mValue;
|
||||
|
||||
protected TextView mValueTextView;
|
||||
protected ImageView mResetImageView;
|
||||
protected ImageView mMinusImageView;
|
||||
protected ImageView mPlusImageView;
|
||||
protected Slider mSlider;
|
||||
|
||||
protected boolean mTrackingTouch = false;
|
||||
protected int mTrackingValue;
|
||||
|
||||
// Custom value mapping for non-linear values (like time durations)
|
||||
protected int[] mCustomValues = null;
|
||||
protected String[] mCustomLabels = null;
|
||||
|
||||
public PartsCustomSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PartsCustomSeekBarPreference);
|
||||
try {
|
||||
mShowSign = a.getBoolean(R.styleable.PartsCustomSeekBarPreference_showSign, mShowSign);
|
||||
String units = a.getString(R.styleable.PartsCustomSeekBarPreference_units);
|
||||
if (units != null)
|
||||
mUnits = " " + units;
|
||||
mContinuousUpdates = a.getBoolean(
|
||||
R.styleable.PartsCustomSeekBarPreference_continuousUpdates, false);
|
||||
} finally {
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
String newInterval = attrs.getAttributeValue(SETTINGS_NS, "interval");
|
||||
if (newInterval != null) {
|
||||
mInterval = Integer.parseInt(newInterval);
|
||||
}
|
||||
if (newInterval == null) {
|
||||
newInterval = attrs.getAttributeValue(SETTINGS_NS_ALT, "interval");
|
||||
if (newInterval != null) mInterval = Integer.parseInt(newInterval);
|
||||
}
|
||||
if (newInterval == null) {
|
||||
newInterval = attrs.getAttributeValue(ANDROIDNS, "interval");
|
||||
if (newInterval != null) mInterval = Integer.parseInt(newInterval);
|
||||
}
|
||||
|
||||
mMinValue = attrs.getAttributeIntValue(SETTINGS_NS, "min", mMinValue);
|
||||
if (mMinValue == 0) {
|
||||
int min = attrs.getAttributeIntValue(SETTINGS_NS_ALT, "min", mMinValue);
|
||||
if (min != 0) mMinValue = min;
|
||||
}
|
||||
if (mMinValue == 0) {
|
||||
int min = attrs.getAttributeIntValue(ANDROIDNS, "min", mMinValue);
|
||||
if (min != 0) mMinValue = min;
|
||||
}
|
||||
|
||||
mMaxValue = attrs.getAttributeIntValue(ANDROIDNS, "max", mMaxValue);
|
||||
if (mMaxValue == 100) {
|
||||
int max = attrs.getAttributeIntValue(SETTINGS_NS, "max", mMaxValue);
|
||||
if (max != 100) mMaxValue = max;
|
||||
}
|
||||
if (mMaxValue == 100) {
|
||||
int max = attrs.getAttributeIntValue(SETTINGS_NS_ALT, "max", mMaxValue);
|
||||
if (max != 100) mMaxValue = max;
|
||||
}
|
||||
if (mMaxValue < mMinValue)
|
||||
mMaxValue = mMinValue;
|
||||
|
||||
String defaultValue = attrs.getAttributeValue(ANDROIDNS, "defaultValue");
|
||||
mDefaultValueExists = defaultValue != null && !defaultValue.isEmpty();
|
||||
if (!mDefaultValueExists) {
|
||||
defaultValue = attrs.getAttributeValue(SETTINGS_NS, "defaultValue");
|
||||
mDefaultValueExists = defaultValue != null && !defaultValue.isEmpty();
|
||||
}
|
||||
if (!mDefaultValueExists) {
|
||||
defaultValue = attrs.getAttributeValue(SETTINGS_NS_ALT, "defaultValue");
|
||||
mDefaultValueExists = defaultValue != null && !defaultValue.isEmpty();
|
||||
}
|
||||
if (mDefaultValueExists) {
|
||||
mDefaultValue = getLimitedValue(Integer.parseInt(defaultValue));
|
||||
mValue = mDefaultValue;
|
||||
} else {
|
||||
mValue = mMinValue;
|
||||
}
|
||||
|
||||
Context materialContext = new ContextThemeWrapper(context,
|
||||
com.google.android.material.R.style.Theme_MaterialComponents_DayNight);
|
||||
mSlider = new Slider(materialContext, attrs);
|
||||
|
||||
setLayoutResource(R.layout.preference_custom_seekbar);
|
||||
}
|
||||
|
||||
public PartsCustomSeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
this(context, attrs, defStyleAttr, 0);
|
||||
}
|
||||
|
||||
public PartsCustomSeekBarPreference(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, TypedArrayUtils.getAttr(context,
|
||||
androidx.preference.R.attr.preferenceStyle,
|
||||
android.R.attr.preferenceStyle));
|
||||
}
|
||||
|
||||
public PartsCustomSeekBarPreference(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
try
|
||||
{
|
||||
// move our seekbar to the new view we've been given
|
||||
ViewParent oldContainer = mSlider.getParent();
|
||||
ViewGroup newContainer = (ViewGroup) holder.findViewById(R.id.seekbar);
|
||||
if (oldContainer != newContainer) {
|
||||
// remove the seekbar from the old view
|
||||
if (oldContainer != null) {
|
||||
((ViewGroup) oldContainer).removeView(mSlider);
|
||||
}
|
||||
// remove the existing seekbar (there may not be one) and add ours
|
||||
newContainer.removeAllViews();
|
||||
newContainer.addView(mSlider, ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Log.e(TAG, "Error binding view", ex);
|
||||
}
|
||||
|
||||
mSlider.setValueTo(mMaxValue);
|
||||
mSlider.setValueFrom(mMinValue);
|
||||
mSlider.setValue(mValue);
|
||||
mSlider.setEnabled(isEnabled());
|
||||
mSlider.setLabelBehavior(LabelFormatter.LABEL_GONE);
|
||||
mSlider.setTickVisible(false);
|
||||
if (mInterval > 0) {
|
||||
mSlider.setStepSize(mInterval);
|
||||
} else {
|
||||
Log.w(TAG, "Step size is zero or invalid: " + mInterval);
|
||||
}
|
||||
|
||||
// Set up slider color
|
||||
mSlider.setTrackActiveTintList(getContext().getColorStateList(
|
||||
com.android.settingslib.widget.preference.slider.R.color
|
||||
.settingslib_expressive_color_slider_track_active));
|
||||
mSlider.setTrackInactiveTintList(getContext().getColorStateList(
|
||||
com.android.settingslib.widget.preference.slider.R.color
|
||||
.settingslib_expressive_color_slider_track_inactive));
|
||||
mSlider.setThumbTintList(getContext().getColorStateList(
|
||||
com.android.settingslib.widget.preference.slider.R.color
|
||||
.settingslib_expressive_color_slider_thumb));
|
||||
mSlider.setHaloTintList(getContext().getColorStateList(
|
||||
com.android.settingslib.widget.preference.slider.R.color
|
||||
.settingslib_expressive_color_slider_halo));
|
||||
mSlider.setTickActiveTintList(getContext().getColorStateList(
|
||||
com.android.settingslib.widget.preference.slider.R.color
|
||||
.settingslib_expressive_color_slider_track_active));
|
||||
mSlider.setTickInactiveTintList(getContext().getColorStateList(
|
||||
com.android.settingslib.widget.preference.slider.R.color
|
||||
.settingslib_expressive_color_slider_track_inactive));
|
||||
|
||||
// Set up slider size
|
||||
if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
|
||||
Resources res = getContext().getResources();
|
||||
mSlider.setTrackHeight(res.getDimensionPixelSize(
|
||||
com.android.settingslib.widget.preference.slider.R.dimen
|
||||
.settingslib_expressive_slider_track_height));
|
||||
mSlider.setTrackInsideCornerSize(res.getDimensionPixelSize(
|
||||
com.android.settingslib.widget.preference.slider.R.dimen
|
||||
.settingslib_expressive_slider_track_inside_corner_size));
|
||||
mSlider.setTrackStopIndicatorSize(res.getDimensionPixelSize(
|
||||
com.android.settingslib.widget.preference.slider.R.dimen
|
||||
.settingslib_expressive_slider_track_stop_indicator_size));
|
||||
mSlider.setThumbWidth(res.getDimensionPixelSize(
|
||||
com.android.settingslib.widget.preference.slider.R.dimen
|
||||
.settingslib_expressive_slider_thumb_width));
|
||||
mSlider.setThumbHeight(res.getDimensionPixelSize(
|
||||
com.android.settingslib.widget.preference.slider.R.dimen
|
||||
.settingslib_expressive_slider_thumb_height));
|
||||
mSlider.setThumbElevation(res.getDimensionPixelSize(
|
||||
com.android.settingslib.widget.preference.slider.R.dimen
|
||||
.settingslib_expressive_slider_thumb_elevation));
|
||||
mSlider.setThumbStrokeWidth(res.getDimensionPixelSize(
|
||||
com.android.settingslib.widget.preference.slider.R.dimen
|
||||
.settingslib_expressive_slider_thumb_stroke_width));
|
||||
mSlider.setThumbTrackGapSize(res.getDimensionPixelSize(
|
||||
com.android.settingslib.widget.preference.slider.R.dimen
|
||||
.settingslib_expressive_slider_thumb_track_gap_size));
|
||||
mSlider.setTickActiveRadius(res.getDimensionPixelSize(
|
||||
com.android.settingslib.widget.preference.slider.R.dimen
|
||||
.settingslib_expressive_slider_tick_radius));
|
||||
mSlider.setTickInactiveRadius(res.getDimensionPixelSize(
|
||||
com.android.settingslib.widget.preference.slider.R
|
||||
.dimen.settingslib_expressive_slider_tick_radius));
|
||||
}
|
||||
|
||||
mValueTextView = (TextView) holder.findViewById(R.id.value);
|
||||
mResetImageView = (ImageView) holder.findViewById(R.id.reset);
|
||||
mMinusImageView = (ImageView) holder.findViewById(R.id.minus);
|
||||
mPlusImageView = (ImageView) holder.findViewById(R.id.plus);
|
||||
|
||||
updateValueViews();
|
||||
|
||||
mSlider.addOnChangeListener(this);
|
||||
mSlider.addOnSliderTouchListener(this);
|
||||
mResetImageView.setOnClickListener(this);
|
||||
mMinusImageView.setOnClickListener(this);
|
||||
mPlusImageView.setOnClickListener(this);
|
||||
mResetImageView.setOnLongClickListener(this);
|
||||
mMinusImageView.setOnLongClickListener(this);
|
||||
mPlusImageView.setOnLongClickListener(this);
|
||||
}
|
||||
|
||||
protected int getLimitedValue(int v) {
|
||||
return v < mMinValue ? mMinValue : (v > mMaxValue ? mMaxValue : v);
|
||||
}
|
||||
|
||||
protected String getTextValue(int v) {
|
||||
if (mCustomLabels != null && v >= 0 && v < mCustomLabels.length) {
|
||||
return mCustomLabels[v];
|
||||
}
|
||||
return (mShowSign && v > 0 ? "+" : "") + String.valueOf(v) + mUnits;
|
||||
}
|
||||
|
||||
protected void updateValueViews() {
|
||||
if (mValueTextView != null) {
|
||||
String add = "";
|
||||
if (mDefaultValueExists && mValue == mDefaultValue) {
|
||||
add = " (" + getContext().getString(
|
||||
R.string.custom_seekbar_default_value) + ")";
|
||||
}
|
||||
String textValue = getTextValue(mValue) + add;
|
||||
if (mTrackingTouch && !mContinuousUpdates) {
|
||||
textValue = getTextValue(mTrackingValue);
|
||||
}
|
||||
mValueTextView.setText(getContext().getString(
|
||||
R.string.custom_seekbar_value, textValue));
|
||||
}
|
||||
|
||||
if (mResetImageView != null) {
|
||||
if (!mDefaultValueExists || mValue == mDefaultValue || mTrackingTouch)
|
||||
mResetImageView.setVisibility(View.INVISIBLE);
|
||||
else
|
||||
mResetImageView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if (mMinusImageView != null) {
|
||||
if (mValue == mMinValue || mTrackingTouch) {
|
||||
mMinusImageView.setClickable(false);
|
||||
mMinusImageView.setColorFilter(getContext().getColor(R.color.disabled_text_color),
|
||||
PorterDuff.Mode.MULTIPLY);
|
||||
} else {
|
||||
mMinusImageView.setClickable(true);
|
||||
mMinusImageView.clearColorFilter();
|
||||
}
|
||||
}
|
||||
|
||||
if (mPlusImageView != null) {
|
||||
if (mValue == mMaxValue || mTrackingTouch) {
|
||||
mPlusImageView.setClickable(false);
|
||||
mPlusImageView.setColorFilter(getContext().getColor(R.color.disabled_text_color),
|
||||
PorterDuff.Mode.MULTIPLY);
|
||||
} else {
|
||||
mPlusImageView.setClickable(true);
|
||||
mPlusImageView.clearColorFilter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void changeValue(int newValue) {
|
||||
// for subclasses
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueChange(Slider slider, float value, boolean fromUser) {
|
||||
int newValue = getLimitedValue(Math.round(value));
|
||||
if (mTrackingTouch && !mContinuousUpdates) {
|
||||
mTrackingValue = newValue;
|
||||
} else if (mValue != newValue) {
|
||||
// change rejected, revert to the previous value
|
||||
if (!callChangeListener(newValue)) {
|
||||
mSlider.setValue(mValue);
|
||||
return;
|
||||
}
|
||||
// change accepted, store it
|
||||
changeValue(newValue);
|
||||
persistInt(newValue);
|
||||
|
||||
mValue = newValue;
|
||||
}
|
||||
updateValueViews();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(Slider slider) {
|
||||
mTrackingValue = mValue;
|
||||
mTrackingTouch = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(Slider slider) {
|
||||
mTrackingTouch = false;
|
||||
if (!mContinuousUpdates)
|
||||
onValueChange(mSlider, mTrackingValue, false);
|
||||
notifyChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int id = v.getId();
|
||||
if (id == R.id.reset) {
|
||||
Toast.makeText(getContext(), getContext().getString(
|
||||
R.string.custom_seekbar_default_value_to_set, getTextValue(mDefaultValue)),
|
||||
Toast.LENGTH_LONG).show();
|
||||
} else if (id == R.id.minus) {
|
||||
setValue(mValue - mInterval, true);
|
||||
} else if (id == R.id.plus) {
|
||||
setValue(mValue + mInterval, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
int id = v.getId();
|
||||
if (id == R.id.reset) {
|
||||
setValue(mDefaultValue, true);
|
||||
} else if (id == R.id.minus) {
|
||||
int value = mMinValue;
|
||||
if (mMaxValue - mMinValue > mInterval * 2 && mMaxValue + mMinValue < mValue * 2) {
|
||||
value = Math.floorDiv(mMaxValue + mMinValue, 2);
|
||||
}
|
||||
setValue(value, true);
|
||||
} else if (id == R.id.plus) {
|
||||
int value = mMaxValue;
|
||||
if (mMaxValue - mMinValue > mInterval * 2 && mMaxValue + mMinValue > mValue * 2) {
|
||||
value = -1 * Math.floorDiv(-1 * (mMaxValue + mMinValue), 2);
|
||||
}
|
||||
setValue(value, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
|
||||
if (restoreValue)
|
||||
mValue = getPersistedInt(mValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDefaultValue(Object defaultValue) {
|
||||
if (defaultValue instanceof Integer)
|
||||
setDefaultValue((Integer) defaultValue, mSlider != null);
|
||||
else
|
||||
setDefaultValue(defaultValue == null ? (String) null : defaultValue.toString(), mSlider != null);
|
||||
}
|
||||
|
||||
public void setDefaultValue(int newValue, boolean update) {
|
||||
newValue = getLimitedValue(newValue);
|
||||
if (!mDefaultValueExists || mDefaultValue != newValue) {
|
||||
mDefaultValueExists = true;
|
||||
mDefaultValue = newValue;
|
||||
if (update)
|
||||
updateValueViews();
|
||||
}
|
||||
}
|
||||
|
||||
public void setDefaultValue(String newValue, boolean update) {
|
||||
if (mDefaultValueExists && (newValue == null || newValue.isEmpty())) {
|
||||
mDefaultValueExists = false;
|
||||
if (update)
|
||||
updateValueViews();
|
||||
} else if (newValue != null && !newValue.isEmpty()) {
|
||||
setDefaultValue(Integer.parseInt(newValue), update);
|
||||
}
|
||||
}
|
||||
|
||||
public void setMax(int max) {
|
||||
mMaxValue = max;
|
||||
if (mSlider != null) mSlider.setValueTo(mMaxValue);
|
||||
}
|
||||
|
||||
public int getMax() {
|
||||
return mMaxValue;
|
||||
}
|
||||
|
||||
public void setMin(int min) {
|
||||
mMinValue = min;
|
||||
if (mSlider != null) mSlider.setValueFrom(mMinValue);
|
||||
}
|
||||
|
||||
public void setValue(int newValue) {
|
||||
mValue = getLimitedValue(newValue);
|
||||
if (mSlider != null) mSlider.setValue(mValue);
|
||||
onValueChange(mSlider, mValue, false);
|
||||
notifyChanged();
|
||||
}
|
||||
|
||||
public void setValue(int newValue, boolean update) {
|
||||
newValue = getLimitedValue(newValue);
|
||||
if (mValue != newValue) {
|
||||
if (!callChangeListener(newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mValue = newValue;
|
||||
persistInt(newValue);
|
||||
changeValue(newValue); // if needed
|
||||
if (update && mSlider != null)
|
||||
mSlider.setValue(newValue);
|
||||
|
||||
updateValueViews();
|
||||
notifyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public void setUnits(String units) {
|
||||
mUnits = units;
|
||||
updateValueViews();
|
||||
}
|
||||
|
||||
public String getUnits() {
|
||||
return mUnits;
|
||||
}
|
||||
|
||||
// Custom value mapping methods for time durations and other non-linear scales
|
||||
public void setCustomValues(int[] values, String[] labels) {
|
||||
mCustomValues = values;
|
||||
mCustomLabels = labels;
|
||||
if (values != null && values.length > 0) {
|
||||
setMax(values.length - 1);
|
||||
setMin(0);
|
||||
}
|
||||
updateValueViews();
|
||||
}
|
||||
|
||||
public int getMappedValue() {
|
||||
if (mCustomValues != null && mValue >= 0 && mValue < mCustomValues.length) {
|
||||
return mCustomValues[mValue];
|
||||
}
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public void refresh(int newValue) {
|
||||
// Update the value without triggering change listeners to avoid infinite recursion
|
||||
mValue = getLimitedValue(newValue);
|
||||
if (mSlider != null) {
|
||||
// Temporarily remove listener to prevent triggering onValueChange
|
||||
mSlider.removeOnChangeListener(this);
|
||||
mSlider.setValue(mValue);
|
||||
mSlider.addOnChangeListener(this);
|
||||
}
|
||||
updateValueViews();
|
||||
persistInt(mValue);
|
||||
}
|
||||
}
|
||||
79
src/com/android/gamebar/utils/TileHandlerActivity.java
Normal file
79
src/com/android/gamebar/utils/TileHandlerActivity.java
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 kenway214
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package com.android.gamebar.utils;
|
||||
|
||||
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 com.android.gamebar.GameBarSettingsActivity;
|
||||
import com.android.gamebar.GameBarTileService;
|
||||
|
||||
public final class TileHandlerActivity extends Activity {
|
||||
private static final String TAG = "TileHandlerActivity";
|
||||
|
||||
// Map QS Tile services to their corresponding activity
|
||||
private static final Map<String, Class<?>> TILE_ACTIVITY_MAP = new HashMap<>();
|
||||
|
||||
static {
|
||||
TILE_ACTIVITY_MAP.put(GameBarTileService.class.getName(), GameBarSettingsActivity.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();
|
||||
}
|
||||
}
|
||||
52
src/com/android/gamebar/utils/TileUtils.java
Normal file
52
src/com/android/gamebar/utils/TileUtils.java
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: The LineageOS Project
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
|
||||
package com.android.gamebar.utils;
|
||||
|
||||
import android.app.StatusBarManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.gamebar.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user