Compare commits

13 Commits

Author SHA1 Message Date
Acid 76a59638de Bump version to 1.3 (versionCode 5) 2026-06-06 19:41:00 -04:00
Acid e0e1a53128 changed colors for dark theme 2026-06-06 19:04:43 -04:00
Acid 1b5c060fd0 fixed crash on first run : createNotificationChannel() 2026-06-06 18:43:11 -04:00
Acid cae4ae53a4 modified: README.md 2026-04-18 02:39:57 -04:00
Acid cec33577be modified: fdroid/acidburn.stims.yml
modified:   gradle.properties
2026-03-28 02:40:57 -04:00
Acid a4aa7da072 modified: app/build.gradle
modified:   gradle/wrapper/gradle-wrapper.properties
2026-03-28 02:15:38 -04:00
Acid 44bd84e2c7 modified: README.md 2026-03-28 01:12:42 -04:00
Acid ae74cb9ee7 compileSdk 34 → 35, targetSdk 34 → 35, Jvm 17 -> 25 2026-03-28 01:09:54 -04:00
Acid 908fc99342 added images 2026-03-26 02:57:12 -04:00
Acid b1be1b5e43 modified: README.md 2026-03-25 22:12:45 -04:00
Acid 1ad37bd6b7 bump 1.1 2026-03-25 21:37:45 -04:00
Acid 3328708828 implemented overaly awake for Samsung and needed vendors , added
settings menu
2026-03-25 21:27:48 -04:00
Acid b804d94fff added check for battery optimization unrestricted 2026-03-24 18:50:40 -04:00
15 changed files with 423 additions and 71 deletions
+22 -12
View File
@@ -1,25 +1,35 @@
# stims
# About
An Android app that keeps your screen awake while specific apps are in the foreground.
Stims lets you select apps that should prevent your screen from turning off. When a "stimmed" app is active in the foreground, the screen stays on . When you switch to a different app, the screen resumes normal sleep behavior.
## What it does
### Why use over alternatives
Stims lets you select apps that should prevent your screen from turning off. When a "stimmed" app is active in the foreground, the screen stays on at full brightness. When you switch to a different app, the screen resumes normal sleep behavior.
Stims is designed to handle aggressive power managers like samsung's one UI , alternatives like caffeine won't work on these devices. If you encounter a problem please open a New Issue
---
## Permissions
Stims requires **Usage Access** permission to detect which app is in the foreground so it can keep the phone awake.
### Usage Access (required)
On first launch the app will redirect you to the system settings screen. To enable it:
Required to detect which app is in the foreground. On first launch the app will redirect you to the system settings screen.
1. Go to **Settings → Apps → Special app access → Usage access**
2. Find **Stims** in the list and toggle it on
2. Find **Stims** and toggle it on
3. Return to the app
Without this permission the background service cannot detect foreground apps and the screen will not be kept awake.
### Display over other apps (required on Samsung / OneUI)
Samsung's One UI disables the standard wake lock mechanism, so Stims uses a transparent overlay window instead to keep the screen on. The overlay is invisible and non-interactive.
1. Go to **Settings → Apps → Stims → Display over other apps**
2. Toggle it on
3. Return to the app — the warning banner will disappear
On stock Android and most other devices this permission is not needed and the app will work without it. It can also be enabled manually via the in-app settings for any device.
---
# Building from source
@@ -27,7 +37,7 @@ Without this permission the background service cannot detect foreground apps and
**Requirements**
- Android Studio or the Android SDK command-line tools
- JDK 8+
- JDK 11+
**Steps**
@@ -43,11 +53,11 @@ The APK will be output to:
app/build/outputs/apk/debug/app-debug.apk
```
For a release build:
For a release bundle (AAB):
```bash
./gradlew assembleRelease
# output: app/build/outputs/apk/release/app-release-unsigned.apk
./gradlew bundleRelease
# output: app/build/outputs/bundle/release/app-release.aab
```
---
@@ -55,4 +65,4 @@ For a release build:
## Compatibility
- **Minimum:** Android 7.0 (Nougat, API 24)
- **Target:** Android 14 (API 34)
- **Target:** Android 15 (API 35)
+23 -17
View File
@@ -5,14 +5,14 @@ plugins {
android {
namespace 'acidburn.stims'
compileSdk 34
compileSdk 35
defaultConfig {
applicationId "acidburn.stims"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
targetSdk 35
versionCode 5
versionName "1.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -28,17 +28,18 @@ android {
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = '11'
}
buildFeatures {
compose true
buildConfig true
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.8'
kotlinCompilerExtensionVersion '1.5.15'
}
packagingOptions {
resources {
@@ -48,24 +49,29 @@ android {
}
dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation 'androidx.activity:activity-compose:1.8.2'
implementation platform('androidx.compose:compose-bom:2023.10.01')
implementation 'androidx.core:core-ktx:1.15.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7'
implementation 'androidx.activity:activity-compose:1.10.1'
implementation platform('androidx.compose:compose-bom:2025.02.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.material:material-icons-extended'
// Added for XML theme support
implementation 'com.google.android.material:material:1.11.0'
implementation 'com.google.android.material:material:1.12.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation platform('androidx.compose:compose-bom:2023.10.01')
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
androidTestImplementation platform('androidx.compose:compose-bom:2025.02.00')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
}
// Alias for CI/CD systems or scripts that expect the standard Java 'testClasses' task
tasks.register("testClasses") {
// This maps to compiling the debug unit tests
dependsOn "compileDebugUnitTestSources"
}
+1
View File
@@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application
android:allowBackup="true"
+257 -26
View File
@@ -6,6 +6,7 @@ import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Process
@@ -14,13 +15,26 @@ import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowForward
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -44,18 +58,30 @@ data class AppInfo(
class MainActivity : ComponentActivity() {
private lateinit var prefs: SharedPreferences
private var showOverlayWarning by mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
prefs = getSharedPreferences("stims_prefs", Context.MODE_PRIVATE)
setContent {
StimsTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AppListScreen(prefs)
AppListScreen(
prefs = prefs,
showOverlayWarning = showOverlayWarning,
onOpenOverlaySettings = {
startActivity(
Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.fromParts("package", packageName, null)
)
)
}
)
}
}
}
@@ -67,6 +93,10 @@ class MainActivity : ComponentActivity() {
Toast.makeText(this, "Please enable Usage Stats permission", Toast.LENGTH_LONG).show()
startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS))
}
val needsOverlay = StimsService.OVERLAY_VENDORS.any {
Build.MANUFACTURER.equals(it, ignoreCase = true)
}
showOverlayWarning = needsOverlay && !Settings.canDrawOverlays(this)
}
private fun hasUsageStatsPermission(context: Context): Boolean {
@@ -83,13 +113,17 @@ class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppListScreen(prefs: SharedPreferences) {
fun AppListScreen(
prefs: SharedPreferences,
showOverlayWarning: Boolean,
onOpenOverlaySettings: () -> Unit,
) {
val context = LocalContext.current
val packageManager = context.packageManager
var allApps by remember { mutableStateOf<List<AppInfo>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
val stimmedPackages = remember {
mutableStateListOf<String>().apply {
addAll(prefs.getStringSet("stimmed_apps", emptySet()) ?: emptySet())
@@ -98,13 +132,16 @@ fun AppListScreen(prefs: SharedPreferences) {
val tempSelected = remember { mutableStateListOf<String>() }
LaunchedEffect(stimmedPackages.toList()) {
var forceOverlay by remember { mutableStateOf(prefs.getBoolean("force_overlay", false)) }
LaunchedEffect(stimmedPackages.toList(), forceOverlay) {
prefs.edit().putStringSet("stimmed_apps", stimmedPackages.toSet()).apply()
val intent = Intent(context, StimsService::class.java).apply {
putStringArrayListExtra("selected_packages", ArrayList(stimmedPackages))
putExtra(StimsService.EXTRA_FORCE_OVERLAY, forceOverlay)
}
if (stimmedPackages.isEmpty()) {
context.stopService(intent)
} else {
@@ -122,7 +159,7 @@ fun AppListScreen(prefs: SharedPreferences) {
addCategory(Intent.CATEGORY_LAUNCHER)
}
val resolveInfos = packageManager.queryIntentActivities(intent, 0)
resolveInfos.map { resolveInfo ->
AppInfo(
name = resolveInfo.loadLabel(packageManager).toString(),
@@ -136,21 +173,42 @@ fun AppListScreen(prefs: SharedPreferences) {
}
var searchQuery by remember { mutableStateOf("") }
var showSettings by remember { mutableStateOf(false) }
val stimList = remember(stimmedPackages.toList(), allApps) {
allApps.filter { it.packageName in stimmedPackages }
}
val availableApps = remember(searchQuery, stimmedPackages.toList(), allApps) {
allApps.filter { it.packageName !in stimmedPackages &&
(searchQuery.isBlank() || it.name.contains(searchQuery, ignoreCase = true) || it.packageName.contains(searchQuery, ignoreCase = true))
allApps.filter {
it.packageName !in stimmedPackages &&
(searchQuery.isBlank() || it.name.contains(searchQuery, ignoreCase = true) ||
it.packageName.contains(searchQuery, ignoreCase = true))
}
}
if (showSettings) {
SettingsScreen(
forceOverlay = forceOverlay,
onForceOverlayChange = { checked ->
forceOverlay = checked
prefs.edit().putBoolean("force_overlay", checked).apply()
},
onOpenOverlaySettings = onOpenOverlaySettings,
onBack = { showSettings = false }
)
return
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Stims Manager", fontWeight = FontWeight.Bold) }
title = { Text("Stims Manager", fontWeight = FontWeight.Bold) },
actions = {
IconButton(onClick = { showSettings = true }) {
Icon(Icons.Default.Settings, contentDescription = "Settings")
}
}
)
}
) { padding ->
@@ -160,6 +218,9 @@ fun AppListScreen(prefs: SharedPreferences) {
}
} else {
Column(modifier = Modifier.padding(padding)) {
if (showOverlayWarning) {
OverlayWarningBanner(onClick = onOpenOverlaySettings)
}
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
@@ -173,9 +234,7 @@ fun AppListScreen(prefs: SharedPreferences) {
LazyColumn(modifier = Modifier.weight(1f)) {
if (stimList.isNotEmpty()) {
item {
SectionHeader("STAY AWAKE (STIMMED)")
}
item { SectionHeader("STAY AWAKE (STIMMED)") }
items(stimList) { app ->
StimmedAppItem(app = app) {
stimmedPackages.remove(app.packageName)
@@ -183,9 +242,7 @@ fun AppListScreen(prefs: SharedPreferences) {
}
}
item {
SectionHeader("ALL APPS")
}
item { SectionHeader("ALL APPS") }
items(availableApps) { app ->
SelectableAppItem(
app = app,
@@ -197,7 +254,7 @@ fun AppListScreen(prefs: SharedPreferences) {
)
}
}
Button(
onClick = {
stimmedPackages.addAll(tempSelected)
@@ -216,6 +273,134 @@ fun AppListScreen(prefs: SharedPreferences) {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
forceOverlay: Boolean,
onForceOverlayChange: (Boolean) -> Unit,
onOpenOverlaySettings: () -> Unit,
onBack: () -> Unit,
) {
val context = LocalContext.current
val canDrawOverlays = Settings.canDrawOverlays(context)
val isVendorDevice = StimsService.OVERLAY_VENDORS.any {
Build.MANUFACTURER.equals(it, ignoreCase = true)
}
val effectiveOverlay = forceOverlay || isVendorDevice
val overlayActive = effectiveOverlay && canDrawOverlays
Scaffold(
topBar = {
TopAppBar(
title = { Text("Settings") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
}
)
}
) { padding ->
Column(modifier = Modifier.padding(padding)) {
SettingsSectionHeader("DISPLAY")
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text("Force awake overlay", style = MaterialTheme.typography.bodyLarge)
Text(
text = when {
overlayActive && isVendorDevice -> "Active — required on this device"
overlayActive -> "Active — screen kept on via overlay"
effectiveOverlay -> "Enabled — overlay permission required"
else -> "Inactive"
},
style = MaterialTheme.typography.bodySmall,
color = when {
overlayActive -> Color(0xFF2E7D32)
effectiveOverlay -> MaterialTheme.colorScheme.error
else -> MaterialTheme.colorScheme.onSurfaceVariant
}
)
}
Switch(
checked = effectiveOverlay,
enabled = !isVendorDevice,
onCheckedChange = { checked ->
onForceOverlayChange(checked)
if (checked && !canDrawOverlays) {
onOpenOverlaySettings()
}
}
)
}
HorizontalDivider()
SettingsSectionHeader("ABOUT")
SettingsInfoRow(label = "Version", value = BuildConfig.VERSION_NAME)
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
SettingsInfoRow(label = "Author", value = "acidburnmonkey")
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
context.startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/acidburnmonkey/stims"))
)
}
.padding(horizontal = 16.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "GitHub",
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.weight(1f)
)
Icon(
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = "Open GitHub",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(18.dp)
)
}
}
}
}
@Composable
fun SettingsSectionHeader(title: String) {
Text(
text = title,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(start = 16.dp, top = 20.dp, bottom = 4.dp)
)
}
@Composable
fun SettingsInfoRow(label: String, value: String) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(label, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
Text(value, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
@Composable
fun SectionHeader(title: String) {
Text(
@@ -231,13 +416,16 @@ fun SectionHeader(title: String) {
@Composable
fun StimmedAppItem(app: AppInfo, onRemove: () -> Unit) {
val dark = isSystemInDarkTheme()
val cardColor = if (dark) Color(0xFF1B3A2B) else Color(0xFFE8F5E9)
val nameColor = if (dark) Color(0xFFE8F5E9) else Color(0xFF1B5E20)
val stimmedColor = if (dark) Color(0xFFFFB74D) else Color(0xFFEF6C00)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 6.dp),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFE8F5E9)
),
colors = CardDefaults.cardColors(containerColor = cardColor),
elevation = CardDefaults.cardElevation(4.dp)
) {
Row(
@@ -247,11 +435,11 @@ fun StimmedAppItem(app: AppInfo, onRemove: () -> Unit) {
AppIcon(app.icon)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(text = app.name, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold)
Text(text = "STIMMED", style = MaterialTheme.typography.labelSmall, color = Color(0xFF2E7D32))
Text(text = app.name, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold, color = nameColor)
Text(text = "STIMMED", style = MaterialTheme.typography.labelSmall, color = stimmedColor)
}
IconButton(onClick = onRemove) {
Icon(Icons.Default.Delete, contentDescription = "Remove", tint = Color.Gray)
Icon(Icons.Default.Delete, contentDescription = "Remove", tint = if (dark) Color(0xFFB0BEC5) else Color.Gray)
}
}
}
@@ -295,3 +483,46 @@ fun AppIcon(icon: Drawable?) {
) {}
}
}
@Composable
fun OverlayWarningBanner(onClick: () -> Unit) {
val warningColor = Color(0xFFEA580C)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 6.dp)
.border(width = 1.5.dp, color = warningColor, shape = MaterialTheme.shapes.small)
.clickable { onClick() }
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Visibility,
contentDescription = null,
tint = warningColor,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(10.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Overlay permission required (Samsung)",
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
color = Color(0xFF1E40AF)
)
Text(
text = "Tap → Allow display over other apps",
style = MaterialTheme.typography.bodySmall,
color = warningColor
)
}
Spacer(modifier = Modifier.width(6.dp))
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = null,
tint = warningColor,
modifier = Modifier.size(16.dp)
)
}
}
@@ -5,19 +5,34 @@ import android.app.usage.UsageEvents
import android.app.usage.UsageStatsManager
import android.content.Context
import android.content.Intent
import android.graphics.PixelFormat
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.PowerManager
import android.provider.Settings
import android.util.Log
import android.view.View
import android.view.WindowManager
import androidx.core.app.NotificationCompat
class StimsService : Service() {
// Samsung and certain OEMs disable SCREEN_BRIGHT_WAKE_LOCK; use a 1x1 overlay with
// FLAG_KEEP_SCREEN_ON instead, which the WindowManager honours on all devices.
private var useOverlayStrategy = OVERLAY_VENDORS.any {
Build.MANUFACTURER.equals(it, ignoreCase = true)
}
private var wakeLock: PowerManager.WakeLock? = null
private var overlayView: View? = null
private val windowManager by lazy { getSystemService(WINDOW_SERVICE) as WindowManager }
private var selectedPackages = mutableSetOf<String>()
private val handler = Handler(Looper.getMainLooper())
private val checkInterval = 10000L // Check every 10 seconds
private var lastForegroundPackage: String? = null
private val monitorRunnable = object : Runnable {
override fun run() {
@@ -34,17 +49,24 @@ class StimsService : Service() {
if (packages != null) {
selectedPackages = packages.toMutableSet()
if (BuildConfig.DEBUG) Log.d(TAG, "Updated stimmed packages: $selectedPackages")
updateNotification()
}
val forceOverlay = intent?.getBooleanExtra(EXTRA_FORCE_OVERLAY, false) ?: false
useOverlayStrategy = forceOverlay || OVERLAY_VENDORS.any {
Build.MANUFACTURER.equals(it, ignoreCase = true)
}
if (BuildConfig.DEBUG) Log.d(TAG, "useOverlayStrategy=$useOverlayStrategy forceOverlay=$forceOverlay")
if (action == ACTION_STOP) {
if (BuildConfig.DEBUG) Log.d(TAG, "Stop action received")
stopSelf()
return START_NOT_STICKY
}
if (BuildConfig.DEBUG) Log.d(TAG, "Service starting")
startForegroundService()
handler.removeCallbacks(monitorRunnable)
handler.post(monitorRunnable)
return START_STICKY
}
@@ -52,10 +74,13 @@ class StimsService : Service() {
private fun startForegroundService() {
createNotificationChannel()
updateNotification()
handler.removeCallbacks(monitorRunnable)
handler.post(monitorRunnable)
}
private fun updateNotification() {
// Ensure the channel exists before startForeground(); onStartCommand may call
createNotificationChannel()
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Stims Daemon Running")
.setContentText("Monitoring ${selectedPackages.size} apps")
@@ -68,30 +93,41 @@ class StimsService : Service() {
private fun checkForegroundApp() {
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
if (!powerManager.isInteractive) {
releaseWakeLock()
if (BuildConfig.DEBUG) Log.d(TAG, "Screen not interactive, releasing screen lock")
releaseScreenLock()
return
}
val usageStatsManager = getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
val time = System.currentTimeMillis()
val events = usageStatsManager.queryEvents(time - 15000, time)
val events = usageStatsManager.queryEvents(time - checkInterval * 2, time)
val event = UsageEvents.Event()
var currentPackage: String? = null
while (events.hasNextEvent()) {
events.getNextEvent(event)
if (event.eventType == UsageEvents.Event.MOVE_TO_FOREGROUND) {
currentPackage = event.packageName
if (event.eventType == UsageEvents.Event.ACTIVITY_RESUMED) {
lastForegroundPackage = event.packageName
}
}
if (currentPackage != null && selectedPackages.contains(currentPackage)) {
acquireWakeLock()
} else if (currentPackage != null) {
releaseWakeLock()
val shouldHold = lastForegroundPackage != null && selectedPackages.contains(lastForegroundPackage)
if (BuildConfig.DEBUG) Log.d(TAG, "Poll: foreground=$lastForegroundPackage shouldHold=$shouldHold wakeLockHeld=${wakeLock?.isHeld}")
if (shouldHold) {
acquireScreenLock()
} else {
releaseScreenLock()
}
}
private fun acquireScreenLock() {
if (useOverlayStrategy) acquireOverlay() else acquireWakeLock()
}
private fun releaseScreenLock() {
if (useOverlayStrategy) releaseOverlay() else releaseWakeLock()
}
private fun acquireWakeLock() {
if (wakeLock == null || !wakeLock!!.isHeld) {
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
@@ -100,17 +136,47 @@ class StimsService : Service() {
PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
"Stims::WakeLock"
)
wakeLock?.acquire(10 * 60 * 1000L) // 10 min timeout
@Suppress("WakelockTimeout")
wakeLock?.acquire()
if (BuildConfig.DEBUG) Log.d(TAG, "Wake lock acquired for $lastForegroundPackage")
}
}
private fun releaseWakeLock() {
if (wakeLock?.isHeld == true) {
wakeLock?.release()
if (BuildConfig.DEBUG) Log.d(TAG, "Wake lock released")
}
wakeLock = null
}
private fun acquireOverlay() {
if (overlayView != null) return
if (!Settings.canDrawOverlays(this)) {
if (BuildConfig.DEBUG) Log.d(TAG, "Overlay permission not granted, cannot acquire overlay")
return
}
val params = WindowManager.LayoutParams(
1, 1,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
PixelFormat.TRANSLUCENT
)
overlayView = View(this)
windowManager.addView(overlayView, params)
if (BuildConfig.DEBUG) Log.d(TAG, "Overlay acquired for $lastForegroundPackage")
}
private fun releaseOverlay() {
overlayView?.let {
windowManager.removeView(it)
overlayView = null
if (BuildConfig.DEBUG) Log.d(TAG, "Overlay released")
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val manager = getSystemService(NotificationManager::class.java)
@@ -126,12 +192,17 @@ class StimsService : Service() {
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacks(monitorRunnable)
releaseWakeLock()
releaseScreenLock()
}
companion object {
const val CHANNEL_ID = "StimsServiceChannel"
const val NOTIFICATION_ID = 1
const val ACTION_STOP = "acidburn.stims.STOP"
const val EXTRA_FORCE_OVERLAY = "force_overlay"
private const val TAG = "StimsService"
// Vendors that disable SCREEN_BRIGHT_WAKE_LOCK — use overlay strategy instead
val OVERLAY_VENDORS = setOf("samsung","Realme")
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

+2 -2
View File
@@ -5,8 +5,8 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22"
classpath 'com.android.tools.build:gradle:8.7.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 889 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

+31
View File
@@ -0,0 +1,31 @@
Categories:
- System
License: GPL-3.0-only
AuthorName: acidburnmonkey
AuthorEmail: developercontact.gnarly348@slmail.me
WebSite: https://acidburnmonkey.github.io/stims/
SourceCode: https://github.com/acidburnmonkey/stims
IssueTracker: https://github.com/acidburnmonkey/stims/issues
AutoName: Stims
RepoType: git
Repo: https://github.com/acidburnmonkey/stims
Builds:
- versionName: '1.2'
versionCode: 4
commit: TO_BE_TAGGED
subdir: app
sudo:
- echo "deb https://deb.debian.org/debian trixie main" > /etc/apt/sources.list.d/trixie.list
- apt-get update
- apt-get install -y -t trixie openjdk-21-jdk-headless
- update-alternatives --auto java
gradle:
- yes
AutoUpdateMode: Version
UpdateCheckMode: Tags
CurrentVersion: '1.2'
CurrentVersionCode: 4
+1
View File
@@ -1,3 +1,4 @@
android.useAndroidX=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.configuration-cache=false
+2 -1
View File
@@ -1,6 +1,7 @@
#Sat Mar 28 00:18:44 EDT 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists