├── app
├── proguard-rules.pro
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── colors.xml
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── strings.xml
│ │ │ └── themes.xml
│ │ ├── font
│ │ │ └── jetbrainsmono.ttf
│ │ ├── mipmap-hdpi
│ │ │ ├── icon_round.png
│ │ │ ├── noti_icon.png
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-mdpi
│ │ │ ├── icon_round.png
│ │ │ ├── noti_icon.png
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── icon_round.png
│ │ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── icon_round.png
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_foreground.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── icon_round.png
│ │ │ └── ic_launcher_foreground.png
│ │ ├── anim
│ │ │ ├── anim_left.xml
│ │ │ ├── anim_left_sen.xml
│ │ │ ├── anim_right.xml
│ │ │ └── anim_right_sen.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ └── ic_launcher.xml
│ │ ├── drawable
│ │ │ ├── arrow_forward.xml
│ │ │ ├── arrow_back.xml
│ │ │ ├── swap.xml
│ │ │ ├── current.xml
│ │ │ ├── apps.xml
│ │ │ ├── auto_done.xml
│ │ │ ├── device.xml
│ │ │ ├── warning.xml
│ │ │ ├── analysis.xml
│ │ │ ├── monitor.xml
│ │ │ ├── usage.xml
│ │ │ ├── charging.xml
│ │ │ ├── settings.xml
│ │ │ └── app_settings.xml
│ │ ├── drawable-night
│ │ │ ├── arrow_forward.xml
│ │ │ ├── arrow_back.xml
│ │ │ ├── swap.xml
│ │ │ ├── current.xml
│ │ │ ├── apps.xml
│ │ │ ├── auto_done.xml
│ │ │ ├── device.xml
│ │ │ ├── warning.xml
│ │ │ ├── analysis.xml
│ │ │ ├── monitor.xml
│ │ │ ├── usage.xml
│ │ │ ├── charging.xml
│ │ │ ├── settings.xml
│ │ │ └── app_settings.xml
│ │ └── xml
│ │ │ └── configurate.xml
│ │ ├── java
│ │ └── cumulus
│ │ │ └── battery
│ │ │ └── stats
│ │ │ ├── ui
│ │ │ └── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Type.kt
│ │ │ │ └── Theme.kt
│ │ │ ├── utils
│ │ │ ├── misc.kt
│ │ │ ├── BatteryDataSQLHelper.kt
│ │ │ └── BatteryStatsRecordAnalysis.kt
│ │ │ ├── objects
│ │ │ ├── CpuStatsProvider.kt
│ │ │ ├── BatteryStatsRecorder.kt
│ │ │ └── BatteryStatsProvider.kt
│ │ │ ├── BackgroundService.kt
│ │ │ ├── FloatMonitorService.kt
│ │ │ ├── widgets
│ │ │ ├── CumulusWidgets.kt
│ │ │ └── CumulusCharts.kt
│ │ │ ├── AdditionalFunctionActivity.kt
│ │ │ ├── SettingsActivity.kt
│ │ │ ├── CurrentAdjustActivity.kt
│ │ │ ├── MainActivity.kt
│ │ │ └── PowerConsumptionAnalysisActivity.kt
│ │ └── AndroidManifest.xml
└── build.gradle.kts
├── .gitattributes
├── README.md
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── .gitignore
├── gradle.properties
├── settings.gradle.kts
├── gradlew.bat
└── gradlew
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cumulus-APP
2 | A Simple Android Battery Monitoring APP.
3 | Opensourced under GPL3 License.
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/font/jetbrainsmono.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/font/jetbrainsmono.ttf
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/icon_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-hdpi/icon_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/noti_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-hdpi/noti_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/icon_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-mdpi/icon_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/noti_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-mdpi/noti_icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/icon_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-xhdpi/icon_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/icon_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-xxhdpi/icon_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/icon_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-xxxhdpi/icon_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chenzyadb/Cumulus-APP/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Cumulus 后台服务\n此服务用于收集前台应用包名和电池数据.
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/anim_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/anim_left_sen.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/anim_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/anim_right_sen.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Jun 23 17:20:10 CST 2025
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.9-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_forward.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/arrow_forward.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_back.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/arrow_back.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/swap.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/swap.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/current.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/current.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/apps.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/apps.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/auto_done.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/auto_done.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/device.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/warning.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/device.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/warning.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/analysis.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/analysis.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/monitor.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/monitor.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/configurate.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Gradle files
2 | .gradle/
3 | build/
4 |
5 | # Local configuration file (sdk path, etc)
6 | local.properties
7 |
8 | # Log/OS Files
9 | *.log
10 |
11 | # Android Studio generated files and folders
12 | release/
13 | captures/
14 | .externalNativeBuild/
15 | .cxx/
16 | .kotlin/
17 | *.apk
18 | output.json
19 |
20 | # IntelliJ
21 | *.iml
22 | .idea/
23 | misc.xml
24 | deploymentTargetDropDown.xml
25 | render.experimental.xml
26 |
27 | # Keystore files
28 | *.jks
29 | *.keystore
30 |
31 | # Google Services (e.g. APIs or Firebase)
32 | google-services.json
33 |
34 | # Android Profiling
35 | *.hprof
36 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/usage.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/usage.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/charging.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/charging.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.graphics.Color
6 |
7 | open class CumulusColor(
8 | val blue: Color,
9 | val yellow: Color,
10 | val pink: Color,
11 | val purple: Color
12 | )
13 |
14 | object CumulusLightColor : CumulusColor(
15 | blue = Color(0xFF1A6AE6),
16 | yellow = Color(0xFFFFB000),
17 | pink = Color(0xFF9A4675),
18 | purple = Color(0xFF4F3E9C)
19 | )
20 |
21 | object CumulusDarkColor : CumulusColor(
22 | blue = Color(0xFF3D8AFF),
23 | yellow = Color(0xFFFFA500),
24 | pink = Color(0xFFC05F97),
25 | purple = Color(0xFF6A5ACD)
26 | )
27 |
28 | @Composable
29 | fun cumulusColor(): CumulusColor {
30 | return when {
31 | isSystemInDarkTheme() -> CumulusDarkColor
32 | else -> CumulusLightColor
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/utils/misc.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats.utils
2 |
3 | import android.os.Build
4 | import java.time.LocalDateTime
5 | import java.time.ZoneOffset
6 | import java.util.Calendar
7 | import java.util.TimeZone
8 |
9 | fun GetTimeStamp(): Long {
10 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
11 | val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"))
12 | return (calendar.timeInMillis / 1000)
13 | }
14 | val currentDateTime = LocalDateTime.now()
15 | return currentDateTime.toInstant(ZoneOffset.UTC).epochSecond
16 | }
17 |
18 | fun DurationToText(duration: Long): String {
19 | if (duration >= 60) {
20 | var text = ""
21 | val day = duration / 3600 / 24
22 | if (day > 0) {
23 | text += "${day}天"
24 | }
25 | val hour = (duration / 3600) % 24
26 | if (hour > 0) {
27 | text += "${hour}小时"
28 | }
29 | val minute = (duration / 60) % 60
30 | if (minute > 0) {
31 | text += "${minute}分钟"
32 | }
33 | return text
34 | }
35 | return "小于1分钟"
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/settings.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/settings.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/app_settings.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-night/app_settings.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | appcompat = "1.6.1"
3 | core-ktx = "1.13.0"
4 | compose = "1.6.7"
5 | compose-material3 = "1.2.1"
6 | compose-material = "1.6.3"
7 | runtime-ktx = "2.7.0"
8 | activity-compose = "1.8.2"
9 | material = "1.10.0"
10 | kotlin = "2.0.0"
11 | android-plugin = "8.5.1"
12 |
13 | [libraries]
14 | appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
15 | core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
16 | compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose" }
17 | compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "compose-material3" }
18 | compose-material = { group = "androidx.compose.material", name = "material", version.ref = "compose-material" }
19 | lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "runtime-ktx" }
20 | activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" }
21 | material = { group = "com.google.android.material", name = "material", version.ref = "material" }
22 |
23 | [plugins]
24 | org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
25 | android-application = { id = "com.android.application", version.ref = "android-plugin" }
26 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
27 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.defaults.buildfeatures.buildconfig=false
25 | android.nonFinalResIds=false
26 | org.gradle.configuration-cache=true
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | pluginManagement {
4 | repositories {
5 | maven { url = uri("https://maven.aliyun.com/repository/central") }
6 | maven { url = uri("https://maven.aliyun.com/repository/jcenter") }
7 | maven { url = uri("https://maven.aliyun.com/repository/public") }
8 | maven { url = uri("https://maven.aliyun.com/repository/google") }
9 | maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
10 | maven { url = uri("https://maven.aliyun.com/repository/spring") }
11 | maven { url = uri("https://maven.aliyun.com/repository/spring-plugin") }
12 | maven { url = uri("https://dl.google.com/dl/android/maven2/") }
13 | maven { url = uri("https://www.jitpack.io") }
14 | mavenCentral()
15 | google()
16 | }
17 | }
18 | dependencyResolutionManagement {
19 | repositories {
20 | maven { url = uri("https://maven.aliyun.com/repository/central") }
21 | maven { url = uri("https://maven.aliyun.com/repository/jcenter") }
22 | maven { url = uri("https://maven.aliyun.com/repository/public") }
23 | maven { url = uri("https://maven.aliyun.com/repository/google") }
24 | maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") }
25 | maven { url = uri("https://maven.aliyun.com/repository/spring") }
26 | maven { url = uri("https://maven.aliyun.com/repository/spring-plugin") }
27 | maven { url = uri("https://dl.google.com/dl/android/maven2/") }
28 | maven { url = uri("https://jitpack.io") }
29 | mavenCentral()
30 | google()
31 | }
32 | }
33 | rootProject.name = "Cumulus"
34 | include(":app")
35 |
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/objects/CpuStatsProvider.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats.objects
2 |
3 | import java.io.File
4 |
5 | object CpuStatsProvider {
6 | private val cpuGovernorFile: File =
7 | File("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
8 | private val cpuFreqFiles: MutableList = mutableListOf()
9 |
10 | init {
11 | for (cpuCore in 0..9) {
12 | val cpuFreqFile = File("/sys/devices/system/cpu/cpu${cpuCore}/cpufreq/scaling_cur_freq")
13 | if (cpuFreqFile.exists()) {
14 | cpuFreqFiles.add(cpuFreqFile)
15 | }
16 | }
17 | }
18 |
19 | fun getCpuGovernor(): String {
20 | if (cpuGovernorFile.exists()) {
21 | var cpuGovernor = ""
22 | try {
23 | cpuGovernor = cpuGovernorFile.readText(Charsets.UTF_8)
24 | } catch (e: Exception) {
25 | e.printStackTrace()
26 | }
27 | return cpuGovernor
28 | .replace("\n", "")
29 | .replace(" ", "")
30 | }
31 | return "unknown"
32 | }
33 |
34 | fun getCpuFreqs(): IntArray {
35 | val cpuFreqs = IntArray(cpuFreqFiles.size) { 0 }
36 | for (cpuCore in 0 until cpuFreqFiles.size) {
37 | var cpuFreq = 0
38 | try {
39 | cpuFreq = cpuFreqFiles[cpuCore]
40 | .readText(Charsets.UTF_8)
41 | .replace("\n", "")
42 | .replace(" ", "")
43 | .toInt()
44 | } catch (e: Exception) {
45 | e.printStackTrace()
46 | }
47 | cpuFreqs[cpuCore] = cpuFreq
48 | }
49 | return cpuFreqs
50 | }
51 | }
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.org.jetbrains.kotlin.android)
4 | alias(libs.plugins.compose.compiler)
5 | }
6 |
7 | // noinspection GradleDependency
8 | android {
9 | compileSdk = 34
10 |
11 | // noinspection OldTargetApi
12 | defaultConfig {
13 | applicationId = "cumulus.battery.stats"
14 | minSdk = 23
15 | targetSdk = 34
16 | versionCode = 100070
17 | versionName = "1.0.7"
18 |
19 | vectorDrawables {
20 | useSupportLibrary = true
21 | }
22 | }
23 |
24 | buildTypes {
25 | release {
26 | isMinifyEnabled = true
27 | proguardFiles(
28 | getDefaultProguardFile("proguard-android-optimize.txt"),
29 | "proguard-rules.pro"
30 | )
31 | }
32 | }
33 |
34 | compileOptions {
35 | sourceCompatibility = JavaVersion.VERSION_11
36 | targetCompatibility = JavaVersion.VERSION_11
37 | }
38 |
39 | kotlinOptions {
40 | jvmTarget = "11"
41 | }
42 |
43 | packaging {
44 | resources {
45 | excludes.add("META-INF/**")
46 | excludes.add("okhttp3/**")
47 | excludes.add("schema/**")
48 | excludes.add("assets/dexopt/**")
49 | excludes.add("DebugProbesKt.bin")
50 | excludes.add("kotlin-tooling-metadata.json")
51 | excludes.add("**/*.kotlin_builtins")
52 | excludes.add("**/*.kotlin_module")
53 | excludes.add("**/*.properties")
54 | excludes.add("**/*.txt")
55 | }
56 | }
57 |
58 | namespace = "cumulus.battery.stats"
59 | buildToolsVersion = "34.0.0"
60 | }
61 |
62 | dependencies {
63 | implementation(libs.appcompat)
64 | implementation(libs.core.ktx)
65 | implementation(libs.compose.ui)
66 | implementation(libs.compose.material3)
67 | implementation(libs.compose.material)
68 | implementation(libs.lifecycle.runtime.ktx)
69 | implementation(libs.activity.compose)
70 | implementation(libs.material)
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/objects/BatteryStatsRecorder.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats.objects
2 |
3 | import android.content.Context
4 | import cumulus.battery.stats.utils.BatteryDataSQLHelper
5 | import cumulus.battery.stats.utils.BatteryStatsItem
6 | import cumulus.battery.stats.utils.GetTimeStamp
7 | import kotlinx.coroutines.CoroutineScope
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.launch
10 |
11 | object BatteryStatsRecorder {
12 | private var sqlHelper: BatteryDataSQLHelper? = null
13 |
14 | fun init(context: Context) {
15 | CoroutineScope(Dispatchers.IO).launch {
16 | if (sqlHelper == null) {
17 | sqlHelper = BatteryDataSQLHelper(context)
18 | }
19 | }
20 | }
21 |
22 | fun addItem(
23 | packageName: String,
24 | batteryStatus: Int,
25 | batteryPercentage: Int,
26 | batteryPower: Int,
27 | batteryTemperature: Int
28 | ) {
29 | CoroutineScope(Dispatchers.IO).launch {
30 | val item = BatteryStatsItem()
31 | item.timestamp = GetTimeStamp()
32 | item.packageName = packageName
33 | item.batteryStatus = batteryStatus
34 | item.batteryPercentage = batteryPercentage
35 | item.batteryPower = batteryPower
36 | item.batteryTemperature = batteryTemperature
37 | sqlHelper?.insert(item)
38 | }
39 | }
40 |
41 | fun getRecords(): MutableList {
42 | if (sqlHelper != null) {
43 | val records = sqlHelper!!.readAll()
44 | if (records != null) {
45 | return records
46 | }
47 | }
48 | return mutableListOf()
49 | }
50 |
51 | fun optimize() {
52 | CoroutineScope(Dispatchers.IO).launch {
53 | if (sqlHelper != null) {
54 | sqlHelper!!.optimize()
55 | }
56 | }
57 | }
58 |
59 | fun deleteHistoryData() {
60 | CoroutineScope(Dispatchers.IO).launch {
61 | if (sqlHelper != null) {
62 | sqlHelper!!.deleteAll()
63 | }
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.lightColorScheme
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.SideEffect
11 | import androidx.compose.ui.graphics.Color
12 | import androidx.compose.ui.graphics.toArgb
13 | import androidx.compose.ui.platform.LocalView
14 | import androidx.core.view.WindowCompat
15 |
16 | private val DarkColorScheme = darkColorScheme(
17 | primary = Color(0xFFFFFFFF),
18 | secondary = Color(0xFF888888),
19 | tertiary = Color(0xFF404040),
20 | background = Color(0xFF000000),
21 | surface = Color(0xFF202020),
22 | outline = Color(0xFF888888)
23 | )
24 |
25 | private val LightColorScheme = lightColorScheme(
26 | primary = Color(0xFF000000),
27 | secondary = Color(0xFF888888),
28 | tertiary = Color(0xFFE0E0E0),
29 | background = Color(0xFFF8F8F8),
30 | surface = Color(0xFFFFFFFF),
31 | outline = Color(0xFF888888)
32 | )
33 |
34 | @Composable
35 | fun CumulusTheme(
36 | darkTheme: Boolean = isSystemInDarkTheme(),
37 | content: @Composable () -> Unit
38 | ) {
39 | val colorScheme = when {
40 | darkTheme -> DarkColorScheme
41 | else -> LightColorScheme
42 | }
43 | val view = LocalView.current
44 | if (!view.isInEditMode) {
45 | SideEffect {
46 | val window = (view.context as Activity).window
47 | window.statusBarColor = colorScheme.background.toArgb()
48 | window.navigationBarColor = colorScheme.background.toArgb()
49 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
50 | window.navigationBarDividerColor = colorScheme.background.toArgb()
51 | }
52 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
53 | }
54 | }
55 |
56 | MaterialTheme(
57 | colorScheme = colorScheme,
58 | typography = Typography,
59 | content = content
60 | )
61 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
33 |
34 |
39 |
40 |
45 |
46 |
51 |
52 |
57 |
58 |
63 |
64 |
65 |
66 |
67 |
70 |
71 |
72 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/BackgroundService.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats
2 |
3 | import android.accessibilityservice.AccessibilityService
4 | import android.graphics.Rect
5 | import android.os.PowerManager
6 | import android.view.accessibility.AccessibilityEvent
7 | import android.view.accessibility.AccessibilityWindowInfo
8 | import cumulus.battery.stats.objects.BatteryStatsProvider
9 | import cumulus.battery.stats.objects.BatteryStatsRecorder
10 | import java.util.Timer
11 | import java.util.TimerTask
12 |
13 | class BackgroundService : AccessibilityService() {
14 | companion object {
15 | private var serviceCreated = false
16 |
17 | fun isServiceCreated(): Boolean {
18 | return serviceCreated
19 | }
20 | }
21 |
22 | private var timer: Timer? = null
23 | private var deviceScreenSize: Long = 0
24 |
25 | override fun onCreate() {
26 | super.onCreate()
27 | serviceCreated = true
28 | deviceScreenSize = getDeviceScreenSize()
29 | }
30 |
31 | override fun onServiceConnected() {
32 | super.onServiceConnected()
33 | BatteryStatsProvider.init(applicationContext)
34 | BatteryStatsRecorder.init(applicationContext)
35 | startTimer()
36 | }
37 |
38 | override fun onInterrupt() {
39 | BatteryStatsRecorder.optimize()
40 | }
41 |
42 | override fun onDestroy() {
43 | serviceCreated = false
44 | stopTimer()
45 | BatteryStatsRecorder.optimize()
46 | super.onDestroy()
47 | }
48 |
49 | override fun onAccessibilityEvent(event: AccessibilityEvent?) {}
50 |
51 | private fun addRecordItem() {
52 | var foregroundPkgName = "standby"
53 | if (isScreenOn()) {
54 | foregroundPkgName = getForegroundPkgName()
55 | }
56 |
57 | val context = applicationContext
58 | val batteryPower = BatteryStatsProvider.getBatteryVoltage(context) *
59 | BatteryStatsProvider.getBatteryCurrent(context) / 1000
60 | BatteryStatsRecorder.addItem(
61 | foregroundPkgName,
62 | BatteryStatsProvider.getBatteryStatus(context),
63 | BatteryStatsProvider.getBatteryCapacity(context),
64 | batteryPower,
65 | BatteryStatsProvider.getBatteryTemperature(context)
66 | )
67 | }
68 |
69 | private fun startTimer() {
70 | if (timer == null) {
71 | timer = Timer()
72 | timer!!.schedule(object : TimerTask() {
73 | override fun run() {
74 | addRecordItem()
75 | }
76 | }, 0, 2000)
77 | }
78 | }
79 |
80 | private fun stopTimer() {
81 | if (timer != null) {
82 | timer!!.cancel()
83 | timer = null
84 | }
85 | }
86 |
87 | private fun isScreenOn(): Boolean {
88 | val powerManager = getSystemService(POWER_SERVICE) as PowerManager
89 | return powerManager.isInteractive
90 | }
91 |
92 | private fun getForegroundPkgName(): String {
93 | var topWindow: AccessibilityWindowInfo? = null
94 | windows.forEach { window ->
95 | val windowRect = Rect()
96 | window.getBoundsInScreen(windowRect)
97 | val windowSize = windowRect.width().toLong() * windowRect.height()
98 | if (windowSize > (deviceScreenSize / 2)) {
99 | topWindow = window
100 | }
101 | }
102 | if (topWindow != null) {
103 | val pkgName = topWindow?.root?.packageName?.toString()
104 | if (pkgName != null) {
105 | return pkgName
106 | }
107 | }
108 | return "other"
109 | }
110 |
111 | private fun getDeviceScreenSize(): Long {
112 | val metrics = resources.displayMetrics
113 | return metrics.widthPixels.toLong() * metrics.heightPixels
114 | }
115 | }
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/utils/BatteryDataSQLHelper.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats.utils
2 |
3 | import android.content.ContentValues
4 | import android.content.Context
5 | import android.database.sqlite.SQLiteDatabase
6 | import android.database.sqlite.SQLiteOpenHelper
7 |
8 | data class BatteryStatsItem(
9 | var timestamp: Long = 0,
10 | var packageName: String = "",
11 | var batteryStatus: Int = 0,
12 | var batteryPercentage: Int = 0,
13 | var batteryPower: Int = 0,
14 | var batteryTemperature: Int = 0
15 | )
16 |
17 | class BatteryDataSQLHelper(context: Context) :
18 | SQLiteOpenHelper(context, "battery_data.db", null, 1) {
19 |
20 | private val tableName = "BatteryData"
21 |
22 | override fun onCreate(dataBase: SQLiteDatabase?) {
23 | if (dataBase != null) {
24 | createTable(dataBase)
25 | }
26 | }
27 |
28 | override fun onUpgrade(dataBase: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
29 | if (dataBase != null) {
30 | createTable(dataBase)
31 | }
32 | }
33 |
34 | fun createTable(dataBase: SQLiteDatabase) {
35 | val createTableEntries = """
36 | CREATE TABLE IF NOT EXISTS ${tableName} (
37 | timestamp INTEGER PRIMARY KEY,
38 | packageName TEXT NOT NULL,
39 | batteryStatus INTEGER NOT NULL,
40 | batteryPercentage INTEGER NOT NULL,
41 | batteryPower INTEGER NOT NULL,
42 | batteryTemperature INTEGER NOT NULL
43 | )
44 | """.trimIndent()
45 |
46 | try {
47 | dataBase.execSQL(createTableEntries)
48 | } catch (e: Exception) {
49 | e.printStackTrace()
50 | }
51 | }
52 |
53 | fun optimize() {
54 | try {
55 | val dataBase = writableDatabase
56 | val outOfDateTimeStamp = (GetTimeStamp() - (7L * 24L * 3600L)).toString()
57 | dataBase.execSQL("DELETE FROM ${tableName} WHERE timestamp < ${outOfDateTimeStamp}")
58 | dataBase.execSQL("PRAGMA synchronous = NORMAL")
59 | dataBase.execSQL("PRAGMA wal_checkpoint(FULL)")
60 | dataBase.execSQL("PRAGMA optimize")
61 | } catch (e: Exception) {
62 | e.printStackTrace()
63 | }
64 | }
65 |
66 | fun insert(item: BatteryStatsItem) {
67 | val dataBase = writableDatabase
68 | createTable(dataBase)
69 |
70 | try {
71 | val contentValues = ContentValues()
72 | contentValues.apply {
73 | put("timestamp", item.timestamp)
74 | put("packageName", item.packageName)
75 | put("batteryStatus", item.batteryStatus)
76 | put("batteryPercentage", item.batteryPercentage)
77 | put("batteryPower", item.batteryPower)
78 | put("batteryTemperature", item.batteryTemperature)
79 | }
80 | dataBase.insertWithOnConflict(
81 | tableName,
82 | null,
83 | contentValues,
84 | SQLiteDatabase.CONFLICT_REPLACE
85 | )
86 | } catch (e: Exception) {
87 | e.printStackTrace()
88 | }
89 | }
90 |
91 | fun readAll(): MutableList? {
92 | try {
93 | val records: MutableList = mutableListOf()
94 | readableDatabase.query(
95 | tableName,
96 | null,
97 | null,
98 | null,
99 | null,
100 | null,
101 | "timestamp ASC"
102 | ).use { cursor ->
103 | while (cursor.moveToNext()) {
104 | val item = BatteryStatsItem()
105 | item.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow("timestamp"))
106 | item.packageName = cursor.getString(cursor.getColumnIndexOrThrow("packageName"))
107 | item.batteryStatus =
108 | cursor.getInt(cursor.getColumnIndexOrThrow("batteryStatus"))
109 | item.batteryPercentage =
110 | cursor.getInt(cursor.getColumnIndexOrThrow("batteryPercentage"))
111 | item.batteryPower =
112 | cursor.getInt(cursor.getColumnIndexOrThrow("batteryPower"))
113 | item.batteryTemperature =
114 | cursor.getInt(cursor.getColumnIndexOrThrow("batteryTemperature"))
115 | records.add(item)
116 | }
117 | }
118 | return records
119 | } catch (e: Exception) {
120 | e.printStackTrace()
121 | }
122 | return null
123 | }
124 |
125 | fun deleteAll() {
126 | try {
127 | writableDatabase.execSQL("DROP TABLE IF EXISTS ${tableName}")
128 | } catch (e: Exception) {
129 | e.printStackTrace()
130 | }
131 | }
132 | }
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/FloatMonitorService.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Service
5 | import android.content.Intent
6 | import android.graphics.PixelFormat
7 | import android.os.Build
8 | import android.os.Handler
9 | import android.os.IBinder
10 | import android.os.Looper
11 | import android.view.Gravity
12 | import android.view.WindowManager
13 | import android.widget.LinearLayout
14 | import android.widget.TextView
15 | import androidx.compose.ui.graphics.Color
16 | import androidx.compose.ui.graphics.toArgb
17 | import androidx.core.content.res.ResourcesCompat
18 | import cumulus.battery.stats.objects.BatteryStatsProvider
19 | import cumulus.battery.stats.objects.CpuStatsProvider
20 | import java.util.Timer
21 | import java.util.TimerTask
22 |
23 | class FloatMonitorService : Service() {
24 | companion object {
25 | private var serviceCreated = false
26 | fun isServiceCreated(): Boolean {
27 | return serviceCreated
28 | }
29 | }
30 |
31 | private var floatWindow: TextView? = null
32 | private var timer: Timer? = null
33 |
34 | override fun onBind(intent: Intent?): IBinder? {
35 | return null
36 | }
37 |
38 | override fun onCreate() {
39 | super.onCreate()
40 | serviceCreated = true
41 | createFloatWindow()
42 | startTimer()
43 | }
44 |
45 | override fun onDestroy() {
46 | stopTimer()
47 | destroyFloatWindow()
48 | serviceCreated = false
49 | super.onDestroy()
50 | }
51 |
52 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
53 | return super.onStartCommand(intent, START_FLAG_REDELIVERY, startId)
54 | }
55 |
56 | @SuppressLint("SetTextI18n")
57 | @Suppress("deprecation")
58 | private fun createFloatWindow() {
59 | if (floatWindow != null) {
60 | return
61 | }
62 | floatWindow = TextView(this)
63 | floatWindow!!.setBackgroundColor(Color(0x88000000).toArgb())
64 | val penddingVal = (applicationContext.resources.displayMetrics.density * 5 + 0.5f).toInt()
65 | floatWindow!!.setPadding(penddingVal, penddingVal, penddingVal, penddingVal)
66 | floatWindow!!.text = ""
67 | floatWindow!!.setTextColor(Color(0xFFFFFFFF).toArgb())
68 | floatWindow!!.textSize = 9f
69 | val typeface = ResourcesCompat.getFont(applicationContext, R.font.jetbrainsmono)
70 | floatWindow!!.typeface = typeface
71 |
72 | val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
73 | val layoutParams = WindowManager.LayoutParams()
74 | layoutParams.type = when {
75 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
76 | else -> WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
77 | }
78 | layoutParams.flags =
79 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
80 | layoutParams.format = PixelFormat.RGBA_8888
81 | layoutParams.width = LinearLayout.LayoutParams.WRAP_CONTENT
82 | layoutParams.height = LinearLayout.LayoutParams.WRAP_CONTENT
83 | layoutParams.gravity = Gravity.TOP or Gravity.END
84 | layoutParams.alpha = 0.6f
85 | windowManager.addView(floatWindow, layoutParams)
86 | }
87 |
88 | private fun destroyFloatWindow() {
89 | val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
90 | windowManager.removeViewImmediate(floatWindow)
91 | }
92 |
93 | private fun updateMonitorText() {
94 | val batteryCapacity = BatteryStatsProvider.getBatteryCapacity(applicationContext)
95 | val batteryTemperature = BatteryStatsProvider.getBatteryTemperature(applicationContext)
96 | val batteryCurrent = BatteryStatsProvider.getBatteryCurrent(applicationContext)
97 | val batteryPower =
98 | BatteryStatsProvider.getBatteryVoltage(applicationContext) * batteryCurrent / 1000
99 | val cpuFreqs = CpuStatsProvider.getCpuFreqs()
100 | val cpuGovernor = CpuStatsProvider.getCpuGovernor()
101 | var monitorText = ""
102 | monitorText += "#Battery ${batteryCapacity} % ${batteryTemperature} °C \n"
103 | monitorText += "#Power ${batteryPower} mW \n"
104 | monitorText += "#Current ${batteryCurrent} mA \n"
105 | for (cpuCore in 0 until cpuFreqs.size) {
106 | val cpuFreqMHz = cpuFreqs[cpuCore] / 1000
107 | monitorText += "#CPU${cpuCore} ${cpuFreqMHz} MHz \n"
108 | }
109 | monitorText += "#Governor ${cpuGovernor}"
110 | val handler = Handler(Looper.getMainLooper())
111 | handler.post {
112 | if (floatWindow == null) {
113 | return@post
114 | }
115 | floatWindow!!.text = monitorText
116 | }
117 | }
118 |
119 | private fun startTimer() {
120 | if (timer != null) {
121 | return
122 | }
123 | timer = Timer()
124 | timer!!.schedule(object : TimerTask() {
125 | override fun run() {
126 | updateMonitorText()
127 | }
128 | }, 0, 1000)
129 | }
130 |
131 | private fun stopTimer() {
132 | if (timer == null) {
133 | return
134 | }
135 | timer!!.cancel()
136 | timer = null
137 | }
138 | }
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/widgets/CumulusWidgets.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats.widgets
2 |
3 | import android.graphics.drawable.Drawable
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.PaddingValues
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.width
12 | import androidx.compose.foundation.shape.RoundedCornerShape
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Switch
15 | import androidx.compose.material3.SwitchDefaults
16 | import androidx.compose.material3.Text
17 | import androidx.compose.material3.TextButton
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.draw.scale
22 | import androidx.compose.ui.graphics.asImageBitmap
23 | import androidx.compose.ui.res.painterResource
24 | import androidx.compose.ui.text.font.FontWeight
25 | import androidx.compose.ui.unit.dp
26 | import androidx.compose.ui.unit.sp
27 | import androidx.core.graphics.drawable.toBitmap
28 | import cumulus.battery.stats.R
29 |
30 | @Composable
31 | fun GoToButton(
32 | modifier: Modifier = Modifier,
33 | icon: Drawable? = null,
34 | text: String? = null,
35 | goto: (() -> Unit)? = null
36 | ) {
37 | TextButton(
38 | onClick = {
39 | if (goto != null) {
40 | goto()
41 | }
42 | },
43 | shape = RoundedCornerShape(10.dp),
44 | contentPadding = PaddingValues(0.dp),
45 | modifier = modifier
46 | ) {
47 | Row(
48 | modifier = Modifier
49 | .padding(start = 20.dp, end = 20.dp)
50 | .fillMaxSize(),
51 | horizontalArrangement = Arrangement.Start,
52 | verticalAlignment = Alignment.CenterVertically
53 | ) {
54 | if (icon != null) {
55 | Image(
56 | bitmap = icon.toBitmap().asImageBitmap(),
57 | contentDescription = null,
58 | modifier = Modifier
59 | .padding(end = 20.dp)
60 | .height(28.dp)
61 | .width(28.dp)
62 | )
63 | }
64 | if (text != null) {
65 | Text(
66 | text = text,
67 | fontSize = 16.sp,
68 | fontWeight = FontWeight.Bold,
69 | color = MaterialTheme.colorScheme.primary,
70 | maxLines = 1
71 | )
72 | }
73 | Row(
74 | modifier = Modifier.fillMaxSize(),
75 | horizontalArrangement = Arrangement.End,
76 | verticalAlignment = Alignment.CenterVertically
77 | ) {
78 | Image(
79 | painter = painterResource(id = R.drawable.arrow_forward),
80 | contentDescription = null,
81 | modifier = Modifier
82 | .height(16.dp)
83 | .width(16.dp)
84 | )
85 | }
86 | }
87 | }
88 | }
89 |
90 | @Composable
91 | fun Switch(
92 | modifier: Modifier = Modifier,
93 | icon: Drawable? = null,
94 | text: String? = null,
95 | state: Boolean = false,
96 | onStateChanged: ((Boolean) -> Unit)? = null
97 | ) {
98 | TextButton(
99 | onClick = {
100 | if (onStateChanged != null) {
101 | onStateChanged(!state)
102 | }
103 | },
104 | shape = RoundedCornerShape(10.dp),
105 | contentPadding = PaddingValues(0.dp),
106 | modifier = modifier
107 | ) {
108 | Row(
109 | modifier = Modifier
110 | .padding(start = 20.dp, end = 20.dp)
111 | .fillMaxSize(),
112 | horizontalArrangement = Arrangement.Start,
113 | verticalAlignment = Alignment.CenterVertically
114 | ) {
115 | if (icon != null) {
116 | Image(
117 | bitmap = icon.toBitmap().asImageBitmap(),
118 | contentDescription = null,
119 | modifier = Modifier
120 | .padding(end = 20.dp)
121 | .height(28.dp)
122 | .width(28.dp)
123 | )
124 | }
125 | if (text != null) {
126 | Text(
127 | text = text,
128 | fontSize = 16.sp,
129 | fontWeight = FontWeight.Medium,
130 | color = MaterialTheme.colorScheme.primary,
131 | maxLines = 1
132 | )
133 | }
134 | Row(
135 | modifier = Modifier.fillMaxSize(),
136 | horizontalArrangement = Arrangement.End,
137 | verticalAlignment = Alignment.CenterVertically
138 | ) {
139 | val switchColors = SwitchDefaults.colors(
140 | checkedThumbColor = MaterialTheme.colorScheme.primary,
141 | checkedTrackColor = MaterialTheme.colorScheme.tertiary,
142 | checkedBorderColor = MaterialTheme.colorScheme.outline,
143 | uncheckedThumbColor = MaterialTheme.colorScheme.secondary,
144 | uncheckedTrackColor = MaterialTheme.colorScheme.surface,
145 | uncheckedBorderColor = MaterialTheme.colorScheme.outline
146 | )
147 | Switch(
148 | modifier = Modifier.scale(0.8f),
149 | colors = switchColors,
150 | checked = state,
151 | onCheckedChange = {
152 | if (onStateChanged != null) {
153 | onStateChanged(it)
154 | }
155 | }
156 | )
157 | }
158 | }
159 | }
160 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/AdditionalFunctionActivity.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats
2 |
3 | import android.content.Intent
4 | import android.content.res.Resources
5 | import android.net.Uri
6 | import android.os.Bundle
7 | import android.provider.Settings
8 | import android.widget.Toast
9 | import androidx.activity.ComponentActivity
10 | import androidx.activity.compose.setContent
11 | import androidx.appcompat.content.res.AppCompatResources
12 | import androidx.compose.foundation.Image
13 | import androidx.compose.foundation.layout.Arrangement
14 | import androidx.compose.foundation.layout.Column
15 | import androidx.compose.foundation.layout.Row
16 | import androidx.compose.foundation.layout.fillMaxSize
17 | import androidx.compose.foundation.layout.fillMaxWidth
18 | import androidx.compose.foundation.layout.height
19 | import androidx.compose.foundation.layout.padding
20 | import androidx.compose.foundation.layout.width
21 | import androidx.compose.foundation.rememberScrollState
22 | import androidx.compose.foundation.shape.RoundedCornerShape
23 | import androidx.compose.foundation.verticalScroll
24 | import androidx.compose.material3.MaterialTheme
25 | import androidx.compose.material3.Scaffold
26 | import androidx.compose.material3.Surface
27 | import androidx.compose.material3.Text
28 | import androidx.compose.material3.TextButton
29 | import androidx.compose.runtime.Composable
30 | import androidx.compose.ui.Alignment
31 | import androidx.compose.ui.Modifier
32 | import androidx.compose.ui.res.painterResource
33 | import androidx.compose.ui.text.font.FontWeight
34 | import androidx.compose.ui.unit.dp
35 | import androidx.compose.ui.unit.sp
36 | import cumulus.battery.stats.ui.theme.CumulusTheme
37 | import cumulus.battery.stats.widgets.GoToButton
38 |
39 | class AdditionalFunctionActivity : ComponentActivity() {
40 | override fun onCreate(savedInstanceState: Bundle?) {
41 | super.onCreate(savedInstanceState)
42 | setContent {
43 | CumulusTheme {
44 | Surface(
45 | modifier = Modifier.fillMaxSize(),
46 | color = MaterialTheme.colorScheme.background
47 | ) {
48 | Scaffold(
49 | modifier = Modifier.fillMaxSize(),
50 | topBar = {
51 | Row(
52 | modifier = Modifier
53 | .padding(top = 10.dp, start = 10.dp, end = 10.dp)
54 | .height(50.dp)
55 | .fillMaxWidth(),
56 | horizontalArrangement = Arrangement.Start,
57 | verticalAlignment = Alignment.CenterVertically
58 | ) {
59 | TextButton(
60 | onClick = { finish() },
61 | modifier = Modifier
62 | .height(50.dp)
63 | .width(50.dp),
64 | shape = RoundedCornerShape(10.dp)
65 | ) {
66 | Image(
67 | painter = painterResource(id = R.drawable.arrow_back),
68 | contentDescription = null,
69 | modifier = Modifier
70 | .height(32.dp)
71 | .width(32.dp)
72 | )
73 | }
74 | Text(
75 | modifier = Modifier
76 | .padding(start = 10.dp),
77 | text = "附加功能",
78 | fontSize = 18.sp,
79 | fontWeight = FontWeight.Bold,
80 | color = MaterialTheme.colorScheme.primary
81 | )
82 | }
83 | }
84 | ) {
85 | Column(
86 | modifier = Modifier
87 | .padding(
88 | top = it.calculateTopPadding() + 10.dp,
89 | start = 20.dp,
90 | end = 20.dp
91 | )
92 | .fillMaxSize()
93 | .padding(top = 10.dp)
94 | .verticalScroll(rememberScrollState()),
95 | verticalArrangement = Arrangement.Top,
96 | horizontalAlignment = Alignment.CenterHorizontally
97 | ) {
98 | StartFloatMonitorButton()
99 | OpenBatteryUsageSettingButton()
100 | }
101 | }
102 | }
103 | }
104 | }
105 | }
106 |
107 | override fun getResources(): Resources {
108 | val resources = super.getResources()
109 | val configContext = createConfigurationContext(resources.configuration)
110 | return configContext.resources.apply {
111 | configuration.fontScale = 1.0f
112 | }
113 | }
114 |
115 | @Composable
116 | private fun StartFloatMonitorButton() {
117 | GoToButton(
118 | modifier = Modifier
119 | .fillMaxWidth()
120 | .height(50.dp),
121 | icon = AppCompatResources.getDrawable(applicationContext, R.drawable.monitor),
122 | text = "监视器悬浮窗"
123 | ) {
124 | if (FloatMonitorService.isServiceCreated()) {
125 | val intent = Intent(applicationContext, FloatMonitorService::class.java)
126 | stopService(intent)
127 | } else {
128 | if (!Settings.canDrawOverlays(this)) {
129 | Toast.makeText(applicationContext, "请授权悬浮窗权限", Toast.LENGTH_LONG)
130 | .show()
131 | val intent = Intent()
132 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
133 | intent.action = "android.settings.APPLICATION_DETAILS_SETTINGS"
134 | intent.data = Uri.fromParts("package", packageName, null)
135 | startActivity(intent)
136 | } else {
137 | val intent = Intent(applicationContext, FloatMonitorService::class.java)
138 | startService(intent)
139 | }
140 | }
141 | }
142 | }
143 |
144 | @Composable
145 | private fun OpenBatteryUsageSettingButton() {
146 | GoToButton(
147 | modifier = Modifier
148 | .padding(top = 5.dp)
149 | .fillMaxWidth()
150 | .height(50.dp),
151 | icon = AppCompatResources.getDrawable(applicationContext, R.drawable.app_settings),
152 | text = "电池使用状况"
153 | ) {
154 | val intent = Intent(Intent.ACTION_POWER_USAGE_SUMMARY)
155 | startActivity(intent)
156 | }
157 | }
158 | }
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/objects/BatteryStatsProvider.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats.objects
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.IntentFilter
7 | import android.content.SharedPreferences
8 | import android.os.BatteryManager
9 | import androidx.core.content.edit
10 |
11 | object BatteryStatsProvider {
12 | private var sharedPreferences: SharedPreferences? = null
13 |
14 | fun init(context: Context) {
15 | if (sharedPreferences == null) {
16 | sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
17 | }
18 | }
19 |
20 | @SuppressLint("PrivateApi")
21 | fun getBatteryDesignCapacity(context: Context): Int {
22 | try {
23 | val powerProfileClassName = "com.android.internal.os.PowerProfile"
24 | val mPowerProfile =
25 | Class.forName(powerProfileClassName).getConstructor(Context::class.java)
26 | .newInstance(context)
27 | val capacity = Class.forName(powerProfileClassName).getMethod("getBatteryCapacity")
28 | .invoke(mPowerProfile)
29 | return (capacity as Double).toInt()
30 | } catch (e: Exception) {
31 | e.printStackTrace()
32 | }
33 | return 0
34 | }
35 |
36 | fun getBatteryTemperature(context: Context): Int {
37 | try {
38 | val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
39 | intentFilter.priority = IntentFilter.SYSTEM_HIGH_PRIORITY
40 | val intent = context.registerReceiver(null, intentFilter)
41 | if (intent != null) {
42 | return (intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) / 10)
43 | }
44 | } catch (e: Exception) {
45 | e.printStackTrace()
46 | }
47 | return 0
48 | }
49 |
50 | fun getBatteryVoltage(context: Context): Int {
51 | try {
52 | val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
53 | intentFilter.priority = IntentFilter.SYSTEM_HIGH_PRIORITY
54 | val intent = context.registerReceiver(null, intentFilter)
55 | if (intent != null) {
56 | val batteryVolt = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0)
57 | if (batteryVolt < 1000) {
58 | return batteryVolt * 1000
59 | }
60 | return batteryVolt
61 | }
62 | } catch (e: Exception) {
63 | e.printStackTrace()
64 | }
65 | return 0
66 | }
67 |
68 | fun getBatteryStatus(context: Context): Int {
69 | try {
70 | val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
71 | intentFilter.priority = IntentFilter.SYSTEM_HIGH_PRIORITY
72 | val intent = context.registerReceiver(null, intentFilter)
73 | if (intent != null) {
74 | return intent.getIntExtra(
75 | BatteryManager.EXTRA_STATUS,
76 | BatteryManager.BATTERY_STATUS_UNKNOWN
77 | )
78 | }
79 | } catch (e: Exception) {
80 | e.printStackTrace()
81 | }
82 | return BatteryManager.BATTERY_STATUS_UNKNOWN
83 | }
84 |
85 | fun getBatteryCapacity(context: Context): Int {
86 | try {
87 | val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
88 | intentFilter.priority = IntentFilter.SYSTEM_HIGH_PRIORITY
89 | val intent = context.registerReceiver(null, intentFilter)
90 | if (intent != null) {
91 | return intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)
92 | }
93 | } catch (e: Exception) {
94 | e.printStackTrace()
95 | }
96 | return 0
97 | }
98 |
99 | fun getBatteryCurrent(context: Context): Int {
100 | try {
101 | val batteryManager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
102 | var batteryCurrent =
103 | batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW)
104 | if (isDualBattery()) {
105 | batteryCurrent *= 2
106 | }
107 | if (isCurrentUnitUA()) {
108 | batteryCurrent /= 1000
109 | }
110 | if (isCurrentReverse()) {
111 | batteryCurrent = -batteryCurrent
112 | }
113 | return batteryCurrent
114 | } catch (e: Exception) {
115 | e.printStackTrace()
116 | }
117 | return 0
118 | }
119 |
120 | fun getBatteryHealth(context: Context): Int {
121 | try {
122 | val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
123 | intentFilter.priority = IntentFilter.SYSTEM_HIGH_PRIORITY
124 | val intent = context.registerReceiver(null, intentFilter)
125 | val batteryHealth = intent?.getIntExtra(
126 | BatteryManager.EXTRA_HEALTH,
127 | BatteryManager.BATTERY_HEALTH_UNKNOWN
128 | )
129 | if (batteryHealth != null) {
130 | return batteryHealth
131 | }
132 | } catch (e: Exception) {
133 | e.printStackTrace()
134 | }
135 | return BatteryManager.BATTERY_HEALTH_UNKNOWN
136 | }
137 |
138 | fun getBatteryTechnology(context: Context): String {
139 | try {
140 | val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
141 | intentFilter.priority = IntentFilter.SYSTEM_HIGH_PRIORITY
142 | val intent = context.registerReceiver(null, intentFilter)
143 | val batteryTechnology = intent?.getStringExtra(BatteryManager.EXTRA_TECHNOLOGY)
144 | if (batteryTechnology != null) {
145 | return batteryTechnology
146 | }
147 | } catch (e: Exception) {
148 | e.printStackTrace()
149 | }
150 | return "unknown"
151 | }
152 |
153 | @SuppressLint("InlinedApi")
154 | fun getBatteryCycleCount(context: Context): Int {
155 | try {
156 | val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
157 | intentFilter.priority = IntentFilter.SYSTEM_HIGH_PRIORITY
158 | val intent = context.registerReceiver(null, intentFilter)
159 | if (intent != null) {
160 | return intent.getIntExtra(BatteryManager.EXTRA_CYCLE_COUNT, 0)
161 | }
162 | } catch (e: Exception) {
163 | e.printStackTrace()
164 | }
165 | return 0
166 | }
167 |
168 | fun setDualBattery(dualBattery: Boolean) {
169 | sharedPreferences?.edit {
170 | putBoolean("dualBattery", dualBattery)
171 | }
172 | }
173 |
174 | fun setCurrentUnitUA(currentUnitUA: Boolean) {
175 | sharedPreferences?.edit {
176 | putBoolean("currentUnitUA", currentUnitUA)
177 | }
178 | }
179 |
180 | fun setCurrentReverse(currentReverse: Boolean) {
181 | sharedPreferences?.edit {
182 | putBoolean("currentReverse", currentReverse)
183 | }
184 | }
185 |
186 | fun setCurrentAdjusted(currentAdjusted: Boolean) {
187 | sharedPreferences?.edit {
188 | putBoolean("currentAdjusted", currentAdjusted)
189 | }
190 | }
191 |
192 | fun isDualBattery(): Boolean {
193 | if (sharedPreferences != null) {
194 | return sharedPreferences!!.getBoolean("dualBattery", false)
195 | }
196 | return false
197 | }
198 |
199 | fun isCurrentUnitUA(): Boolean {
200 | if (sharedPreferences != null) {
201 | return sharedPreferences!!.getBoolean("currentUnitUA", false)
202 | }
203 | return false
204 | }
205 |
206 | fun isCurrentReverse(): Boolean {
207 | if (sharedPreferences != null) {
208 | return sharedPreferences!!.getBoolean("currentReverse", false)
209 | }
210 | return false
211 | }
212 |
213 | fun isCurrentAdjusted(): Boolean {
214 | if (sharedPreferences != null) {
215 | return sharedPreferences!!.getBoolean("currentAdjusted", false)
216 | }
217 | return false
218 | }
219 | }
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/SettingsActivity.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Intent
5 | import android.content.pm.PackageManager
6 | import android.content.res.Resources
7 | import android.os.Bundle
8 | import android.provider.Settings
9 | import android.widget.Toast
10 | import androidx.activity.ComponentActivity
11 | import androidx.activity.compose.setContent
12 | import androidx.appcompat.app.AlertDialog
13 | import androidx.compose.foundation.Image
14 | import androidx.compose.foundation.layout.Arrangement
15 | import androidx.compose.foundation.layout.Column
16 | import androidx.compose.foundation.layout.Row
17 | import androidx.compose.foundation.layout.fillMaxSize
18 | import androidx.compose.foundation.layout.fillMaxWidth
19 | import androidx.compose.foundation.layout.height
20 | import androidx.compose.foundation.layout.padding
21 | import androidx.compose.foundation.layout.width
22 | import androidx.compose.foundation.rememberScrollState
23 | import androidx.compose.foundation.shape.RoundedCornerShape
24 | import androidx.compose.foundation.verticalScroll
25 | import androidx.compose.material3.MaterialTheme
26 | import androidx.compose.material3.Scaffold
27 | import androidx.compose.material3.Surface
28 | import androidx.compose.material3.Text
29 | import androidx.compose.material3.TextButton
30 | import androidx.compose.runtime.Composable
31 | import androidx.compose.ui.Alignment
32 | import androidx.compose.ui.Modifier
33 | import androidx.compose.ui.res.painterResource
34 | import androidx.compose.ui.text.font.FontWeight
35 | import androidx.compose.ui.unit.dp
36 | import androidx.compose.ui.unit.sp
37 | import androidx.core.net.toUri
38 | import cumulus.battery.stats.objects.BatteryStatsRecorder
39 | import cumulus.battery.stats.ui.theme.CumulusTheme
40 | import cumulus.battery.stats.ui.theme.cumulusColor
41 | import cumulus.battery.stats.widgets.GoToButton
42 |
43 | class SettingsActivity : ComponentActivity() {
44 | override fun onCreate(savedInstanceState: Bundle?) {
45 | super.onCreate(savedInstanceState)
46 | setContent {
47 | CumulusTheme {
48 | Surface(
49 | modifier = Modifier.fillMaxSize(),
50 | color = MaterialTheme.colorScheme.background
51 | ) {
52 | Scaffold(
53 | modifier = Modifier.fillMaxSize(),
54 | topBar = {
55 | Row(
56 | modifier = Modifier
57 | .padding(top = 10.dp, start = 10.dp, end = 10.dp)
58 | .height(50.dp)
59 | .fillMaxWidth(),
60 | horizontalArrangement = Arrangement.Start,
61 | verticalAlignment = Alignment.CenterVertically
62 | ) {
63 | TextButton(
64 | onClick = { finish() },
65 | modifier = Modifier
66 | .height(50.dp)
67 | .width(50.dp),
68 | shape = RoundedCornerShape(10.dp)
69 | ) {
70 | Image(
71 | painter = painterResource(id = R.drawable.arrow_back),
72 | contentDescription = null,
73 | modifier = Modifier
74 | .height(32.dp)
75 | .width(32.dp)
76 | )
77 | }
78 | Text(
79 | modifier = Modifier
80 | .padding(start = 10.dp),
81 | text = "设置",
82 | fontSize = 18.sp,
83 | fontWeight = FontWeight.Bold,
84 | color = MaterialTheme.colorScheme.primary
85 | )
86 | }
87 | }
88 | ) {
89 | Column(
90 | modifier = Modifier
91 | .padding(
92 | top = it.calculateTopPadding() + 10.dp,
93 | start = 20.dp,
94 | end = 20.dp
95 | )
96 | .fillMaxSize()
97 | .padding(top = 10.dp)
98 | .verticalScroll(rememberScrollState()),
99 | verticalArrangement = Arrangement.Top,
100 | horizontalAlignment = Alignment.CenterHorizontally
101 | ) {
102 | Text(
103 | modifier = Modifier
104 | .padding(start = 20.dp, top = 10.dp)
105 | .height(20.dp)
106 | .fillMaxWidth(),
107 | text = "应用设置",
108 | fontSize = 14.sp,
109 | fontWeight = FontWeight.Medium,
110 | color = cumulusColor().purple,
111 | maxLines = 1
112 | )
113 | RequireIgnoreBatteryOptimizationButton()
114 | DeleteHistoryDataButton()
115 | AdjustCurrentButton()
116 | Text(
117 | modifier = Modifier
118 | .padding(start = 20.dp, top = 20.dp)
119 | .height(20.dp)
120 | .fillMaxWidth(),
121 | text = "关于",
122 | fontSize = 14.sp,
123 | fontWeight = FontWeight.Medium,
124 | color = cumulusColor().purple,
125 | maxLines = 1
126 | )
127 | AboutInformation()
128 | }
129 | }
130 | }
131 | }
132 | }
133 | }
134 |
135 | override fun getResources(): Resources {
136 | val resources = super.getResources()
137 | val configContext = createConfigurationContext(resources.configuration)
138 | return configContext.resources.apply {
139 | configuration.fontScale = 1.0f
140 | }
141 | }
142 |
143 | @SuppressLint("BatteryLife")
144 | @Composable
145 | private fun RequireIgnoreBatteryOptimizationButton() {
146 | GoToButton(
147 | modifier = Modifier
148 | .padding(top = 10.dp)
149 | .fillMaxWidth()
150 | .height(40.dp),
151 | text = "请求忽略电池优化"
152 | ) {
153 | val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
154 | intent.data = ("package:" + applicationContext.packageName).toUri()
155 | startActivity(intent)
156 | }
157 | }
158 |
159 | @Composable
160 | private fun DeleteHistoryDataButton() {
161 | GoToButton(
162 | modifier = Modifier
163 | .padding(top = 5.dp)
164 | .fillMaxWidth()
165 | .height(40.dp),
166 | text = "清除历史数据"
167 | ) {
168 | val builder = AlertDialog.Builder(this)
169 | builder.setTitle("警告")
170 | builder.setMessage("清除数据操作不可逆")
171 | builder.setPositiveButton("继续") { _, _ ->
172 | BatteryStatsRecorder.deleteHistoryData()
173 | Toast.makeText(
174 | applicationContext,
175 | "历史数据已清除",
176 | Toast.LENGTH_LONG
177 | ).show()
178 | }
179 | builder.setNegativeButton("取消") { _, _ ->
180 | Toast.makeText(applicationContext, "已取消操作", Toast.LENGTH_LONG).show()
181 | }
182 | val dialog = builder.create()
183 | dialog.show()
184 | }
185 | }
186 |
187 | @Composable
188 | private fun AdjustCurrentButton() {
189 | GoToButton(
190 | modifier = Modifier
191 | .padding(top = 5.dp)
192 | .fillMaxWidth()
193 | .height(40.dp),
194 | text = "调整电流显示"
195 | ) {
196 | val intent =
197 | Intent(applicationContext, CurrentAdjustActivity::class.java)
198 | startActivity(intent)
199 | }
200 | }
201 |
202 | @Suppress("deprecation")
203 | @Composable
204 | private fun AboutInformation() {
205 | val versionName =
206 | packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA).versionName
207 | val versionCode =
208 | packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA).versionCode
209 |
210 | Column(
211 | modifier = Modifier
212 | .padding(start = 20.dp, end = 20.dp, top = 5.dp)
213 | .fillMaxWidth()
214 | .height(90.dp),
215 | verticalArrangement = Arrangement.Center,
216 | horizontalAlignment = Alignment.Start
217 | ) {
218 | Row(
219 | modifier = Modifier
220 | .fillMaxWidth()
221 | .height(40.dp),
222 | horizontalArrangement = Arrangement.Start,
223 | verticalAlignment = Alignment.CenterVertically
224 | ) {
225 | Text(
226 | text = "Cu",
227 | fontSize = 20.sp,
228 | fontWeight = FontWeight.Bold,
229 | color = cumulusColor().pink,
230 | maxLines = 1
231 | )
232 | Text(
233 | text = "mulus",
234 | fontSize = 20.sp,
235 | fontWeight = FontWeight.Bold,
236 | color = cumulusColor().blue,
237 | maxLines = 1
238 | )
239 | }
240 | Text(
241 | text = "版本: ${versionName} (${versionCode})",
242 | fontSize = 14.sp,
243 | fontWeight = FontWeight.Medium,
244 | color = MaterialTheme.colorScheme.secondary,
245 | maxLines = 1
246 | )
247 | Text(
248 | text = "Copyright (C) Chenzyadb 2025",
249 | fontSize = 14.sp,
250 | fontWeight = FontWeight.Medium,
251 | color = MaterialTheme.colorScheme.secondary,
252 | maxLines = 1
253 | )
254 | }
255 | }
256 | }
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/CurrentAdjustActivity.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats
2 |
3 | import android.content.res.Resources
4 | import android.os.BatteryManager
5 | import android.os.Bundle
6 | import android.widget.Toast
7 | import androidx.activity.ComponentActivity
8 | import androidx.activity.compose.setContent
9 | import androidx.appcompat.content.res.AppCompatResources
10 | import androidx.compose.foundation.Image
11 | import androidx.compose.foundation.background
12 | import androidx.compose.foundation.layout.Arrangement
13 | import androidx.compose.foundation.layout.Column
14 | import androidx.compose.foundation.layout.Row
15 | import androidx.compose.foundation.layout.fillMaxSize
16 | import androidx.compose.foundation.layout.fillMaxWidth
17 | import androidx.compose.foundation.layout.height
18 | import androidx.compose.foundation.layout.padding
19 | import androidx.compose.foundation.layout.width
20 | import androidx.compose.foundation.rememberScrollState
21 | import androidx.compose.foundation.shape.RoundedCornerShape
22 | import androidx.compose.foundation.verticalScroll
23 | import androidx.compose.material3.MaterialTheme
24 | import androidx.compose.material3.Scaffold
25 | import androidx.compose.material3.Surface
26 | import androidx.compose.material3.Text
27 | import androidx.compose.material3.TextButton
28 | import androidx.compose.runtime.Composable
29 | import androidx.compose.runtime.getValue
30 | import androidx.compose.runtime.mutableIntStateOf
31 | import androidx.compose.runtime.mutableStateOf
32 | import androidx.compose.runtime.setValue
33 | import androidx.compose.ui.Alignment
34 | import androidx.compose.ui.Modifier
35 | import androidx.compose.ui.res.painterResource
36 | import androidx.compose.ui.text.font.FontWeight
37 | import androidx.compose.ui.text.style.TextOverflow
38 | import androidx.compose.ui.unit.dp
39 | import androidx.compose.ui.unit.sp
40 | import cumulus.battery.stats.objects.BatteryStatsProvider
41 | import cumulus.battery.stats.ui.theme.CumulusTheme
42 | import cumulus.battery.stats.ui.theme.cumulusColor
43 | import cumulus.battery.stats.widgets.GoToButton
44 | import cumulus.battery.stats.widgets.Switch
45 | import java.util.Timer
46 | import java.util.TimerTask
47 |
48 | class CurrentAdjustActivity : ComponentActivity() {
49 | private var timer: Timer? = null
50 | private var batteryCurrent by mutableIntStateOf(0)
51 | private var batteryStatus by mutableIntStateOf(BatteryManager.BATTERY_STATUS_UNKNOWN)
52 | private var currentReverse by mutableStateOf(false)
53 | private var currentUnitUA by mutableStateOf(false)
54 | private var dualBattery by mutableStateOf(false)
55 |
56 | override fun onCreate(savedInstanceState: Bundle?) {
57 | super.onCreate(savedInstanceState)
58 | BatteryStatsProvider.setCurrentAdjusted(true)
59 | setContent {
60 | CumulusTheme {
61 | Surface(
62 | modifier = Modifier.fillMaxSize(),
63 | color = MaterialTheme.colorScheme.background
64 | ) {
65 | Scaffold(
66 | modifier = Modifier.fillMaxSize(),
67 | topBar = {
68 | Row(
69 | modifier = Modifier
70 | .padding(top = 10.dp, start = 10.dp, end = 10.dp)
71 | .height(50.dp)
72 | .fillMaxWidth(),
73 | horizontalArrangement = Arrangement.Start,
74 | verticalAlignment = Alignment.CenterVertically
75 | ) {
76 | TextButton(
77 | onClick = { finish() },
78 | modifier = Modifier
79 | .height(50.dp)
80 | .width(50.dp),
81 | shape = RoundedCornerShape(10.dp)
82 | ) {
83 | Image(
84 | painter = painterResource(id = R.drawable.arrow_back),
85 | contentDescription = null,
86 | modifier = Modifier
87 | .height(32.dp)
88 | .width(32.dp)
89 | )
90 | }
91 | Text(
92 | modifier = Modifier
93 | .padding(start = 10.dp),
94 | text = "电流调整",
95 | fontSize = 18.sp,
96 | fontWeight = FontWeight.Bold,
97 | color = MaterialTheme.colorScheme.primary
98 | )
99 | }
100 | }
101 | ) {
102 | Column(
103 | modifier = Modifier
104 | .padding(
105 | top = it.calculateTopPadding() + 10.dp,
106 | start = 20.dp,
107 | end = 20.dp
108 | )
109 | .fillMaxSize()
110 | .padding(top = 10.dp)
111 | .verticalScroll(rememberScrollState()),
112 | verticalArrangement = Arrangement.Top,
113 | horizontalAlignment = Alignment.CenterHorizontally
114 | ) {
115 | BatteryCurrentBar()
116 | CurrentReverseSwitch()
117 | CurrentUnitUASwitch()
118 | DualBatterySwitch()
119 | AutoAdjustButton()
120 | }
121 | }
122 | }
123 | }
124 | }
125 | }
126 |
127 | override fun getResources(): Resources {
128 | val resources = super.getResources()
129 | val configContext = createConfigurationContext(resources.configuration)
130 | return configContext.resources.apply {
131 | configuration.fontScale = 1.0f
132 | }
133 | }
134 |
135 | override fun onStart() {
136 | super.onStart()
137 | startTimer()
138 | }
139 |
140 | override fun onStop() {
141 | stopTimer()
142 | super.onStop()
143 | }
144 |
145 | @Composable
146 | private fun BatteryCurrentBar() {
147 | Column(
148 | modifier = Modifier
149 | .fillMaxWidth()
150 | .height(120.dp)
151 | .background(
152 | shape = RoundedCornerShape(10.dp),
153 | color = MaterialTheme.colorScheme.surface
154 | ),
155 | verticalArrangement = Arrangement.Center,
156 | horizontalAlignment = Alignment.CenterHorizontally
157 | ) {
158 | Text(
159 | text = "${batteryCurrent} mA",
160 | fontSize = 32.sp,
161 | fontWeight = FontWeight.Bold,
162 | color = cumulusColor().blue
163 | )
164 |
165 | var tipText = "请确保电流值为负且大小正确"
166 | if (batteryStatus == BatteryManager.BATTERY_STATUS_CHARGING) {
167 | tipText = "请确保电流值为正且大小正确"
168 | }
169 | Text(
170 | modifier = Modifier.padding(top = 5.dp),
171 | text = tipText,
172 | fontSize = 14.sp,
173 | fontWeight = FontWeight.Medium,
174 | color = MaterialTheme.colorScheme.secondary,
175 | maxLines = 1,
176 | overflow = TextOverflow.Ellipsis
177 | )
178 | }
179 | }
180 |
181 | @Composable
182 | fun CurrentReverseSwitch() {
183 | Switch(
184 | modifier = Modifier
185 | .padding(top = 20.dp)
186 | .height(50.dp)
187 | .fillMaxWidth()
188 | .background(
189 | shape = RoundedCornerShape(10.dp),
190 | color = MaterialTheme.colorScheme.surface
191 | ),
192 | icon = AppCompatResources.getDrawable(
193 | applicationContext,
194 | R.drawable.current
195 | ),
196 | text = "电流反转",
197 | state = currentReverse
198 | ) { state ->
199 | BatteryStatsProvider.setCurrentReverse(state)
200 | currentReverse = state
201 | }
202 | }
203 |
204 | @Composable
205 | fun CurrentUnitUASwitch() {
206 | Switch(
207 | modifier = Modifier
208 | .padding(top = 5.dp)
209 | .height(50.dp)
210 | .fillMaxWidth()
211 | .background(
212 | shape = RoundedCornerShape(10.dp),
213 | color = MaterialTheme.colorScheme.surface
214 | ),
215 | icon = AppCompatResources.getDrawable(applicationContext, R.drawable.swap),
216 | text = "uA-mA单位切换",
217 | state = currentUnitUA
218 | ) { state ->
219 | BatteryStatsProvider.setCurrentUnitUA(state)
220 | currentUnitUA = state
221 | }
222 | }
223 |
224 | @Composable
225 | fun DualBatterySwitch() {
226 | Switch(
227 | modifier = Modifier
228 | .padding(top = 5.dp)
229 | .height(50.dp)
230 | .fillMaxWidth()
231 | .background(
232 | shape = RoundedCornerShape(10.dp),
233 | color = MaterialTheme.colorScheme.surface
234 | ),
235 | icon = AppCompatResources.getDrawable(
236 | applicationContext,
237 | R.drawable.device
238 | ),
239 | text = "双电芯设备",
240 | state = dualBattery
241 | ) { state ->
242 | BatteryStatsProvider.setDualBattery(state)
243 | dualBattery = state
244 | }
245 | }
246 |
247 | @Composable
248 | fun AutoAdjustButton() {
249 | GoToButton(
250 | modifier = Modifier
251 | .padding(top = 20.dp)
252 | .height(50.dp)
253 | .fillMaxWidth()
254 | .background(
255 | shape = RoundedCornerShape(10.dp),
256 | color = MaterialTheme.colorScheme.surface
257 | ),
258 | icon = AppCompatResources.getDrawable(applicationContext, R.drawable.auto_done),
259 | text = "自动调整"
260 | ) {
261 | autoAdjustCurrent()
262 | }
263 | }
264 |
265 | private fun autoAdjustCurrent() {
266 | if ((batteryStatus != BatteryManager.BATTERY_STATUS_CHARGING && batteryCurrent > 0) ||
267 | (batteryStatus == BatteryManager.BATTERY_STATUS_CHARGING && batteryCurrent < 0)
268 | ) {
269 | BatteryStatsProvider.setCurrentReverse(!BatteryStatsProvider.isCurrentReverse())
270 | }
271 | if (batteryCurrent > 100000 || batteryCurrent < -100000) {
272 | BatteryStatsProvider.setCurrentUnitUA(true)
273 | } else if (batteryCurrent == 0) {
274 | BatteryStatsProvider.setCurrentUnitUA(false)
275 | }
276 | updateBatteryStats()
277 | Toast.makeText(applicationContext, "自动调整完毕", Toast.LENGTH_LONG).show()
278 | }
279 |
280 | private fun updateBatteryStats() {
281 | batteryCurrent = BatteryStatsProvider.getBatteryCurrent(applicationContext)
282 | batteryStatus = BatteryStatsProvider.getBatteryStatus(applicationContext)
283 | currentReverse = BatteryStatsProvider.isCurrentReverse()
284 | currentUnitUA = BatteryStatsProvider.isCurrentUnitUA()
285 | dualBattery = BatteryStatsProvider.isDualBattery()
286 | }
287 |
288 | private fun startTimer() {
289 | if (timer != null) {
290 | return
291 | }
292 | timer = Timer()
293 | timer!!.schedule(object : TimerTask() {
294 | override fun run() {
295 | updateBatteryStats()
296 | }
297 | }, 0, 1000)
298 | }
299 |
300 | private fun stopTimer() {
301 | if (timer == null) {
302 | return
303 | }
304 | timer!!.cancel()
305 | timer = null
306 | }
307 | }
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/widgets/CumulusCharts.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats.widgets
2 |
3 | import android.graphics.Paint
4 | import android.graphics.Path
5 | import androidx.compose.foundation.Canvas
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.geometry.Offset
10 | import androidx.compose.ui.geometry.Size
11 | import androidx.compose.ui.graphics.Color
12 | import androidx.compose.ui.graphics.PathEffect
13 | import androidx.compose.ui.graphics.nativeCanvas
14 | import androidx.compose.ui.graphics.toArgb
15 | import androidx.compose.ui.platform.LocalContext
16 |
17 | typealias DataPointList = List>
18 | typealias DataPointMutableList = MutableList>
19 |
20 | private fun SimplifyDataPoints(dataPoints: DataPointList): DataPointList {
21 | if (dataPoints.size > 1000) {
22 | val simplifiedDataPoints: DataPointMutableList = mutableListOf()
23 | val factor = dataPoints.size.toDouble() / 500.0
24 | for (i in 0 until 500) {
25 | simplifiedDataPoints.add(dataPoints[(factor * i).toInt()])
26 | }
27 | simplifiedDataPoints.add(dataPoints.last())
28 | return simplifiedDataPoints
29 | }
30 | return dataPoints
31 | }
32 |
33 | private fun GetDataPointsTickMax(dataPoints: DataPointList): UInt {
34 | var tickMax = 10U
35 | dataPoints.forEach { dataPoint ->
36 | if (dataPoint.second > tickMax) {
37 | tickMax = dataPoint.second
38 | }
39 | }
40 | if ((tickMax % 10U) == 0U) {
41 | return tickMax
42 | }
43 | return ((tickMax / 10U + 1U) * 10U)
44 | }
45 |
46 | private fun DpToPx(density: Float, dp: Float): Float {
47 | return (dp * density)
48 | }
49 |
50 | @Composable
51 | fun SingleLineChart(
52 | modifier: Modifier,
53 | dataPointList: DataPointList,
54 | lineColor: Color
55 | ) {
56 | val density = LocalContext.current.resources.displayMetrics.density
57 | val primaryColor = MaterialTheme.colorScheme.primary
58 | val tickMarkColor = Color(0xFFAAAAAA)
59 | val dataPoints = SimplifyDataPoints(dataPointList)
60 | val tickMax = GetDataPointsTickMax(dataPoints)
61 |
62 | Canvas(
63 | modifier = modifier
64 | ) {
65 | val nativeCanvas = drawContext.canvas.nativeCanvas
66 |
67 | for (i in 0..4) {
68 | val y = DpToPx(density, 5f) + (size.height - DpToPx(density, 10f)) * i / 4
69 | drawLine(
70 | color = tickMarkColor,
71 | strokeWidth = DpToPx(density, 1f),
72 | start = Offset(x = 0f, y = y),
73 | end = Offset(x = size.width - DpToPx(density, 20f), y = y),
74 | pathEffect = PathEffect.dashPathEffect(
75 | intervals = floatArrayOf(DpToPx(density, 1f), DpToPx(density, 1f)),
76 | phase = 0f
77 | )
78 | )
79 | }
80 |
81 | val tickPaint = Paint().let {
82 | it.apply {
83 | style = Paint.Style.FILL
84 | strokeWidth = DpToPx(density, 1f)
85 | textSize = DpToPx(density, 8f)
86 | color = primaryColor.toArgb()
87 | isAntiAlias = true
88 | }
89 | }
90 | for (i in 0..4) {
91 | val tickValue = tickMax.toDouble() * i / 4
92 | if (tickValue % 1.0 == 0.0) {
93 | val text = tickValue.toInt().toString()
94 | val y = (size.height - DpToPx(density, 2f)) -
95 | (size.height - DpToPx(density, 10f)) * i / 4
96 | nativeCanvas.drawText(text, size.width - DpToPx(density, 15f), y, tickPaint)
97 | }
98 | }
99 |
100 | val chartPaint = Paint().let {
101 | it.apply {
102 | style = Paint.Style.STROKE
103 | strokeWidth = DpToPx(density, 2f)
104 | color = lineColor.toArgb()
105 | isAntiAlias = true
106 | }
107 | }
108 | if (dataPoints.lastIndex > 2) {
109 | val chartPath = Path()
110 | val startX = 0f
111 | val startY = (size.height - DpToPx(density, 5f)) -
112 | (size.height - DpToPx(density, 10f)) *
113 | (dataPoints[0].second.toFloat() / tickMax.toFloat())
114 | chartPath.moveTo(startX, startY)
115 | var previousPoint = Offset(startX, startY)
116 | for (i in 1..dataPoints.lastIndex) {
117 | val x = (size.width - DpToPx(density, 20f)) *
118 | (dataPoints[i].first.toFloat() / dataPoints.last().first.toFloat())
119 | val y = (size.height - DpToPx(density, 5f)) -
120 | (size.height - DpToPx(density, 10f)) *
121 | (dataPoints[i].second.toFloat() / tickMax.toFloat())
122 | val currentPoint = Offset(x, y)
123 | val bezierControlPoint1 =
124 | previousPoint + Offset((currentPoint.x - previousPoint.x) * 0.25f, 0f)
125 | val bezierControlPoint2 =
126 | currentPoint - Offset((currentPoint.x - previousPoint.x) * 0.25f, 0f)
127 | chartPath.cubicTo(
128 | bezierControlPoint1.x,
129 | bezierControlPoint1.y,
130 | bezierControlPoint2.x,
131 | bezierControlPoint2.y,
132 | currentPoint.x,
133 | currentPoint.y
134 | )
135 | previousPoint = currentPoint
136 | }
137 | nativeCanvas.drawPath(chartPath, chartPaint)
138 | }
139 | }
140 | }
141 |
142 | @Composable
143 | fun MultiLineChart(
144 | modifier: Modifier,
145 | dataPointList0: DataPointList,
146 | dataPointList1: DataPointList,
147 | line0Color: Color,
148 | line1Color: Color,
149 | line0Title: String,
150 | line1Title: String
151 | ) {
152 | val density = LocalContext.current.resources.displayMetrics.density
153 | val primaryColor = MaterialTheme.colorScheme.primary
154 | val tickMarkColor = Color(0xFFAAAAAA)
155 | val line0DataPoints = SimplifyDataPoints(dataPointList0)
156 | val line1DataPoints = SimplifyDataPoints(dataPointList1)
157 | val line0TickMax = GetDataPointsTickMax(line0DataPoints)
158 | val line1TickMax = GetDataPointsTickMax(line1DataPoints)
159 |
160 | Canvas(
161 | modifier = modifier
162 | ) {
163 | val nativeCanvas = drawContext.canvas.nativeCanvas
164 |
165 | for (i in 0..4) {
166 | val y = DpToPx(density, 5f) + (size.height - DpToPx(density, 30f)) * i / 4
167 | drawLine(
168 | color = tickMarkColor,
169 | strokeWidth = DpToPx(density, 1f),
170 | start = Offset(x = DpToPx(density, 20f), y = y),
171 | end = Offset(x = size.width - DpToPx(density, 20f), y = y),
172 | pathEffect = PathEffect.dashPathEffect(
173 | intervals = floatArrayOf(DpToPx(density, 1f), DpToPx(density, 1f)),
174 | phase = 0f
175 | )
176 | )
177 | }
178 |
179 | val tickPaint = Paint().let {
180 | it.apply {
181 | style = Paint.Style.FILL
182 | strokeWidth = DpToPx(density, 1f)
183 | textSize = DpToPx(density, 8f)
184 | color = primaryColor.toArgb()
185 | isAntiAlias = true
186 | }
187 | }
188 | for (i in 0..4) {
189 | val tickValue = line0TickMax.toDouble() * i / 4
190 | if (tickValue % 1.0 == 0.0) {
191 | val text = tickValue.toInt().toString()
192 | val y = (size.height - DpToPx(density, 22f)) -
193 | (size.height - DpToPx(density, 30f)) * i / 4
194 | nativeCanvas.drawText(text, 0f, y, tickPaint)
195 | }
196 | }
197 | for (i in 1..4) {
198 | val tickValue = line1TickMax.toDouble() * i / 4
199 | if (tickValue % 1.0 == 0.0) {
200 | val text = tickValue.toInt().toString()
201 | val y = (size.height - DpToPx(density, 22f)) -
202 | (size.height - DpToPx(density, 30f)) * i / 4
203 | nativeCanvas.drawText(text, (size.width - DpToPx(density, 15f)), y, tickPaint)
204 | }
205 | }
206 |
207 | if (line0DataPoints.lastIndex > 2) {
208 | val chartPath = Path()
209 | val startX = DpToPx(density, 20f)
210 | val startY = (size.height - DpToPx(density, 25f)) -
211 | (size.height - DpToPx(density, 30f)) *
212 | (line0DataPoints[0].second.toFloat() / line0TickMax.toFloat())
213 | chartPath.moveTo(startX, startY)
214 | var previousPoint = Offset(startX, startY)
215 | for (i in 1..line0DataPoints.lastIndex) {
216 | val x = DpToPx(density, 20f) + (size.width - DpToPx(density, 40f)) *
217 | (line0DataPoints[i].first.toFloat() / line0DataPoints.last().first.toFloat())
218 | val y = (size.height - DpToPx(density, 25f)) -
219 | (size.height - DpToPx(density, 30f)) *
220 | (line0DataPoints[i].second.toFloat() / line0TickMax.toFloat())
221 | val currentPoint = Offset(x, y)
222 | val bezierControlPoint1 =
223 | previousPoint + Offset((currentPoint.x - previousPoint.x) * 0.25f, 0f)
224 | val bezierControlPoint2 =
225 | currentPoint - Offset((currentPoint.x - previousPoint.x) * 0.25f, 0f)
226 | chartPath.cubicTo(
227 | bezierControlPoint1.x,
228 | bezierControlPoint1.y,
229 | bezierControlPoint2.x,
230 | bezierControlPoint2.y,
231 | currentPoint.x,
232 | currentPoint.y
233 | )
234 | previousPoint = currentPoint
235 | }
236 | val chartPaint = Paint().let {
237 | it.apply {
238 | style = Paint.Style.STROKE
239 | strokeWidth = DpToPx(density, 2f)
240 | color = line0Color.toArgb()
241 | isAntiAlias = true
242 | }
243 | }
244 | nativeCanvas.drawPath(chartPath, chartPaint)
245 | }
246 |
247 | if (line1DataPoints.lastIndex > 2) {
248 | val chartPath = Path()
249 | val startX = DpToPx(density, 20f)
250 | val startY = (size.height - DpToPx(density, 25f)) -
251 | (size.height - DpToPx(density, 30f)) *
252 | (line1DataPoints[0].second.toFloat() / line1TickMax.toFloat())
253 | chartPath.moveTo(startX, startY)
254 | var previousPoint = Offset(startX, startY)
255 | for (i in 1..line1DataPoints.lastIndex) {
256 | val x = DpToPx(density, 20f) + (size.width - DpToPx(density, 40f)) *
257 | (line1DataPoints[i].first.toFloat() / line1DataPoints.last().first.toFloat())
258 | val y = (size.height - DpToPx(density, 25f)) -
259 | (size.height - DpToPx(density, 30f)) *
260 | (line1DataPoints[i].second.toFloat() / line1TickMax.toFloat())
261 | val currentPoint = Offset(x, y)
262 | val bezierControlPoint1 =
263 | previousPoint + Offset((currentPoint.x - previousPoint.x) * 0.25f, 0f)
264 | val bezierControlPoint2 =
265 | currentPoint - Offset((currentPoint.x - previousPoint.x) * 0.25f, 0f)
266 | chartPath.cubicTo(
267 | bezierControlPoint1.x,
268 | bezierControlPoint1.y,
269 | bezierControlPoint2.x,
270 | bezierControlPoint2.y,
271 | currentPoint.x,
272 | currentPoint.y
273 | )
274 | previousPoint = currentPoint
275 | }
276 | val chartPaint = Paint().let {
277 | it.apply {
278 | style = Paint.Style.STROKE
279 | strokeWidth = DpToPx(density, 2f)
280 | color = line1Color.toArgb()
281 | isAntiAlias = true
282 | }
283 | }
284 | nativeCanvas.drawPath(chartPath, chartPaint)
285 | }
286 |
287 | drawRect(
288 | color = line0Color,
289 | topLeft = Offset(DpToPx(density, 30f), size.height - DpToPx(density, 15f)),
290 | size = Size(width = DpToPx(density, 15f), height = DpToPx(density, 10f))
291 | )
292 | val line0TitlePaint = Paint().let {
293 | it.apply {
294 | style = Paint.Style.FILL
295 | strokeWidth = DpToPx(density, 1f)
296 | textSize = DpToPx(density, 8f)
297 | color = line0Color.toArgb()
298 | isAntiAlias = true
299 | }
300 | }
301 | nativeCanvas.drawText(
302 | line0Title,
303 | DpToPx(density, 50f),
304 | size.height - DpToPx(density, 8f),
305 | line0TitlePaint
306 | )
307 |
308 | drawRect(
309 | color = line1Color,
310 | topLeft = Offset(
311 | (size.width - DpToPx(density, 40f)) / 2,
312 | size.height - DpToPx(density, 15f)
313 | ),
314 | size = Size(width = DpToPx(density, 15f), height = DpToPx(density, 10f))
315 | )
316 | val line1TitlePaint = Paint().let {
317 | it.apply {
318 | style = Paint.Style.FILL
319 | strokeWidth = DpToPx(density, 1f)
320 | textSize = DpToPx(density, 8f)
321 | color = line1Color.toArgb()
322 | isAntiAlias = true
323 | }
324 | }
325 | nativeCanvas.drawText(
326 | line1Title,
327 | (size.width - DpToPx(density, 40f)) / 2 + DpToPx(density, 20f),
328 | size.height - DpToPx(density, 8f),
329 | line1TitlePaint
330 | )
331 | }
332 | }
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/utils/BatteryStatsRecordAnalysis.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats.utils
2 |
3 | import android.os.BatteryManager
4 | import cumulus.battery.stats.widgets.DataPointList
5 | import cumulus.battery.stats.widgets.DataPointMutableList
6 |
7 | data class BatteryHealthReport(
8 | var sampleSize: Int = 0,
9 | var totalChargedPercentage: Int = 0,
10 | var totalChargedCapacity: Int = 0,
11 | var estimatingCapacity: Int = 0
12 | )
13 |
14 | data class BatteryStatsReport(
15 | var duration: Long = 0L,
16 | var percentageDifference: Int = 0,
17 | var averagePower: Int = 0,
18 | var maxPower: Int = 0,
19 | var averageTemperature: Int = 0,
20 | var maxTemperature: Int = 0,
21 | var percentageDataPoints: DataPointList = listOf(),
22 | var powerDataPoints: DataPointList = listOf(),
23 | var temperatureDataPoints: DataPointList = listOf()
24 | )
25 |
26 | class BatteryStatsRecordAnalysis(private val records: List) {
27 | private val dischargingRecords = getLastDischargingRecords()
28 | private val chargingRecords = getLastChargingRecords()
29 |
30 | private fun getLastChargingRecords(): List {
31 | if (records.isNotEmpty()) {
32 | var endPos = 0
33 | for (i in records.lastIndex downTo 0) {
34 | if (records[i].batteryStatus == BatteryManager.BATTERY_STATUS_CHARGING) {
35 | endPos = i
36 | break
37 | }
38 | }
39 |
40 | var beginPos = endPos
41 | for (i in endPos downTo 0) {
42 | if (records[i].batteryStatus != BatteryManager.BATTERY_STATUS_CHARGING) {
43 | beginPos = i
44 | break
45 | }
46 | }
47 | if (beginPos == endPos) {
48 | beginPos = 0
49 | }
50 |
51 | return records.subList(beginPos, endPos)
52 | }
53 | return listOf()
54 | }
55 |
56 | private fun getLastDischargingRecords(): List {
57 | if (records.isNotEmpty()) {
58 | var endPos = 0
59 | for (i in records.lastIndex downTo 0) {
60 | if (records[i].batteryStatus == BatteryManager.BATTERY_STATUS_DISCHARGING ||
61 | records[i].batteryStatus == BatteryManager.BATTERY_STATUS_NOT_CHARGING
62 | ) {
63 | endPos = i
64 | break
65 | }
66 | }
67 |
68 | var beginPos = endPos
69 | for (i in endPos downTo 0) {
70 | if (records[i].batteryStatus != BatteryManager.BATTERY_STATUS_DISCHARGING &&
71 | records[i].batteryStatus != BatteryManager.BATTERY_STATUS_NOT_CHARGING
72 | ) {
73 | beginPos = i
74 | break
75 | }
76 | }
77 | if (beginPos == endPos) {
78 | beginPos = 0
79 | }
80 |
81 | return records.subList(beginPos, endPos)
82 | }
83 | return listOf()
84 | }
85 |
86 | private fun generateBatteryStatsReport(records: List): BatteryStatsReport? {
87 | if (records.size > 10) {
88 | val statsReport = BatteryStatsReport()
89 |
90 | val timestampList: MutableList = mutableListOf()
91 | val percentageList: MutableList = mutableListOf()
92 | val powerList: MutableList = mutableListOf()
93 | val temperatureList: MutableList = mutableListOf()
94 | for (item in records) {
95 | timestampList.add(item.timestamp)
96 | percentageList.add(item.batteryPercentage)
97 | if (item.batteryStatus == BatteryManager.BATTERY_STATUS_CHARGING) {
98 | powerList.add(
99 | when {
100 | (item.batteryPower > 0) -> item.batteryPower
101 | else -> 0
102 | }
103 | )
104 | } else {
105 | powerList.add(
106 | when {
107 | (item.batteryPower < 0) -> -item.batteryPower
108 | else -> 0
109 | }
110 | )
111 | }
112 | temperatureList.add(item.batteryTemperature)
113 | }
114 |
115 | statsReport.percentageDifference = percentageList.max() - percentageList.min()
116 | statsReport.averagePower = powerList.average().toInt()
117 | statsReport.maxPower = powerList.max()
118 | statsReport.averageTemperature = temperatureList.average().toInt()
119 | statsReport.maxTemperature = temperatureList.max()
120 |
121 | val percentageDataPoints: DataPointMutableList = mutableListOf()
122 | val powerDataPoints: DataPointMutableList = mutableListOf()
123 | val temperatureDataPoints: DataPointMutableList = mutableListOf()
124 | var durationSinceStart = 0U
125 | for (i in 1 until timestampList.size) {
126 | val duration = (timestampList[i] - timestampList[i - 1]).toUInt()
127 | if (duration < 5U) {
128 | durationSinceStart += duration
129 | percentageDataPoints.add(Pair(durationSinceStart, percentageList[i].toUInt()))
130 | powerDataPoints.add(Pair(durationSinceStart, (powerList[i] / 1000).toUInt()))
131 | temperatureDataPoints.add(Pair(durationSinceStart, temperatureList[i].toUInt()))
132 | }
133 | }
134 | statsReport.duration = durationSinceStart.toLong()
135 | statsReport.percentageDataPoints = percentageDataPoints
136 | statsReport.powerDataPoints = powerDataPoints
137 | statsReport.temperatureDataPoints = temperatureDataPoints
138 |
139 | return statsReport
140 | }
141 | return null
142 | }
143 |
144 | fun getUsagePercentageDataPoints(): DataPointList {
145 | if (records.isNotEmpty()) {
146 | val percentageDataPoints: DataPointMutableList = mutableListOf()
147 | val minTimestamp = GetTimeStamp() - 24L * 3600L
148 | for (i in records.lastIndex downTo 0) {
149 | if (records[i].timestamp > minTimestamp) {
150 | percentageDataPoints.add(
151 | Pair(
152 | (records[i].timestamp - minTimestamp).toUInt(),
153 | records[i].batteryPercentage.toUInt()
154 | )
155 | )
156 | } else {
157 | break
158 | }
159 | }
160 | return percentageDataPoints.reversed()
161 | }
162 | return listOf()
163 | }
164 |
165 | fun getScreenOnDuration(): Long {
166 | if (dischargingRecords.isNotEmpty()) {
167 | var duration = 0L
168 | for (i in (dischargingRecords.lastIndex - 1) downTo 0) {
169 | if (dischargingRecords[i].packageName != "standby") {
170 | duration += dischargingRecords[i + 1].timestamp - dischargingRecords[i].timestamp
171 | }
172 | }
173 | return duration
174 | }
175 | return 0L
176 | }
177 |
178 | fun getScreenOnAveragePower(): Int {
179 | if (dischargingRecords.isNotEmpty()) {
180 | var averagePower = 0.0
181 | var recordNum = 0
182 | for (i in dischargingRecords.lastIndex downTo 0) {
183 | if (dischargingRecords[i].packageName != "standby" &&
184 | dischargingRecords[i].batteryPower < 0
185 | ) {
186 | averagePower =
187 | (averagePower * recordNum + (-dischargingRecords[i].batteryPower)) / (recordNum + 1)
188 | recordNum++
189 | }
190 | }
191 | return averagePower.toInt()
192 | }
193 | return 0
194 | }
195 |
196 | fun getScreenOnUsedPercentage(): Int {
197 | if (dischargingRecords.isNotEmpty()) {
198 | var percentage = 0
199 | for (i in (dischargingRecords.lastIndex - 1) downTo 0) {
200 | if (dischargingRecords[i].packageName != "standby") {
201 | percentage +=
202 | dischargingRecords[i].batteryPercentage - dischargingRecords[i + 1].batteryPercentage
203 | }
204 | }
205 | return percentage
206 | }
207 | return 0
208 | }
209 |
210 | fun getScreenOffDuration(): Long {
211 | if (dischargingRecords.isNotEmpty()) {
212 | var duration = 0L
213 | for (i in (dischargingRecords.lastIndex - 1) downTo 0) {
214 | if (dischargingRecords[i].packageName == "standby") {
215 | duration += dischargingRecords[i + 1].timestamp - dischargingRecords[i].timestamp
216 | }
217 | }
218 | return duration
219 | }
220 | return 0L
221 | }
222 |
223 | fun getScreenOffAveragePower(): Int {
224 | if (dischargingRecords.isNotEmpty()) {
225 | var averagePower = 0.0
226 | var recordNum = 0
227 | for (i in dischargingRecords.lastIndex downTo 0) {
228 | if (dischargingRecords[i].packageName == "standby" &&
229 | dischargingRecords[i].batteryPower < 0
230 | ) {
231 | averagePower =
232 | (averagePower * recordNum + (-dischargingRecords[i].batteryPower)) / (recordNum + 1)
233 | recordNum++
234 | }
235 | }
236 | return averagePower.toInt()
237 | }
238 | return 0
239 | }
240 |
241 | fun getScreenOffUsedPercentage(): Int {
242 | if (dischargingRecords.isNotEmpty()) {
243 | var percentage = 0
244 | for (i in (dischargingRecords.lastIndex - 1) downTo 0) {
245 | if (dischargingRecords[i].packageName == "standby") {
246 | percentage +=
247 | dischargingRecords[i].batteryPercentage - dischargingRecords[i + 1].batteryPercentage
248 | }
249 | }
250 | return percentage
251 | }
252 | return 0
253 | }
254 |
255 | fun getRemainingUsageTime(): Long {
256 | val remainingPercentage = records.lastOrNull()?.batteryPercentage
257 | if (remainingPercentage != null && remainingPercentage > 0) {
258 | val screenOnDuration = getScreenOnDuration()
259 | val screenOnAveragePower = getScreenOnAveragePower()
260 | val screenOnUsedPercentage = getScreenOnUsedPercentage()
261 | if (screenOnDuration > 0 && screenOnAveragePower > 0 && screenOnUsedPercentage > 0) {
262 | val predictBatteryCapacity =
263 | (screenOnAveragePower.toDouble() * screenOnDuration / 3600.0) /
264 | ((screenOnUsedPercentage.toDouble() + 1.0) / 100.0)
265 | val remainingBatteryCapacity = predictBatteryCapacity * remainingPercentage / 100.0
266 | return (remainingBatteryCapacity / screenOnAveragePower * 3600.0).toLong()
267 | }
268 | }
269 | return 0L
270 | }
271 |
272 | fun getPerappBatteryStatsReport(): Map {
273 | val perappBatteryStatsReport: MutableMap = mutableMapOf()
274 | if (dischargingRecords.isNotEmpty()) {
275 | val perappRecords: MutableMap> = mutableMapOf()
276 | for (item in dischargingRecords) {
277 | if (!perappRecords.contains(item.packageName)) {
278 | perappRecords[item.packageName] = mutableListOf()
279 | }
280 | perappRecords[item.packageName]!!.add(item)
281 | }
282 | for ((pkgName, records) in perappRecords) {
283 | if (pkgName != "other" && pkgName != "standby") {
284 | val batteryStatsReport = generateBatteryStatsReport(records)
285 | if (batteryStatsReport != null) {
286 | perappBatteryStatsReport[pkgName] = batteryStatsReport
287 | }
288 | }
289 | }
290 | }
291 | return perappBatteryStatsReport
292 | }
293 |
294 | fun getChargingBatteryStatsReport(): BatteryStatsReport {
295 | val batteryStatsReport = generateBatteryStatsReport(chargingRecords)
296 | if (batteryStatsReport != null) {
297 | return batteryStatsReport
298 | }
299 | return BatteryStatsReport()
300 | }
301 |
302 | fun getBatteryHealthReport(): BatteryHealthReport {
303 | if (records.isNotEmpty()) {
304 | val samples: MutableList> = mutableListOf()
305 | var pos = 0
306 | while (pos < records.lastIndex) {
307 | if (records[pos].batteryStatus == BatteryManager.BATTERY_STATUS_CHARGING) {
308 | val sample: MutableList = mutableListOf()
309 | for (i in pos until records.size) {
310 | if (records[i].batteryStatus == BatteryManager.BATTERY_STATUS_CHARGING) {
311 | sample.add(records[i])
312 | } else {
313 | break
314 | }
315 | }
316 | samples.add(sample)
317 | pos += sample.size
318 | } else {
319 | pos++
320 | }
321 | }
322 |
323 | val report = BatteryHealthReport()
324 | for (sample in samples) {
325 | val chargedPercentage =
326 | sample.last().batteryPercentage - sample.first().batteryPercentage
327 | if (sample.size > 100 && chargedPercentage > 50) {
328 | report.totalChargedPercentage += chargedPercentage
329 | report.sampleSize++
330 |
331 | var chargedCapacity = 0.0
332 | for (i in 0 until sample.lastIndex) {
333 | val duration = sample[i + 1].timestamp - sample[i].timestamp
334 | val power =
335 | (sample[i].batteryPower + sample[i + 1].batteryPower).toDouble() / 2.0
336 | chargedCapacity += power * duration / 3600.0
337 | }
338 | report.totalChargedCapacity += chargedCapacity.toInt()
339 | }
340 | }
341 | if (report.totalChargedPercentage > 0 && report.totalChargedCapacity > 0) {
342 | report.estimatingCapacity =
343 | (report.totalChargedCapacity.toDouble() * 100.0 / report.totalChargedPercentage).toInt()
344 | }
345 | return report
346 | }
347 | return BatteryHealthReport()
348 | }
349 | }
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.ActivityManager
5 | import android.content.Intent
6 | import android.content.res.Resources
7 | import android.os.BatteryManager
8 | import android.os.Bundle
9 | import android.provider.Settings
10 | import android.view.KeyEvent
11 | import android.widget.Toast
12 | import androidx.activity.ComponentActivity
13 | import androidx.activity.compose.setContent
14 | import androidx.appcompat.content.res.AppCompatResources
15 | import androidx.compose.animation.AnimatedVisibility
16 | import androidx.compose.foundation.Image
17 | import androidx.compose.foundation.background
18 | import androidx.compose.foundation.layout.Arrangement
19 | import androidx.compose.foundation.layout.Column
20 | import androidx.compose.foundation.layout.PaddingValues
21 | import androidx.compose.foundation.layout.Row
22 | import androidx.compose.foundation.layout.fillMaxSize
23 | import androidx.compose.foundation.layout.fillMaxWidth
24 | import androidx.compose.foundation.layout.height
25 | import androidx.compose.foundation.layout.padding
26 | import androidx.compose.foundation.layout.width
27 | import androidx.compose.foundation.rememberScrollState
28 | import androidx.compose.foundation.shape.RoundedCornerShape
29 | import androidx.compose.foundation.verticalScroll
30 | import androidx.compose.material3.MaterialTheme
31 | import androidx.compose.material3.Scaffold
32 | import androidx.compose.material3.Surface
33 | import androidx.compose.material3.Text
34 | import androidx.compose.material3.TextButton
35 | import androidx.compose.runtime.Composable
36 | import androidx.compose.runtime.getValue
37 | import androidx.compose.runtime.mutableIntStateOf
38 | import androidx.compose.runtime.mutableLongStateOf
39 | import androidx.compose.runtime.mutableStateOf
40 | import androidx.compose.runtime.setValue
41 | import androidx.compose.ui.Alignment
42 | import androidx.compose.ui.Modifier
43 | import androidx.compose.ui.res.painterResource
44 | import androidx.compose.ui.text.font.FontWeight
45 | import androidx.compose.ui.text.style.TextOverflow
46 | import androidx.compose.ui.unit.dp
47 | import androidx.compose.ui.unit.sp
48 | import cumulus.battery.stats.objects.BatteryStatsProvider
49 | import cumulus.battery.stats.objects.BatteryStatsRecorder
50 | import cumulus.battery.stats.ui.theme.CumulusTheme
51 | import cumulus.battery.stats.ui.theme.cumulusColor
52 | import cumulus.battery.stats.utils.BatteryStatsRecordAnalysis
53 | import cumulus.battery.stats.utils.DurationToText
54 | import cumulus.battery.stats.widgets.DataPointList
55 | import cumulus.battery.stats.widgets.GoToButton
56 | import cumulus.battery.stats.widgets.SingleLineChart
57 | import kotlinx.coroutines.CoroutineScope
58 | import kotlinx.coroutines.Dispatchers
59 | import kotlinx.coroutines.launch
60 | import java.util.Timer
61 | import java.util.TimerTask
62 |
63 | class MainActivity : ComponentActivity() {
64 | private var timer: Timer? = null
65 | private var backgroundServiceCreated by mutableStateOf(false)
66 | private var currentAdjusted by mutableStateOf(false)
67 | private var batteryCapacity: Int by mutableIntStateOf(0)
68 | private var batteryCurrent: Int by mutableIntStateOf(0)
69 | private var batteryPower: Int by mutableIntStateOf(0)
70 | private var batteryTemperature: Int by mutableIntStateOf(0)
71 | private var batteryStatus: Int by mutableIntStateOf(BatteryManager.BATTERY_STATUS_UNKNOWN)
72 | private var batteryPercentageDataPoints: DataPointList by mutableStateOf(listOf())
73 | private var screenOnDuration: Long by mutableLongStateOf(0)
74 | private var remainingUsageTime: Long by mutableLongStateOf(0)
75 |
76 | override fun onCreate(savedInstanceState: Bundle?) {
77 | super.onCreate(savedInstanceState)
78 | BatteryStatsProvider.init(applicationContext)
79 | BatteryStatsRecorder.init(applicationContext)
80 | setContent {
81 | CumulusTheme {
82 | Surface(
83 | modifier = Modifier.fillMaxSize(),
84 | color = MaterialTheme.colorScheme.background
85 | ) {
86 | Scaffold(
87 | modifier = Modifier.fillMaxSize(),
88 | topBar = {
89 | Row(
90 | modifier = Modifier
91 | .padding(top = 10.dp, start = 30.dp)
92 | .height(50.dp)
93 | .fillMaxWidth(),
94 | horizontalArrangement = Arrangement.Start,
95 | verticalAlignment = Alignment.CenterVertically
96 | ) {
97 | Image(
98 | painter = painterResource(id = R.mipmap.icon_round),
99 | contentDescription = "icon",
100 | modifier = Modifier
101 | .height(40.dp)
102 | .width(40.dp)
103 | )
104 | Text(
105 | modifier = Modifier
106 | .padding(start = 20.dp),
107 | text = "Cumulus",
108 | fontSize = 24.sp,
109 | fontWeight = FontWeight.Bold,
110 | color = MaterialTheme.colorScheme.primary
111 | )
112 | }
113 | }
114 | )
115 | {
116 | Column(
117 | modifier = Modifier
118 | .fillMaxSize()
119 | .padding(
120 | top = it.calculateTopPadding() + 20.dp,
121 | start = 20.dp,
122 | end = 20.dp
123 | )
124 | .verticalScroll(rememberScrollState()),
125 | verticalArrangement = Arrangement.Top,
126 | horizontalAlignment = Alignment.CenterHorizontally
127 | ) {
128 | BatteryStatsBar()
129 | BackgroundServiceHint()
130 | AdjustCurrentHint()
131 | BatteryBasicInfoBar()
132 | PowerConsumptionAnalysisButton()
133 | ChargingProcessButton()
134 | AdditionalFunctionButton()
135 | SettingsButton()
136 | }
137 | }
138 | }
139 | }
140 | }
141 | }
142 |
143 | override fun getResources(): Resources {
144 | val resources = super.getResources()
145 | val configContext = createConfigurationContext(resources.configuration)
146 | return configContext.resources.apply {
147 | configuration.fontScale = 1.0f
148 | }
149 | }
150 |
151 | override fun onStart() {
152 | super.onStart()
153 | startTimer()
154 | updateRecordAnalysis()
155 | }
156 |
157 | override fun onStop() {
158 | stopTimer()
159 | super.onStop()
160 | }
161 |
162 | override fun onDestroy() {
163 | BatteryStatsRecorder.optimize()
164 | super.onDestroy()
165 | }
166 |
167 | @SuppressLint("ServiceCast")
168 | override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
169 | if (event.keyCode == KeyEvent.KEYCODE_BACK) {
170 | try {
171 | val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
172 | val tasks = activityManager.appTasks.filterNotNull()
173 | tasks.forEach { task ->
174 | task.setExcludeFromRecents(true)
175 | }
176 | } catch (e: Exception) {
177 | e.printStackTrace()
178 | }
179 | finish()
180 | return false
181 | }
182 | return super.onKeyUp(keyCode, event)
183 | }
184 |
185 | @Composable
186 | private fun BatteryStatsBar() {
187 | val statusString = hashMapOf(
188 | BatteryManager.BATTERY_STATUS_FULL to "已充满电",
189 | BatteryManager.BATTERY_STATUS_CHARGING to "充电中",
190 | BatteryManager.BATTERY_STATUS_DISCHARGING to "放电中",
191 | BatteryManager.BATTERY_STATUS_NOT_CHARGING to "未在充电",
192 | BatteryManager.BATTERY_STATUS_UNKNOWN to "未知状态"
193 | )
194 |
195 | Row(
196 | modifier = Modifier
197 | .height(80.dp)
198 | .fillMaxWidth()
199 | .background(
200 | color = MaterialTheme.colorScheme.surface,
201 | shape = RoundedCornerShape(10.dp)
202 | ),
203 | horizontalArrangement = Arrangement.Start,
204 | verticalAlignment = Alignment.CenterVertically
205 | ) {
206 | Text(
207 | modifier = Modifier.padding(start = 20.dp),
208 | text = "${batteryCapacity}%",
209 | fontSize = 40.sp,
210 | fontWeight = FontWeight.Bold,
211 | color = cumulusColor().blue,
212 | maxLines = 1,
213 | overflow = TextOverflow.Ellipsis
214 | )
215 | Column(
216 | modifier = Modifier
217 | .padding(start = 20.dp)
218 | .fillMaxSize(),
219 | verticalArrangement = Arrangement.Center,
220 | horizontalAlignment = Alignment.Start
221 | ) {
222 | Text(
223 | text = statusString[batteryStatus]!!,
224 | fontSize = 14.sp,
225 | fontWeight = FontWeight.Bold,
226 | color = cumulusColor().blue,
227 | maxLines = 1,
228 | overflow = TextOverflow.Ellipsis
229 | )
230 | Text(
231 | text = "${batteryPower} mW (${batteryCurrent} mA) ${batteryTemperature} °C",
232 | fontSize = 14.sp,
233 | fontWeight = FontWeight.Medium,
234 | color = MaterialTheme.colorScheme.secondary,
235 | maxLines = 1,
236 | overflow = TextOverflow.Ellipsis
237 | )
238 | }
239 | }
240 | }
241 |
242 | @Composable
243 | private fun BackgroundServiceHint() {
244 | AnimatedVisibility(visible = !backgroundServiceCreated) {
245 | val buttonColor = cumulusColor().yellow
246 | TextButton(
247 | onClick = {
248 | Toast.makeText(
249 | applicationContext,
250 | "打开Cumulus无障碍以启用后台服务",
251 | Toast.LENGTH_LONG
252 | ).show()
253 | val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
254 | startActivity(intent)
255 | },
256 | contentPadding = PaddingValues(0.dp),
257 | modifier = Modifier
258 | .padding(top = 10.dp)
259 | .height(50.dp)
260 | .fillMaxWidth()
261 | .background(
262 | color = buttonColor,
263 | shape = RoundedCornerShape(10.dp)
264 | ),
265 | shape = RoundedCornerShape(10.dp)
266 | ) {
267 | Row(
268 | modifier = Modifier
269 | .fillMaxSize()
270 | .padding(start = 20.dp, end = 20.dp),
271 | horizontalArrangement = Arrangement.Start,
272 | verticalAlignment = Alignment.CenterVertically
273 | ) {
274 | Image(
275 | painter = painterResource(id = R.drawable.warning),
276 | contentDescription = null,
277 | modifier = Modifier
278 | .height(24.dp)
279 | .width(24.dp)
280 | )
281 | Text(
282 | modifier = Modifier.padding(start = 5.dp),
283 | text = "后台服务未运行, 点击此处启用",
284 | fontSize = 14.sp,
285 | fontWeight = FontWeight.Bold,
286 | color = MaterialTheme.colorScheme.primary,
287 | maxLines = 1,
288 | overflow = TextOverflow.Ellipsis
289 | )
290 | }
291 | }
292 | }
293 | }
294 |
295 | @Composable
296 | private fun AdjustCurrentHint() {
297 | AnimatedVisibility(visible = !currentAdjusted) {
298 | val buttonColor = cumulusColor().yellow
299 | TextButton(
300 | onClick = {
301 | val intent =
302 | Intent(applicationContext, CurrentAdjustActivity::class.java)
303 | startActivity(intent)
304 | },
305 | contentPadding = PaddingValues(0.dp),
306 | modifier = Modifier
307 | .padding(top = 10.dp)
308 | .height(50.dp)
309 | .fillMaxWidth()
310 | .background(
311 | color = buttonColor,
312 | shape = RoundedCornerShape(10.dp)
313 | ),
314 | shape = RoundedCornerShape(10.dp)
315 | ) {
316 | Row(
317 | modifier = Modifier
318 | .fillMaxSize()
319 | .padding(start = 20.dp, end = 20.dp),
320 | horizontalArrangement = Arrangement.Start,
321 | verticalAlignment = Alignment.CenterVertically
322 | ) {
323 | Image(
324 | painter = painterResource(id = R.drawable.warning),
325 | contentDescription = null,
326 | modifier = Modifier
327 | .height(24.dp)
328 | .width(24.dp)
329 | )
330 | Text(
331 | modifier = Modifier.padding(start = 5.dp),
332 | text = "请确保电流值显示正确, 点击此处调整",
333 | fontSize = 14.sp,
334 | fontWeight = FontWeight.Bold,
335 | color = MaterialTheme.colorScheme.primary,
336 | maxLines = 1,
337 | overflow = TextOverflow.Ellipsis
338 | )
339 | }
340 | }
341 | }
342 | }
343 |
344 | @Composable
345 | private fun BatteryBasicInfoBar() {
346 | Column(
347 | modifier = Modifier
348 | .fillMaxWidth()
349 | .height(190.dp)
350 | .padding(top = 10.dp)
351 | .background(
352 | shape = RoundedCornerShape(10.dp),
353 | color = MaterialTheme.colorScheme.surface
354 | ),
355 | verticalArrangement = Arrangement.Top,
356 | horizontalAlignment = Alignment.Start
357 | ) {
358 | SingleLineChart(
359 | dataPointList = batteryPercentageDataPoints,
360 | modifier = Modifier
361 | .padding(start = 20.dp, end = 20.dp, top = 20.dp)
362 | .height(120.dp)
363 | .fillMaxWidth(),
364 | lineColor = cumulusColor().blue
365 | )
366 | Row(
367 | modifier = Modifier
368 | .padding(start = 20.dp, top = 10.dp)
369 | .fillMaxWidth()
370 | .height(20.dp),
371 | horizontalArrangement = Arrangement.Start,
372 | verticalAlignment = Alignment.CenterVertically
373 | ) {
374 | Text(
375 | text = "屏幕使用",
376 | fontSize = 14.sp,
377 | fontWeight = FontWeight.Normal,
378 | color = MaterialTheme.colorScheme.primary,
379 | maxLines = 1,
380 | overflow = TextOverflow.Ellipsis
381 | )
382 | Text(
383 | text = DurationToText(screenOnDuration),
384 | fontSize = 14.sp,
385 | fontWeight = FontWeight.Medium,
386 | color = cumulusColor().blue,
387 | maxLines = 1,
388 | overflow = TextOverflow.Ellipsis
389 | )
390 | Text(
391 | text = ", 预计可用",
392 | fontSize = 14.sp,
393 | fontWeight = FontWeight.Normal,
394 | color = MaterialTheme.colorScheme.primary,
395 | maxLines = 1,
396 | overflow = TextOverflow.Ellipsis
397 | )
398 | Text(
399 | text = DurationToText(remainingUsageTime),
400 | fontSize = 14.sp,
401 | fontWeight = FontWeight.Medium,
402 | color = cumulusColor().blue,
403 | maxLines = 1,
404 | overflow = TextOverflow.Ellipsis
405 | )
406 | }
407 | }
408 | }
409 |
410 | @Composable
411 | private fun PowerConsumptionAnalysisButton() {
412 | GoToButton(
413 | modifier = Modifier
414 | .padding(top = 10.dp)
415 | .height(50.dp)
416 | .fillMaxWidth(),
417 | icon = AppCompatResources.getDrawable(applicationContext, R.drawable.analysis),
418 | text = "耗电分析"
419 | ) {
420 | val intent =
421 | Intent(applicationContext, PowerConsumptionAnalysisActivity::class.java)
422 | startActivity(intent)
423 | }
424 | }
425 |
426 | @Composable
427 | private fun ChargingProcessButton() {
428 | GoToButton(
429 | modifier = Modifier
430 | .padding(top = 5.dp)
431 | .height(50.dp)
432 | .fillMaxWidth(),
433 | icon = AppCompatResources.getDrawable(applicationContext, R.drawable.charging),
434 | text = "充电过程"
435 | ) {
436 | val intent = Intent(applicationContext, ChargingProcessActivity::class.java)
437 | startActivity(intent)
438 | }
439 | }
440 |
441 | @Composable
442 | private fun AdditionalFunctionButton() {
443 | GoToButton(
444 | modifier = Modifier
445 | .padding(top = 5.dp)
446 | .height(50.dp)
447 | .fillMaxWidth(),
448 | icon = AppCompatResources.getDrawable(applicationContext, R.drawable.apps),
449 | text = "附加功能"
450 | ) {
451 | val intent = Intent(applicationContext, AdditionalFunctionActivity::class.java)
452 | startActivity(intent)
453 | }
454 | }
455 |
456 | @Composable
457 | private fun SettingsButton() {
458 | GoToButton(
459 | modifier = Modifier
460 | .padding(top = 5.dp)
461 | .height(50.dp)
462 | .fillMaxWidth(),
463 | icon = AppCompatResources.getDrawable(applicationContext, R.drawable.settings),
464 | text = "设置"
465 | ) {
466 | val intent = Intent(applicationContext, SettingsActivity::class.java)
467 | startActivity(intent)
468 | }
469 | }
470 |
471 | private fun updateRecordAnalysis() {
472 | CoroutineScope(Dispatchers.Default).launch {
473 | val records = BatteryStatsRecorder.getRecords()
474 | val recordAnalysis = BatteryStatsRecordAnalysis(records)
475 | batteryPercentageDataPoints = recordAnalysis.getUsagePercentageDataPoints()
476 | screenOnDuration = recordAnalysis.getScreenOnDuration()
477 | remainingUsageTime = recordAnalysis.getRemainingUsageTime()
478 | }
479 | }
480 |
481 | private fun updateBatteryStats() {
482 | backgroundServiceCreated = BackgroundService.isServiceCreated()
483 | currentAdjusted = BatteryStatsProvider.isCurrentAdjusted()
484 | batteryCapacity = BatteryStatsProvider.getBatteryCapacity(applicationContext)
485 | batteryCurrent = BatteryStatsProvider.getBatteryCurrent(applicationContext)
486 | batteryPower =
487 | BatteryStatsProvider.getBatteryVoltage(applicationContext) * batteryCurrent / 1000
488 | batteryTemperature = BatteryStatsProvider.getBatteryTemperature(applicationContext)
489 | batteryStatus = BatteryStatsProvider.getBatteryStatus(applicationContext)
490 | }
491 |
492 | private fun startTimer() {
493 | if (timer != null) {
494 | return
495 | }
496 | timer = Timer()
497 | timer!!.schedule(object : TimerTask() {
498 | override fun run() {
499 | updateBatteryStats()
500 | }
501 | }, 0, 1000)
502 | }
503 |
504 | private fun stopTimer() {
505 | if (timer == null) {
506 | return
507 | }
508 | timer!!.cancel()
509 | timer = null
510 | }
511 | }
512 |
--------------------------------------------------------------------------------
/app/src/main/java/cumulus/battery/stats/PowerConsumptionAnalysisActivity.kt:
--------------------------------------------------------------------------------
1 | package cumulus.battery.stats
2 |
3 | import android.content.pm.PackageManager
4 | import android.content.res.Resources
5 | import android.graphics.drawable.Drawable
6 | import android.os.Bundle
7 | import androidx.activity.ComponentActivity
8 | import androidx.activity.compose.setContent
9 | import androidx.compose.animation.AnimatedVisibility
10 | import androidx.compose.foundation.Image
11 | import androidx.compose.foundation.background
12 | import androidx.compose.foundation.layout.Arrangement
13 | import androidx.compose.foundation.layout.Column
14 | import androidx.compose.foundation.layout.PaddingValues
15 | import androidx.compose.foundation.layout.Row
16 | import androidx.compose.foundation.layout.fillMaxHeight
17 | import androidx.compose.foundation.layout.fillMaxSize
18 | import androidx.compose.foundation.layout.fillMaxWidth
19 | import androidx.compose.foundation.layout.height
20 | import androidx.compose.foundation.layout.padding
21 | import androidx.compose.foundation.layout.width
22 | import androidx.compose.foundation.rememberScrollState
23 | import androidx.compose.foundation.shape.RoundedCornerShape
24 | import androidx.compose.foundation.verticalScroll
25 | import androidx.compose.material3.MaterialTheme
26 | import androidx.compose.material3.Scaffold
27 | import androidx.compose.material3.Surface
28 | import androidx.compose.material3.Text
29 | import androidx.compose.material3.TextButton
30 | import androidx.compose.runtime.Composable
31 | import androidx.compose.runtime.getValue
32 | import androidx.compose.runtime.mutableIntStateOf
33 | import androidx.compose.runtime.mutableLongStateOf
34 | import androidx.compose.runtime.mutableStateOf
35 | import androidx.compose.runtime.remember
36 | import androidx.compose.runtime.setValue
37 | import androidx.compose.ui.Alignment
38 | import androidx.compose.ui.Modifier
39 | import androidx.compose.ui.graphics.asImageBitmap
40 | import androidx.compose.ui.res.painterResource
41 | import androidx.compose.ui.text.font.FontWeight
42 | import androidx.compose.ui.text.style.TextOverflow
43 | import androidx.compose.ui.unit.dp
44 | import androidx.compose.ui.unit.sp
45 | import androidx.core.graphics.drawable.toBitmap
46 | import cumulus.battery.stats.objects.BatteryStatsRecorder
47 | import cumulus.battery.stats.ui.theme.CumulusTheme
48 | import cumulus.battery.stats.ui.theme.cumulusColor
49 | import cumulus.battery.stats.utils.BatteryStatsRecordAnalysis
50 | import cumulus.battery.stats.utils.BatteryStatsReport
51 | import cumulus.battery.stats.utils.DurationToText
52 | import cumulus.battery.stats.widgets.MultiLineChart
53 | import kotlinx.coroutines.CoroutineScope
54 | import kotlinx.coroutines.Dispatchers
55 | import kotlinx.coroutines.launch
56 |
57 | class PowerConsumptionAnalysisActivity : ComponentActivity() {
58 | private var screenOnUsedPercentage: Int by mutableIntStateOf(0)
59 | private var screenOffUsedPercentage: Int by mutableIntStateOf(0)
60 | private var screenOnAveragePower: Int by mutableIntStateOf(0)
61 | private var screenOffAveragePower: Int by mutableIntStateOf(0)
62 | private var screenOnDuration: Long by mutableLongStateOf(0)
63 | private var screenOffDuration: Long by mutableLongStateOf(0)
64 | private var perappBatteryStatsReport: Map by mutableStateOf(mapOf())
65 |
66 | override fun onCreate(savedInstanceState: Bundle?) {
67 | super.onCreate(savedInstanceState)
68 | setContent {
69 | CumulusTheme {
70 | Surface(
71 | modifier = Modifier.fillMaxSize(),
72 | color = MaterialTheme.colorScheme.background
73 | ) {
74 | Scaffold(
75 | modifier = Modifier.fillMaxSize(),
76 | topBar = {
77 | Row(
78 | modifier = Modifier
79 | .padding(top = 10.dp, start = 10.dp, end = 10.dp)
80 | .height(50.dp)
81 | .fillMaxWidth(),
82 | horizontalArrangement = Arrangement.Start,
83 | verticalAlignment = Alignment.CenterVertically
84 | ) {
85 | TextButton(
86 | onClick = { finish() },
87 | modifier = Modifier
88 | .height(50.dp)
89 | .width(50.dp),
90 | shape = RoundedCornerShape(10.dp)
91 | ) {
92 | Image(
93 | painter = painterResource(id = R.drawable.arrow_back),
94 | contentDescription = null,
95 | modifier = Modifier
96 | .height(32.dp)
97 | .width(32.dp)
98 | )
99 | }
100 | Text(
101 | modifier = Modifier
102 | .padding(start = 10.dp),
103 | text = "耗电分析",
104 | fontSize = 18.sp,
105 | fontWeight = FontWeight.Bold,
106 | color = MaterialTheme.colorScheme.primary
107 | )
108 | }
109 | }
110 | ) {
111 | Column(
112 | modifier = Modifier
113 | .padding(
114 | top = it.calculateTopPadding() + 10.dp,
115 | start = 20.dp,
116 | end = 20.dp
117 | )
118 | .fillMaxSize()
119 | .padding(top = 10.dp),
120 | verticalArrangement = Arrangement.Top,
121 | horizontalAlignment = Alignment.CenterHorizontally
122 | ) {
123 | PowerConsumptionBasicInfoBar()
124 | Text(
125 | modifier = Modifier
126 | .padding(start = 20.dp, top = 20.dp)
127 | .fillMaxWidth(),
128 | text = "应用使用详情",
129 | fontSize = 14.sp,
130 | fontWeight = FontWeight.Bold,
131 | color = cumulusColor().purple
132 | )
133 | AppDetailsList()
134 | }
135 | }
136 | }
137 | }
138 | }
139 | }
140 |
141 | override fun getResources(): Resources {
142 | val resources = super.getResources()
143 | val configContext = createConfigurationContext(resources.configuration)
144 | return configContext.resources.apply {
145 | configuration.fontScale = 1.0f
146 | }
147 | }
148 |
149 | override fun onStart() {
150 | super.onStart()
151 | updateRecordAnalysis()
152 | }
153 |
154 | @Composable
155 | private fun PowerConsumptionBasicInfoBar() {
156 | Column(
157 | modifier = Modifier
158 | .fillMaxWidth()
159 | .height(120.dp)
160 | .background(
161 | color = MaterialTheme.colorScheme.surface,
162 | shape = RoundedCornerShape(10.dp)
163 | ),
164 | verticalArrangement = Arrangement.Center,
165 | horizontalAlignment = Alignment.CenterHorizontally
166 | ) {
167 | Row(
168 | modifier = Modifier
169 | .fillMaxWidth()
170 | .height(50.dp),
171 | horizontalArrangement = Arrangement.Center,
172 | verticalAlignment = Alignment.CenterVertically
173 | ) {
174 | Column(
175 | modifier = Modifier
176 | .width(100.dp)
177 | .height(50.dp),
178 | verticalArrangement = Arrangement.Center,
179 | horizontalAlignment = Alignment.CenterHorizontally
180 | ) {
181 | Text(
182 | text = "亮屏耗电",
183 | fontSize = 12.sp,
184 | fontWeight = FontWeight.Medium,
185 | color = MaterialTheme.colorScheme.secondary
186 | )
187 | Text(
188 | text = "${screenOnUsedPercentage}%",
189 | fontSize = 14.sp,
190 | fontWeight = FontWeight.Bold,
191 | color = cumulusColor().blue,
192 | maxLines = 1,
193 | overflow = TextOverflow.Ellipsis
194 | )
195 | }
196 | Column(
197 | modifier = Modifier
198 | .width(100.dp)
199 | .height(50.dp),
200 | verticalArrangement = Arrangement.Center,
201 | horizontalAlignment = Alignment.CenterHorizontally
202 | ) {
203 | Text(
204 | text = "亮屏平均功耗",
205 | fontSize = 12.sp,
206 | fontWeight = FontWeight.Medium,
207 | color = MaterialTheme.colorScheme.secondary
208 | )
209 | Text(
210 | text = "${screenOnAveragePower}mW",
211 | fontSize = 14.sp,
212 | fontWeight = FontWeight.Bold,
213 | color = cumulusColor().blue,
214 | maxLines = 1,
215 | overflow = TextOverflow.Ellipsis
216 | )
217 | }
218 | Column(
219 | modifier = Modifier
220 | .width(100.dp)
221 | .height(50.dp),
222 | verticalArrangement = Arrangement.Center,
223 | horizontalAlignment = Alignment.CenterHorizontally
224 | ) {
225 | Text(
226 | text = "亮屏时长",
227 | fontSize = 12.sp,
228 | fontWeight = FontWeight.Medium,
229 | color = MaterialTheme.colorScheme.secondary
230 | )
231 | Text(
232 | text = DurationToText(screenOnDuration),
233 | fontSize = 14.sp,
234 | fontWeight = FontWeight.Bold,
235 | color = cumulusColor().blue,
236 | maxLines = 1,
237 | overflow = TextOverflow.Ellipsis
238 | )
239 | }
240 | }
241 | Row(
242 | modifier = Modifier
243 | .fillMaxWidth()
244 | .height(50.dp),
245 | horizontalArrangement = Arrangement.Center,
246 | verticalAlignment = Alignment.CenterVertically
247 | ) {
248 | Column(
249 | modifier = Modifier
250 | .width(100.dp)
251 | .height(50.dp),
252 | verticalArrangement = Arrangement.Center,
253 | horizontalAlignment = Alignment.CenterHorizontally
254 | ) {
255 | Text(
256 | text = "熄屏耗电",
257 | fontSize = 12.sp,
258 | fontWeight = FontWeight.Medium,
259 | color = MaterialTheme.colorScheme.secondary
260 | )
261 | Text(
262 | text = "${screenOffUsedPercentage}%",
263 | fontSize = 14.sp,
264 | fontWeight = FontWeight.Bold,
265 | color = cumulusColor().blue,
266 | maxLines = 1,
267 | overflow = TextOverflow.Ellipsis
268 | )
269 | }
270 | Column(
271 | modifier = Modifier
272 | .width(100.dp)
273 | .height(50.dp),
274 | verticalArrangement = Arrangement.Center,
275 | horizontalAlignment = Alignment.CenterHorizontally
276 | ) {
277 | Text(
278 | text = "熄屏平均功耗",
279 | fontSize = 12.sp,
280 | fontWeight = FontWeight.Medium,
281 | color = MaterialTheme.colorScheme.secondary
282 | )
283 | Text(
284 | text = "${screenOffAveragePower}mW",
285 | fontSize = 14.sp,
286 | fontWeight = FontWeight.Bold,
287 | color = cumulusColor().blue,
288 | maxLines = 1,
289 | overflow = TextOverflow.Ellipsis
290 | )
291 | }
292 | Column(
293 | modifier = Modifier
294 | .width(100.dp)
295 | .height(50.dp),
296 | verticalArrangement = Arrangement.Center,
297 | horizontalAlignment = Alignment.CenterHorizontally
298 | ) {
299 | Text(
300 | text = "熄屏时长",
301 | fontSize = 12.sp,
302 | fontWeight = FontWeight.Medium,
303 | color = MaterialTheme.colorScheme.secondary
304 | )
305 | Text(
306 | text = DurationToText(screenOffDuration),
307 | fontSize = 14.sp,
308 | fontWeight = FontWeight.Bold,
309 | color = cumulusColor().blue,
310 | maxLines = 1,
311 | overflow = TextOverflow.Ellipsis
312 | )
313 | }
314 | }
315 | }
316 | }
317 |
318 | @Composable
319 | private fun AppDetailsList() {
320 | Column(
321 | modifier = Modifier
322 | .padding(top = 5.dp)
323 | .fillMaxSize()
324 | .verticalScroll(rememberScrollState()),
325 | verticalArrangement = Arrangement.Top,
326 | horizontalAlignment = Alignment.CenterHorizontally
327 | ) {
328 | for ((pkgName, batteryStatsReport) in perappBatteryStatsReport) {
329 | AppDetailsBar(pkgName, batteryStatsReport)
330 | }
331 | }
332 | }
333 |
334 | @Composable
335 | private fun AppDetailsBar(pkgName: String, batteryStatsReport: BatteryStatsReport) {
336 | val appIcon = getAppIcon(pkgName)?.toBitmap()?.asImageBitmap()
337 | val appName = getAppName(pkgName)
338 | if (appIcon != null && appName != null) {
339 | Column(
340 | modifier = Modifier
341 | .padding(top = 10.dp)
342 | .fillMaxWidth()
343 | .background(
344 | color = MaterialTheme.colorScheme.surface,
345 | shape = RoundedCornerShape(10.dp)
346 | ),
347 | verticalArrangement = Arrangement.Center,
348 | horizontalAlignment = Alignment.CenterHorizontally
349 | ) {
350 | var showMoreDetails by remember { mutableStateOf(false) }
351 | TextButton(
352 | onClick = { showMoreDetails = !showMoreDetails },
353 | shape = RoundedCornerShape(10.dp),
354 | contentPadding = PaddingValues(0.dp),
355 | modifier = Modifier
356 | .height(60.dp)
357 | .fillMaxWidth()
358 | ) {
359 | Row(
360 | modifier = Modifier
361 | .padding(start = 20.dp)
362 | .fillMaxSize(),
363 | horizontalArrangement = Arrangement.Start,
364 | verticalAlignment = Alignment.CenterVertically
365 | ) {
366 | Image(
367 | bitmap = appIcon,
368 | contentDescription = null,
369 | modifier = Modifier
370 | .height(40.dp)
371 | .width(40.dp)
372 | )
373 | Column(
374 | modifier = Modifier
375 | .padding(start = 20.dp)
376 | .fillMaxHeight()
377 | .width(180.dp),
378 | verticalArrangement = Arrangement.Center,
379 | horizontalAlignment = Alignment.Start
380 | ) {
381 | Text(
382 | text = appName,
383 | fontSize = 14.sp,
384 | fontWeight = FontWeight.Bold,
385 | color = MaterialTheme.colorScheme.primary,
386 | maxLines = 1,
387 | overflow = TextOverflow.Ellipsis
388 | )
389 | Row(
390 | modifier = Modifier
391 | .fillMaxWidth()
392 | .height(20.dp),
393 | horizontalArrangement = Arrangement.Start,
394 | verticalAlignment = Alignment.CenterVertically
395 | ) {
396 | Text(
397 | text = "使用时间: ",
398 | fontSize = 14.sp,
399 | fontWeight = FontWeight.Normal,
400 | color = MaterialTheme.colorScheme.primary,
401 | maxLines = 1,
402 | overflow = TextOverflow.Ellipsis
403 | )
404 | Text(
405 | text = DurationToText(batteryStatsReport.duration),
406 | fontSize = 14.sp,
407 | fontWeight = FontWeight.Medium,
408 | color = cumulusColor().blue,
409 | maxLines = 1,
410 | overflow = TextOverflow.Ellipsis
411 | )
412 | }
413 | }
414 | Row(
415 | modifier = Modifier
416 | .padding(end = 20.dp)
417 | .fillMaxSize(),
418 | horizontalArrangement = Arrangement.End,
419 | verticalAlignment = Alignment.CenterVertically
420 | ) {
421 | val usedPercentage = (batteryStatsReport.averagePower.toDouble() *
422 | batteryStatsReport.duration) / (screenOnAveragePower * screenOnDuration) *
423 | screenOnUsedPercentage
424 | var usedPercentageText = "<1%"
425 | if (usedPercentage >= 1.0) {
426 | usedPercentageText = usedPercentage.toInt().toString() + "%"
427 | }
428 | Text(
429 | text = usedPercentageText,
430 | fontSize = 14.sp,
431 | fontWeight = FontWeight.Bold,
432 | color = cumulusColor().blue,
433 | maxLines = 1,
434 | overflow = TextOverflow.Ellipsis
435 | )
436 | }
437 | }
438 | }
439 | AnimatedVisibility(visible = showMoreDetails) {
440 | Column(
441 | modifier = Modifier
442 | .padding(start = 20.dp, top = 10.dp, end = 20.dp)
443 | .fillMaxWidth()
444 | .height(190.dp),
445 | verticalArrangement = Arrangement.Center,
446 | horizontalAlignment = Alignment.CenterHorizontally
447 | ) {
448 | MultiLineChart(
449 | modifier = Modifier
450 | .fillMaxWidth()
451 | .height(120.dp),
452 | dataPointList0 = batteryStatsReport.powerDataPoints,
453 | dataPointList1 = batteryStatsReport.temperatureDataPoints,
454 | line0Color = cumulusColor().blue,
455 | line1Color = cumulusColor().pink,
456 | line0Title = "功率(W)",
457 | line1Title = "温度(°C)"
458 | )
459 | Row(
460 | modifier = Modifier
461 | .padding(top = 10.dp)
462 | .fillMaxWidth()
463 | .height(20.dp),
464 | horizontalArrangement = Arrangement.Start,
465 | verticalAlignment = Alignment.CenterVertically
466 | ) {
467 | Text(
468 | text = "最高功耗: ",
469 | fontSize = 14.sp,
470 | fontWeight = FontWeight.Normal,
471 | color = MaterialTheme.colorScheme.primary,
472 | maxLines = 1,
473 | overflow = TextOverflow.Ellipsis
474 | )
475 | Text(
476 | text = batteryStatsReport.maxPower.toString(),
477 | fontSize = 14.sp,
478 | fontWeight = FontWeight.Medium,
479 | color = cumulusColor().blue,
480 | maxLines = 1,
481 | overflow = TextOverflow.Ellipsis
482 | )
483 | Text(
484 | text = "mW, 平均功耗: ",
485 | fontSize = 14.sp,
486 | fontWeight = FontWeight.Normal,
487 | color = MaterialTheme.colorScheme.primary,
488 | maxLines = 1,
489 | overflow = TextOverflow.Ellipsis
490 | )
491 | Text(
492 | text = batteryStatsReport.averagePower.toString(),
493 | fontSize = 14.sp,
494 | fontWeight = FontWeight.Medium,
495 | color = cumulusColor().blue,
496 | maxLines = 1,
497 | overflow = TextOverflow.Ellipsis
498 | )
499 | Text(
500 | text = "mW",
501 | fontSize = 14.sp,
502 | fontWeight = FontWeight.Normal,
503 | color = MaterialTheme.colorScheme.primary,
504 | maxLines = 1,
505 | overflow = TextOverflow.Ellipsis
506 | )
507 | }
508 | Row(
509 | modifier = Modifier
510 | .padding(top = 5.dp)
511 | .fillMaxWidth()
512 | .height(20.dp),
513 | horizontalArrangement = Arrangement.Start,
514 | verticalAlignment = Alignment.CenterVertically
515 | ) {
516 | Text(
517 | text = "最高温度: ",
518 | fontSize = 14.sp,
519 | fontWeight = FontWeight.Normal,
520 | color = MaterialTheme.colorScheme.primary,
521 | maxLines = 1,
522 | overflow = TextOverflow.Ellipsis
523 | )
524 | Text(
525 | text = batteryStatsReport.maxTemperature.toString(),
526 | fontSize = 14.sp,
527 | fontWeight = FontWeight.Medium,
528 | color = cumulusColor().blue,
529 | maxLines = 1,
530 | overflow = TextOverflow.Ellipsis
531 | )
532 | Text(
533 | text = "°C, 平均温度: ",
534 | fontSize = 14.sp,
535 | fontWeight = FontWeight.Normal,
536 | color = MaterialTheme.colorScheme.primary,
537 | maxLines = 1,
538 | overflow = TextOverflow.Ellipsis
539 | )
540 | Text(
541 | text = batteryStatsReport.averageTemperature.toString(),
542 | fontSize = 14.sp,
543 | fontWeight = FontWeight.Medium,
544 | color = cumulusColor().blue,
545 | maxLines = 1,
546 | overflow = TextOverflow.Ellipsis
547 | )
548 | Text(
549 | text = "°C",
550 | fontSize = 14.sp,
551 | fontWeight = FontWeight.Normal,
552 | color = MaterialTheme.colorScheme.primary,
553 | maxLines = 1,
554 | overflow = TextOverflow.Ellipsis
555 | )
556 | }
557 | }
558 | }
559 | }
560 | }
561 | }
562 |
563 | private fun updateRecordAnalysis() {
564 | CoroutineScope(Dispatchers.Default).launch {
565 | val records = BatteryStatsRecorder.getRecords()
566 | val recordAnalysis = BatteryStatsRecordAnalysis(records)
567 | screenOnUsedPercentage = recordAnalysis.getScreenOnUsedPercentage()
568 | screenOffUsedPercentage = recordAnalysis.getScreenOffUsedPercentage()
569 | screenOnAveragePower = recordAnalysis.getScreenOnAveragePower()
570 | screenOffAveragePower = recordAnalysis.getScreenOffAveragePower()
571 | screenOnDuration = recordAnalysis.getScreenOnDuration()
572 | screenOffDuration = recordAnalysis.getScreenOffDuration()
573 | perappBatteryStatsReport = recordAnalysis.getPerappBatteryStatsReport()
574 | }
575 | }
576 |
577 | private fun getAppName(pkgName: String): String? {
578 | try {
579 | val appInfo =
580 | packageManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
581 | return packageManager.getApplicationLabel(appInfo).toString()
582 | } catch (e: Exception) {
583 | e.printStackTrace()
584 | }
585 | return null
586 | }
587 |
588 | private fun getAppIcon(pkgName: String): Drawable? {
589 | try {
590 | val appInfo =
591 | packageManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
592 | return packageManager.getApplicationIcon(appInfo)
593 | } catch (e: Exception) {
594 | e.printStackTrace()
595 | }
596 | return null
597 | }
598 | }
--------------------------------------------------------------------------------