Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ Essential tools, mods and workarounds for Pixels and other Androids
<a href="https://github.com/sameerasw/essentials/issues/new?template=bug_report.md"><img alt="GitHub Issues or Pull Requests by label" src="https://img.shields.io/github/issues/sameerasw/essentials/bug?style=for-the-badge&logo=openbugbounty&logoColor=%23fff&label=bug%3F&labelColor=%232a6&color=%232a6">
</a>
<a href="https://github.com/sameerasw/essentials/issues/new?template=feature_request.md"><img alt="GitHub Issues or Pull Requests by label" src="https://img.shields.io/github/issues/sameerasw/essentials/enhancement?style=for-the-badge&logo=apachespark&logoColor=%23fff&label=Feature%20request&labelColor=%23a26&color=%23a26">
</a>
</a>
<a href="https://sameerasw.com"><img src="https://img.shields.io/badge/My%20website-orange?style=for-the-badge&logo=googlechrome&logoColor=%23000&labelColor=%233AFFB8&color=%233AFFB8" alt="My website" /></a>
<a href="https://t.me/tidwib"><img src="https://img.shields.io/badge/Community-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white" alt="Community" /></a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.sameerasw.essentials.domain.model.NotificationLightingSide
import com.sameerasw.essentials.domain.model.NotificationLightingStyle
import com.sameerasw.essentials.domain.model.NotificationLightingSweepPosition
import com.sameerasw.essentials.domain.model.ScaleAnimationsProfile
import com.sameerasw.essentials.domain.model.AppRefreshRateConfig
import com.sameerasw.essentials.domain.model.TrackedRepo
import com.sameerasw.essentials.domain.model.github.GitHubUser
import com.sameerasw.essentials.utils.RootUtils
Expand Down Expand Up @@ -238,6 +239,9 @@ class SettingsRepository(private val context: Context) {
const val KEY_SHUT_UP_ATTEMPT_SHIZUKU_RESTART = "shut_up_attempt_shizuku_restart"
const val KEY_DISABLE_ROTATION_SUGGESTION = "disable_rotation_suggestion"

const val KEY_PER_APP_REFRESH_RATE_ENABLED = "per_app_refresh_rate_enabled"
const val KEY_PER_APP_REFRESH_RATE_CONFIGS = "per_app_refresh_rate_configs"

const val KEY_LOCK_SCREEN_CLOCK_WEIGHT = "lock_screen_clock_weight"
const val KEY_LOCK_SCREEN_CLOCK_WIDTH = "lock_screen_clock_width"
const val KEY_LOCK_SCREEN_CLOCK_GRADE = "lock_screen_clock_grade"
Expand Down Expand Up @@ -552,6 +556,38 @@ class SettingsRepository(private val context: Context) {
}
}

fun loadPerAppRefreshRateConfigs(): List<AppRefreshRateConfig> {
val json = prefs.getString(KEY_PER_APP_REFRESH_RATE_CONFIGS, null)
return if (json != null) {
try {
gson.fromJson(
json,
Array<AppRefreshRateConfig>::class.java
).toList()
} catch (e: Exception) {
emptyList()
}
} else {
emptyList()
}
}

fun savePerAppRefreshRateConfigs(configs: List<AppRefreshRateConfig>) {
val json = gson.toJson(configs)
putString(KEY_PER_APP_REFRESH_RATE_CONFIGS, json)
}

fun updatePerAppRefreshRateConfig(config: AppRefreshRateConfig) {
val current = loadPerAppRefreshRateConfigs().toMutableList()
val index = current.indexOfFirst { it.packageName == config.packageName }
if (index != -1) {
current[index] = config
} else {
current.add(config)
}
savePerAppRefreshRateConfigs(current)
}

private fun updateAppSelection(key: String, packageName: String, enabled: Boolean) {
val current = loadAppSelection(key).toMutableList()
val index = current.indexOfFirst { it.packageName == packageName }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.sameerasw.essentials.domain.model

data class AppRefreshRateConfig(
val packageName: String,
val refreshRate: Float,
val isFixed: Boolean = false,
val isEnabled: Boolean = true
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.app.usage.UsageEvents
import android.app.usage.UsageStats
import android.app.usage.UsageStatsManager
import android.content.BroadcastReceiver
Expand Down Expand Up @@ -101,6 +102,26 @@ class AppDetectionService : Service() {
private fun getForegroundPackage(): String? {
val usageStatsManager = getSystemService(USAGE_STATS_SERVICE) as UsageStatsManager
val time = System.currentTimeMillis()

// 1. Try to find the last resumed activity using queryEvents (real-time & accurate)
try {
val events = usageStatsManager.queryEvents(time - 1000 * 15, time)
val event = UsageEvents.Event()
var lastResumedPackage: String? = null
while (events.hasNextEvent()) {
events.getNextEvent(event)
if (event.eventType == UsageEvents.Event.ACTIVITY_RESUMED) {
lastResumedPackage = event.packageName
}
}
if (lastResumedPackage != null) {
return lastResumedPackage
}
} catch (e: Exception) {
android.util.Log.e("AppDetectionService", "Failed to query usage events", e)
}

// 2. Fallback to queryUsageStats if no events found in the window
val stats = usageStatsManager.queryUsageStats(
UsageStatsManager.INTERVAL_DAILY,
time - 1000 * 10,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@ import android.os.Looper
import android.provider.Settings
import android.util.Log
import androidx.core.app.NotificationCompat
import android.view.inputmethod.InputMethodManager
import com.google.gson.Gson
import com.sameerasw.essentials.domain.diy.Automation
import com.sameerasw.essentials.domain.diy.DIYRepository
import com.sameerasw.essentials.domain.model.AppSelection
import com.sameerasw.essentials.domain.model.AppRefreshRateConfig
import com.sameerasw.essentials.data.repository.SettingsRepository
import com.sameerasw.essentials.services.automation.executors.CombinedActionExecutor
import com.sameerasw.essentials.utils.FreezeManager
import com.sameerasw.essentials.utils.StatusBarManager
import com.sameerasw.essentials.utils.RefreshRateUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
Expand Down Expand Up @@ -77,6 +81,12 @@ class AppFlowHandler(
var currentPackage: String? = null
private set

// Per-App Refresh Rate State
private var perAppRateSnapshot: RefreshRateUtils.RefreshRateState? = null
private var perAppCurrentPackage: String? = null
private var pendingRateRunnable: Runnable? = null
private var pendingRestoreRunnable: Runnable? = null

// App Automation State
private val activeAppAutomationIds = mutableSetOf<String>()

Expand All @@ -92,6 +102,17 @@ class AppFlowHandler(
"com.google.android.inputmethod.latin"
)

private fun isSystemOrIme(packageName: String): Boolean {
if (ignoredSystemPackages.contains(packageName)) return true
return try {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
val ims = imm?.enabledInputMethodList
ims?.any { it.packageName == packageName } == true
} catch (_: Exception) {
false
}
}

fun onPackageChanged(packageName: String, isFromUsageStats: Boolean = false) {
val prefs = context.getSharedPreferences("essentials_prefs", Context.MODE_PRIVATE)
val useUsageAccess = prefs.getBoolean("use_usage_access", false)
Expand All @@ -113,6 +134,7 @@ class AppFlowHandler(
checkAppAutomations(packageName)
checkGestureBarAutomation(packageName)
checkShutUpRestore(oldPackage, packageName)
checkPerAppRefreshRate(packageName)
}

}
Expand Down Expand Up @@ -203,8 +225,7 @@ class AppFlowHandler(

pendingNLRunnable?.let { handler.removeCallbacks(it) }

if (ignoredSystemPackages.contains(packageName)) {
Log.d("NightLight", "Ignoring system package $packageName")
if (ignoredSystemPackages.contains(packageName)) { Log.d("NightLight", "Ignoring system package $packageName")
return
}

Expand Down Expand Up @@ -677,6 +698,99 @@ class AppFlowHandler(
}
}

private fun checkPerAppRefreshRate(packageName: String) {
if (isSystemOrIme(packageName)) {
return
}

val settingsRepository = SettingsRepository(context)
val isEnabled = settingsRepository.getBoolean(SettingsRepository.KEY_PER_APP_REFRESH_RATE_ENABLED, false)
if (!isEnabled) {
cancelPendingRateRunnable()
cancelPendingRestoreRunnable()
if (perAppRateSnapshot != null) {
restoreFromSnapshot()
}
return
}

val configs = settingsRepository.loadPerAppRefreshRateConfigs()
val config = configs.find { it.packageName == packageName && it.isEnabled }

if (config != null) {
cancelPendingRestoreRunnable()
if (perAppRateSnapshot == null) {
perAppRateSnapshot = RefreshRateUtils.getCurrentState(context)
Log.d("AppFlowHandler", "per-app refresh rate: snapshotted state: $perAppRateSnapshot")
}
perAppCurrentPackage = packageName
Log.d("AppFlowHandler", "per-app refresh rate: applying ${config.refreshRate} Hz (isFixed=${config.isFixed}) for $packageName")
if (config.isFixed) {
RefreshRateUtils.applyFixedRefreshRate(context, config.refreshRate)
} else {
RefreshRateUtils.applyDynamicRefreshRate(context, config.refreshRate)
}

// Re-apply after a short delay to beat OEM adaptive display controllers that
// fire asynchronously after window transitions (e.g. resuming from recents).
cancelPendingRateRunnable()
val runnable = Runnable {
if (perAppCurrentPackage == packageName) {
Log.d("AppFlowHandler", "per-app refresh rate: delayed re-apply ${config.refreshRate} Hz (isFixed=${config.isFixed}) for $packageName")
if (config.isFixed) {
RefreshRateUtils.applyFixedRefreshRate(context, config.refreshRate)
} else {
RefreshRateUtils.applyDynamicRefreshRate(context, config.refreshRate)
}
}
}
pendingRateRunnable = runnable
handler.postDelayed(runnable, 400L)
} else {
cancelPendingRateRunnable()
perAppCurrentPackage = null
if (perAppRateSnapshot != null && pendingRestoreRunnable == null) {
Log.d("AppFlowHandler", "per-app refresh rate: scheduling delayed restoration (1000ms) for leaving $packageName")
val runnable = Runnable {
if (perAppCurrentPackage == null && perAppRateSnapshot != null) {
Log.d("AppFlowHandler", "per-app refresh rate: restoring to global state from snapshot (delayed)")
restoreFromSnapshot()
}
pendingRestoreRunnable = null
}
pendingRestoreRunnable = runnable
handler.postDelayed(runnable, 1000L)
}
}
}

private fun cancelPendingRateRunnable() {
pendingRateRunnable?.let { handler.removeCallbacks(it) }
pendingRateRunnable = null
}

private fun cancelPendingRestoreRunnable() {
pendingRestoreRunnable?.let { handler.removeCallbacks(it) }
pendingRestoreRunnable = null
}

private fun restoreFromSnapshot() {
val snapshot = perAppRateSnapshot ?: return
try {
if (snapshot.isSystemManaged) {
RefreshRateUtils.resetRefreshRate(context, snapshot.usesInfinityDefaultPeak)
} else if (snapshot.min > 0f && snapshot.peak > 0f && snapshot.min != snapshot.peak) {
RefreshRateUtils.applyRangeRefreshRate(context, snapshot.min, snapshot.peak)
} else {
RefreshRateUtils.applyFixedRefreshRate(context, snapshot.peak.coerceAtLeast(snapshot.min))
}
} catch (e: Exception) {
Log.e("AppFlowHandler", "Failed to restore refresh rate from snapshot", e)
} finally {
perAppRateSnapshot = null
}
}

private fun restartShizuku() {
try {
val intent = Intent("moe.shizuku.privileged.api.START").apply {
Expand Down
Loading