mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 17:59:51 +02:00
Add new experimental app detection method
This commit is contained in:
parent
d38724c42a
commit
1a0e56ef37
18 changed files with 519 additions and 43 deletions
|
@ -243,4 +243,5 @@ object ExperimentalFlags {
|
||||||
const val HIDE_MANIPULATION_WARNING = 8192L
|
const val HIDE_MANIPULATION_WARNING = 8192L
|
||||||
const val ENABLE_SOFT_BLOCKING = 16384L
|
const val ENABLE_SOFT_BLOCKING = 16384L
|
||||||
const val SYNC_RELATED_NOTIFICATIONS = 32768L
|
const val SYNC_RELATED_NOTIFICATIONS = 32768L
|
||||||
|
const val INSTANCE_ID_FG_APP_DETECTION = 65536L
|
||||||
}
|
}
|
|
@ -49,7 +49,7 @@ abstract class PlatformIntegration(
|
||||||
abstract suspend fun muteAudioIfPossible(packageName: String): Boolean
|
abstract suspend fun muteAudioIfPossible(packageName: String): Boolean
|
||||||
abstract fun setShowBlockingOverlay(show: Boolean, blockedElement: String? = null)
|
abstract fun setShowBlockingOverlay(show: Boolean, blockedElement: String? = null)
|
||||||
// this should throw an SecurityException if the permission is missing
|
// this should throw an SecurityException if the permission is missing
|
||||||
abstract suspend fun getForegroundApps(queryInterval: Long, enableMultiAppDetection: Boolean): Set<ForegroundApp>
|
abstract suspend fun getForegroundApps(queryInterval: Long, experimentalFlags: Long): Set<ForegroundApp>
|
||||||
abstract fun getMusicPlaybackPackage(): String?
|
abstract fun getMusicPlaybackPackage(): String?
|
||||||
abstract fun setAppStatusMessage(message: AppStatusMessage?)
|
abstract fun setAppStatusMessage(message: AppStatusMessage?)
|
||||||
abstract fun isScreenOn(): Boolean
|
abstract fun isScreenOn(): Boolean
|
||||||
|
|
|
@ -150,7 +150,7 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
||||||
return AdminStatus.getAdminStatus(context, policyManager)
|
return AdminStatus.getAdminStatus(context, policyManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getForegroundApps(queryInterval: Long, enableMultiAppDetection: Boolean): Set<ForegroundApp> = foregroundAppHelper.getForegroundApps(queryInterval, enableMultiAppDetection)
|
override suspend fun getForegroundApps(queryInterval: Long, experimentalFlags: Long): Set<ForegroundApp> = foregroundAppHelper.getForegroundApps(queryInterval, experimentalFlags)
|
||||||
|
|
||||||
override fun getForegroundAppPermissionStatus(): RuntimePermissionStatus {
|
override fun getForegroundAppPermissionStatus(): RuntimePermissionStatus {
|
||||||
return foregroundAppHelper.getPermissionStatus()
|
return foregroundAppHelper.getPermissionStatus()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -29,7 +29,7 @@ class CompatForegroundAppHelper(context: Context) : ForegroundAppHelper() {
|
||||||
private var lastForegroundAppList: Set<ForegroundApp> = emptySet()
|
private var lastForegroundAppList: Set<ForegroundApp> = emptySet()
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
override suspend fun getForegroundApps(queryInterval: Long, enableMultiAppDetection: Boolean): Set<ForegroundApp> {
|
override suspend fun getForegroundApps(queryInterval: Long, experimentalFlags: Long): Set<ForegroundApp> {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
try {
|
try {
|
||||||
val activity = activityManager.getRunningTasks(1)[0].topActivity!!
|
val activity = activityManager.getRunningTasks(1)[0].topActivity!!
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -21,7 +21,7 @@ import io.timelimit.android.integration.platform.ForegroundApp
|
||||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
|
|
||||||
abstract class ForegroundAppHelper {
|
abstract class ForegroundAppHelper {
|
||||||
abstract suspend fun getForegroundApps(queryInterval: Long, enableMultiAppDetection: Boolean): Set<ForegroundApp>
|
abstract suspend fun getForegroundApps(queryInterval: Long, experimentalFlags: Long): Set<ForegroundApp>
|
||||||
abstract fun getPermissionStatus(): RuntimePermissionStatus
|
abstract fun getPermissionStatus(): RuntimePermissionStatus
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -32,7 +32,9 @@ abstract class ForegroundAppHelper {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
instance = QForegroundAppHelper(context.applicationContext)
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
instance = LollipopForegroundAppHelper(context.applicationContext)
|
instance = LollipopForegroundAppHelper(context.applicationContext)
|
||||||
} else {
|
} else {
|
||||||
instance = CompatForegroundAppHelper(context.applicationContext)
|
instance = CompatForegroundAppHelper(context.applicationContext)
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android.foregroundapp
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.SparseArray
|
||||||
|
import androidx.core.util.size
|
||||||
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
|
import io.timelimit.android.integration.platform.ForegroundApp
|
||||||
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.Q)
|
||||||
|
class InstanceIdForegroundAppHelper(context: Context): UsageStatsForegroundAppHelper(context) {
|
||||||
|
companion object {
|
||||||
|
private const val START_QUERY_INTERVAL = 1000 * 60 * 60 * 24 * 3 // 3 days
|
||||||
|
private const val TOLERANCE = 3000L
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lastQueryTime = 0L
|
||||||
|
private var lastEventTimestamp = 0L
|
||||||
|
private val apps = SparseArray<ForegroundApp>()
|
||||||
|
|
||||||
|
override suspend fun getForegroundApps(
|
||||||
|
queryInterval: Long,
|
||||||
|
experimentalFlags: Long
|
||||||
|
): Set<ForegroundApp> {
|
||||||
|
if (Build.VERSION.SDK_INT > 32) {
|
||||||
|
throw UntestedSystemVersionException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getPermissionStatus() != RuntimePermissionStatus.Granted) {
|
||||||
|
throw SecurityException()
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = backgroundThread.executeAndWait {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
|
||||||
|
val didTimeWentBackwards = lastQueryTime > now
|
||||||
|
val didNeverQuery = lastQueryTime == 0L
|
||||||
|
val shouldDoFullQuery = didTimeWentBackwards || didNeverQuery
|
||||||
|
|
||||||
|
if (shouldDoFullQuery) {
|
||||||
|
apps.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
val minQueryStartTime = (now - START_QUERY_INTERVAL).coerceAtLeast(1)
|
||||||
|
val queryStartTimeByLastEvent = lastEventTimestamp - TOLERANCE
|
||||||
|
|
||||||
|
val queryStartTime = if (shouldDoFullQuery) {
|
||||||
|
minQueryStartTime
|
||||||
|
} else {
|
||||||
|
queryStartTimeByLastEvent
|
||||||
|
.coerceAtLeast(minQueryStartTime)
|
||||||
|
.coerceAtMost(now - TOLERANCE)
|
||||||
|
}
|
||||||
|
|
||||||
|
val queryEndTime = now + TOLERANCE
|
||||||
|
|
||||||
|
usageStatsManager.queryEvents(queryStartTime, queryEndTime)?.let { nativeEvents ->
|
||||||
|
val events = TlUsageEvents.fromUsageEvents(nativeEvents)
|
||||||
|
|
||||||
|
while (events.readNextItem()) {
|
||||||
|
lastEventTimestamp = events.timestamp
|
||||||
|
|
||||||
|
if (events.eventType == TlUsageEvents.DEVICE_STARTUP) {
|
||||||
|
apps.clear()
|
||||||
|
} else if (events.eventType == TlUsageEvents.MOVE_TO_FOREGROUND) {
|
||||||
|
val app = ForegroundApp(events.packageName, events.className)
|
||||||
|
|
||||||
|
apps.put(events.instanceId, app)
|
||||||
|
} else if (
|
||||||
|
events.eventType == TlUsageEvents.MOVE_TO_BACKGROUND ||
|
||||||
|
events.eventType == TlUsageEvents.ACTIVITY_STOPPED
|
||||||
|
) {
|
||||||
|
apps.remove(events.instanceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastQueryTime = now
|
||||||
|
|
||||||
|
val appsSet = mutableSetOf<ForegroundApp>()
|
||||||
|
|
||||||
|
for (index in 0 until apps.size) {
|
||||||
|
appsSet.add(apps.valueAt(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
appsSet
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
class UntestedSystemVersionException: RuntimeException()
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,9 +16,7 @@
|
||||||
package io.timelimit.android.integration.platform.android.foregroundapp
|
package io.timelimit.android.integration.platform.android.foregroundapp
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.app.AppOpsManager
|
|
||||||
import android.app.usage.UsageEvents
|
import android.app.usage.UsageEvents
|
||||||
import android.app.usage.UsageStatsManager
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
@ -27,13 +25,14 @@ import android.util.Log
|
||||||
import android.util.SparseIntArray
|
import android.util.SparseIntArray
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.coroutines.executeAndWait
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
|
import io.timelimit.android.data.model.ExperimentalFlags
|
||||||
import io.timelimit.android.integration.platform.ForegroundApp
|
import io.timelimit.android.integration.platform.ForegroundApp
|
||||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppHelper() {
|
class LollipopForegroundAppHelper(context: Context) : UsageStatsForegroundAppHelper(context) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val LOG_TAG = "LollipopForegroundApp"
|
private const val LOG_TAG = "LollipopForegroundApp"
|
||||||
|
|
||||||
|
@ -51,10 +50,6 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val usageStatsManager = context.getSystemService(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) Context.USAGE_STATS_SERVICE else "usagestats") as UsageStatsManager
|
|
||||||
private val appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
|
|
||||||
private val packageManager = context.packageManager
|
|
||||||
|
|
||||||
private var lastQueryTime: Long = 0
|
private var lastQueryTime: Long = 0
|
||||||
private val currentForegroundApps = mutableMapOf<ForegroundApp, Int>()
|
private val currentForegroundApps = mutableMapOf<ForegroundApp, Int>()
|
||||||
private val expectedStopEvents = mutableSetOf<ForegroundApp>()
|
private val expectedStopEvents = mutableSetOf<ForegroundApp>()
|
||||||
|
@ -66,11 +61,12 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
|
||||||
private var lastEnableMultiAppDetection = false
|
private var lastEnableMultiAppDetection = false
|
||||||
|
|
||||||
@Throws(SecurityException::class)
|
@Throws(SecurityException::class)
|
||||||
override suspend fun getForegroundApps(queryInterval: Long, enableMultiAppDetection: Boolean): Set<ForegroundApp> {
|
override suspend fun getForegroundApps(queryInterval: Long, experimentalFlags: Long): Set<ForegroundApp> {
|
||||||
if (getPermissionStatus() == RuntimePermissionStatus.NotGranted) {
|
if (getPermissionStatus() == RuntimePermissionStatus.NotGranted) {
|
||||||
throw SecurityException()
|
throw SecurityException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val enableMultiAppDetection = experimentalFlags and ExperimentalFlags.MULTI_APP_DETECTION == ExperimentalFlags.MULTI_APP_DETECTION
|
||||||
val effectiveEnableMultiAppDetection = enableMultiAppDetection && enableMultiAppDetectionGeneral
|
val effectiveEnableMultiAppDetection = enableMultiAppDetection && enableMultiAppDetectionGeneral
|
||||||
|
|
||||||
foregroundAppThread.executeAndWait {
|
foregroundAppThread.executeAndWait {
|
||||||
|
@ -249,20 +245,6 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
|
||||||
return currentForegroundAppsSnapshot
|
return currentForegroundAppsSnapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPermissionStatus(): RuntimePermissionStatus {
|
|
||||||
val appOpsStatus = appOpsManager.checkOpNoThrow("android:get_usage_stats", android.os.Process.myUid(), context.packageName)
|
|
||||||
val packageManagerStatus = packageManager.checkPermission("android.permission.PACKAGE_USAGE_STATS", BuildConfig.APPLICATION_ID)
|
|
||||||
|
|
||||||
val allowedUsingSystemSettings = appOpsStatus == AppOpsManager.MODE_ALLOWED
|
|
||||||
val allowedUsingAdb = appOpsStatus == AppOpsManager.MODE_DEFAULT && packageManagerStatus == PackageManager.PERMISSION_GRANTED
|
|
||||||
|
|
||||||
if(allowedUsingSystemSettings || allowedUsingAdb) {
|
|
||||||
return RuntimePermissionStatus.Granted
|
|
||||||
} else {
|
|
||||||
return RuntimePermissionStatus.NotGranted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Android 9 (and maybe older versions too) do not report pausing Apps if they are disabled while running
|
// Android 9 (and maybe older versions too) do not report pausing Apps if they are disabled while running
|
||||||
private fun doesActivityExist(app: ForegroundApp) = doesActivityExistSimple(app) || doesActivityExistAsAlias(app)
|
private fun doesActivityExist(app: ForegroundApp) = doesActivityExistSimple(app) || doesActivityExistAsAlias(app)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android.foregroundapp
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import java.io.FileInputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class implements a clone of Parcel.readBlob() as this is not a public
|
||||||
|
* interface in all Android versions that support it.
|
||||||
|
*
|
||||||
|
* The wire format is (both as integer): length, useSharedMemory
|
||||||
|
*
|
||||||
|
* useSharedMemory can be 0 or 1
|
||||||
|
*/
|
||||||
|
object ParcelBlob {
|
||||||
|
fun readBlob(parcel: Parcel): ByteArray {
|
||||||
|
val length = parcel.readInt()
|
||||||
|
|
||||||
|
val useSharedMemory = when (parcel.readInt()) {
|
||||||
|
0 -> false
|
||||||
|
1 -> true
|
||||||
|
else -> throw InvalidBooleanException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useSharedMemory) {
|
||||||
|
val fd = parcel.readFileDescriptor()
|
||||||
|
val result = ByteArray(length)
|
||||||
|
|
||||||
|
try {
|
||||||
|
FileInputStream(fd.fileDescriptor).use { stream ->
|
||||||
|
var cursor = 0
|
||||||
|
|
||||||
|
while (cursor < length) {
|
||||||
|
val bytesRead = stream.read(result, cursor, length - cursor)
|
||||||
|
|
||||||
|
if (bytesRead == -1) {
|
||||||
|
throw UnexpectedEndOfSharedMemory()
|
||||||
|
} else {
|
||||||
|
cursor += bytesRead
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
fd.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
val tempParcel = Parcel.obtain()
|
||||||
|
val oldPosition = parcel.dataPosition()
|
||||||
|
|
||||||
|
try {
|
||||||
|
tempParcel.appendFrom(parcel, oldPosition, length)
|
||||||
|
|
||||||
|
parcel.setDataPosition(oldPosition + length)
|
||||||
|
|
||||||
|
val result = tempParcel.marshall()
|
||||||
|
|
||||||
|
if (result.size != length) {
|
||||||
|
throw WrongReturnedDataSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
} finally {
|
||||||
|
tempParcel.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class ParcelBlobException: RuntimeException()
|
||||||
|
class InvalidBooleanException: ParcelBlobException()
|
||||||
|
class UnexpectedEndOfSharedMemory: ParcelBlobException()
|
||||||
|
class WrongReturnedDataSize: ParcelBlobException()
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android.foregroundapp
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import io.timelimit.android.BuildConfig
|
||||||
|
import io.timelimit.android.data.model.ExperimentalFlags
|
||||||
|
import io.timelimit.android.integration.platform.ForegroundApp
|
||||||
|
|
||||||
|
class QForegroundAppHelper(context: Context): UsageStatsForegroundAppHelper(context) {
|
||||||
|
companion object {
|
||||||
|
private const val LOG_TAG = "QForegroundAppHelper"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val legacy = LollipopForegroundAppHelper(context)
|
||||||
|
private val modern = InstanceIdForegroundAppHelper(context)
|
||||||
|
private var fallbackCounter = 0
|
||||||
|
|
||||||
|
override suspend fun getForegroundApps(
|
||||||
|
queryInterval: Long,
|
||||||
|
experimentalFlags: Long
|
||||||
|
): Set<ForegroundApp> {
|
||||||
|
val useInstanceIdForegroundAppDetection = experimentalFlags and ExperimentalFlags.INSTANCE_ID_FG_APP_DETECTION == ExperimentalFlags.INSTANCE_ID_FG_APP_DETECTION
|
||||||
|
|
||||||
|
val result = if (useInstanceIdForegroundAppDetection && fallbackCounter == 0) {
|
||||||
|
try {
|
||||||
|
modern.getForegroundApps(queryInterval, experimentalFlags)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "falling back to the legacy implementation", ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
fallbackCounter = 100
|
||||||
|
|
||||||
|
legacy.getForegroundApps(queryInterval, experimentalFlags)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
legacy.getForegroundApps(queryInterval, experimentalFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallbackCounter > 0) {
|
||||||
|
fallbackCounter -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,180 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android.foregroundapp
|
||||||
|
|
||||||
|
import android.app.usage.UsageEvents
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.os.Parcel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is/should be a Parcel compatible clone of the UsageEvents
|
||||||
|
* system class. This allows reading fields which the public API does not provide.
|
||||||
|
*/
|
||||||
|
class TlUsageEvents (private val content: Parcel) {
|
||||||
|
companion object {
|
||||||
|
const val NONE = 0
|
||||||
|
const val MOVE_TO_FOREGROUND = 1
|
||||||
|
const val MOVE_TO_BACKGROUND = 2
|
||||||
|
// const val END_OF_DAY = 3
|
||||||
|
// const val CONTINUE_PREVIOUS_DAY = 4
|
||||||
|
const val CONFIGURATION_CHANGE = 5
|
||||||
|
// const val SYSTEM_INTERACTION = 6
|
||||||
|
// const val USER_INTERACTION = 7
|
||||||
|
const val SHORTCUT_INVOCATION = 8
|
||||||
|
const val CHOOSER_ACTION = 9
|
||||||
|
// const val NOTIFICATION_SEEN = 10
|
||||||
|
const val STANDBY_BUCKET_CHANGED = 11
|
||||||
|
const val NOTIFICATION_INTERRUPTION = 12
|
||||||
|
// const val SLICE_PINNED_PRIV = 13
|
||||||
|
// const val SLICE_PINNED = 14
|
||||||
|
// const val SCREEN_INTERACTIVE = 15
|
||||||
|
// const val SCREEN_NON_INTERACTIVE = 16
|
||||||
|
// const val KEYGUARD_SHOWN = 17
|
||||||
|
// const val KEYGUARD_HIDDEN = 18
|
||||||
|
// const val FOREGROUND_SERVICE_START = 19
|
||||||
|
// const val FOREGROUND_SERVICE_STOP = 20
|
||||||
|
// const val CONTINUING_FOREGROUND_SERVICE = 21
|
||||||
|
// const val ROLLOVER_FOREGROUND_SERVICE = 22
|
||||||
|
const val ACTIVITY_STOPPED = 23
|
||||||
|
// const val ACTIVITY_DESTROYED = 24
|
||||||
|
// const val FLUSH_TO_DISK = 25
|
||||||
|
// const val DEVICE_SHUTDOWN = 26
|
||||||
|
const val DEVICE_STARTUP = 27
|
||||||
|
// const val USER_UNLOCKED = 28
|
||||||
|
// const val USER_STOPPED = 29
|
||||||
|
const val LOCUS_ID_SET = 30
|
||||||
|
// const val APP_COMPONENT_USED = 31
|
||||||
|
const val MAX_EVENT_TYPE = 31
|
||||||
|
const val DUMMY_STRING = "null"
|
||||||
|
|
||||||
|
fun fromUsageEvents(input: UsageEvents): TlUsageEvents {
|
||||||
|
val outerParcel = Parcel.obtain()
|
||||||
|
|
||||||
|
val blob = try {
|
||||||
|
input.writeToParcel(outerParcel, 0)
|
||||||
|
outerParcel.setDataPosition(0)
|
||||||
|
|
||||||
|
ParcelBlob.readBlob(outerParcel)
|
||||||
|
} finally {
|
||||||
|
outerParcel.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
val innerParcel = Parcel.obtain()
|
||||||
|
|
||||||
|
try {
|
||||||
|
innerParcel.unmarshall(blob, 0, blob.size)
|
||||||
|
innerParcel.setDataPosition(0)
|
||||||
|
|
||||||
|
return TlUsageEvents(innerParcel)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
innerParcel.recycle()
|
||||||
|
|
||||||
|
throw ex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var free = false
|
||||||
|
private val length = content.readInt()
|
||||||
|
private var index = content.readInt()
|
||||||
|
private val strings = if (length > 0) content.createStringArray() else null
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (length > 0) {
|
||||||
|
val listByteLength = content.readInt()
|
||||||
|
val positionInParcel = content.readInt()
|
||||||
|
|
||||||
|
content.setDataPosition(content.dataPosition() + positionInParcel)
|
||||||
|
content.setDataSize(content.dataPosition() + listByteLength)
|
||||||
|
} else {
|
||||||
|
free()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var outputTimestamp = 0L
|
||||||
|
private var outputEventType = 0
|
||||||
|
private var outputInstanceId = 0
|
||||||
|
private var outputPackageName = DUMMY_STRING
|
||||||
|
private var outputClassName = DUMMY_STRING
|
||||||
|
|
||||||
|
val timestamp get() = outputTimestamp
|
||||||
|
val eventType get() = outputEventType
|
||||||
|
val instanceId get() = outputInstanceId
|
||||||
|
val packageName get() = outputPackageName
|
||||||
|
val className get() = outputClassName
|
||||||
|
|
||||||
|
fun readNextItem(): Boolean {
|
||||||
|
if (free) return false
|
||||||
|
if (strings == null) throw IllegalStateException()
|
||||||
|
|
||||||
|
val packageIndex = content.readInt()
|
||||||
|
val classIndex = content.readInt()
|
||||||
|
val instanceId = content.readInt()
|
||||||
|
val taskRootPackageIndex = content.readInt()
|
||||||
|
val taskRootClassIndex = content.readInt()
|
||||||
|
val eventType = content.readInt()
|
||||||
|
val timestamp = content.readLong()
|
||||||
|
|
||||||
|
if (eventType < NONE || eventType > MAX_EVENT_TYPE) {
|
||||||
|
throw UnknownEventTypeException()
|
||||||
|
}
|
||||||
|
|
||||||
|
when (eventType) {
|
||||||
|
CONFIGURATION_CHANGE -> {
|
||||||
|
val newConfiguration = Configuration.CREATOR.createFromParcel(content)
|
||||||
|
}
|
||||||
|
SHORTCUT_INVOCATION -> {
|
||||||
|
val shortcutId = content.readString()
|
||||||
|
}
|
||||||
|
CHOOSER_ACTION -> {
|
||||||
|
val action = content.readString()
|
||||||
|
val contentType = content.readString()
|
||||||
|
val contentAnnotations = content.createStringArray()
|
||||||
|
}
|
||||||
|
STANDBY_BUCKET_CHANGED -> {
|
||||||
|
val bucketAndReason = content.readInt()
|
||||||
|
}
|
||||||
|
NOTIFICATION_INTERRUPTION -> {
|
||||||
|
val notificationChannelId = content.readString()
|
||||||
|
}
|
||||||
|
LOCUS_ID_SET -> {
|
||||||
|
val locusId = content.readString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val flags = content.readInt()
|
||||||
|
|
||||||
|
outputTimestamp = timestamp
|
||||||
|
outputEventType = eventType
|
||||||
|
outputInstanceId = instanceId
|
||||||
|
outputPackageName = if (packageIndex == -1) DUMMY_STRING else strings[packageIndex]
|
||||||
|
outputClassName = if(classIndex == -1) DUMMY_STRING else strings[classIndex]
|
||||||
|
|
||||||
|
index++; if (index == length) free()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun free() {
|
||||||
|
if (!free) {
|
||||||
|
content.recycle()
|
||||||
|
|
||||||
|
free = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class UsageException: RuntimeException()
|
||||||
|
class UnknownEventTypeException: UsageException()
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.integration.platform.android.foregroundapp
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.app.AppOpsManager
|
||||||
|
import android.app.usage.UsageStatsManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import io.timelimit.android.BuildConfig
|
||||||
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
abstract class UsageStatsForegroundAppHelper (context: Context): ForegroundAppHelper() {
|
||||||
|
protected val usageStatsManager = context.getSystemService(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) Context.USAGE_STATS_SERVICE else "usagestats") as UsageStatsManager
|
||||||
|
protected val packageManager: PackageManager = context.packageManager
|
||||||
|
protected val backgroundThread: Executor by lazy { Executors.newSingleThreadExecutor() }
|
||||||
|
private val appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
|
||||||
|
private val packageName = context.packageName
|
||||||
|
|
||||||
|
override fun getPermissionStatus(): RuntimePermissionStatus {
|
||||||
|
val appOpsStatus = appOpsManager.checkOpNoThrow("android:get_usage_stats", android.os.Process.myUid(), packageName)
|
||||||
|
val packageManagerStatus = packageManager.checkPermission("android.permission.PACKAGE_USAGE_STATS", BuildConfig.APPLICATION_ID)
|
||||||
|
|
||||||
|
val allowedUsingSystemSettings = appOpsStatus == AppOpsManager.MODE_ALLOWED
|
||||||
|
val allowedUsingAdb = appOpsStatus == AppOpsManager.MODE_DEFAULT && packageManagerStatus == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
if(allowedUsingSystemSettings || allowedUsingAdb) {
|
||||||
|
return RuntimePermissionStatus.Granted
|
||||||
|
} else {
|
||||||
|
return RuntimePermissionStatus.NotGranted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -111,7 +111,7 @@ class DummyIntegration(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getForegroundApps(queryInterval: Long, enableMultiAppDetection: Boolean): Set<ForegroundApp> {
|
override suspend fun getForegroundApps(queryInterval: Long, experimentalFlags: Long): Set<ForegroundApp> {
|
||||||
if (foregroundAppPermission == RuntimePermissionStatus.NotGranted) {
|
if (foregroundAppPermission == RuntimePermissionStatus.NotGranted) {
|
||||||
throw SecurityException()
|
throw SecurityException()
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ object AppAffectedByPrimaryDeviceUtil {
|
||||||
val currentApps = try {
|
val currentApps = try {
|
||||||
logic.platformIntegration.getForegroundApps(
|
logic.platformIntegration.getForegroundApps(
|
||||||
logic.getForegroundAppQueryInterval(),
|
logic.getForegroundAppQueryInterval(),
|
||||||
logic.getEnableMultiAppDetection()
|
deviceAndUserRelatedData.deviceRelatedData.experimentalFlags
|
||||||
)
|
)
|
||||||
} catch (ex: SecurityException) {
|
} catch (ex: SecurityException) {
|
||||||
emptySet<ForegroundApp>()
|
emptySet<ForegroundApp>()
|
||||||
|
|
|
@ -73,12 +73,8 @@ class AppLogic(
|
||||||
}.ignoreUnchanged()
|
}.ignoreUnchanged()
|
||||||
|
|
||||||
private val foregroundAppQueryInterval = database.config().getForegroundAppQueryIntervalAsync().apply { observeForever { } }
|
private val foregroundAppQueryInterval = database.config().getForegroundAppQueryIntervalAsync().apply { observeForever { } }
|
||||||
private val enableMultiAppDetection = database.config().experimentalFlags
|
|
||||||
.map { it and ExperimentalFlags.MULTI_APP_DETECTION == ExperimentalFlags.MULTI_APP_DETECTION }.ignoreUnchanged()
|
|
||||||
.apply {observeForever { } }
|
|
||||||
|
|
||||||
fun getForegroundAppQueryInterval() = foregroundAppQueryInterval.value ?: 0L
|
fun getForegroundAppQueryInterval() = foregroundAppQueryInterval.value ?: 0L
|
||||||
fun getEnableMultiAppDetection() = enableMultiAppDetection.value ?: false
|
|
||||||
|
|
||||||
val serverLogic = ServerLogic(this)
|
val serverLogic = ServerLogic(this)
|
||||||
val defaultUserLogic = DefaultUserLogic(this)
|
val defaultUserLogic = DefaultUserLogic(this)
|
||||||
|
|
|
@ -159,8 +159,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
if (blockedAppPackageName != appLogic.platformIntegration.getLauncherAppPackageName()) {
|
if (blockedAppPackageName != appLogic.platformIntegration.getLauncherAppPackageName()) {
|
||||||
AccessibilityService.instance?.showHomescreen()
|
AccessibilityService.instance?.showHomescreen()
|
||||||
delay(100)
|
delay(100)
|
||||||
AccessibilityService.instance?.showHomescreen()
|
|
||||||
delay(100)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,7 +294,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
|
|
||||||
val foregroundApps = appLogic.platformIntegration.getForegroundApps(
|
val foregroundApps = appLogic.platformIntegration.getForegroundApps(
|
||||||
appLogic.getForegroundAppQueryInterval(),
|
appLogic.getForegroundAppQueryInterval(),
|
||||||
appLogic.getEnableMultiAppDetection()
|
deviceRelatedData.experimentalFlags
|
||||||
)
|
)
|
||||||
|
|
||||||
val audioPlaybackPackageName = appLogic.platformIntegration.getMusicPlaybackPackage()
|
val audioPlaybackPackageName = appLogic.platformIntegration.getMusicPlaybackPackage()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -195,6 +195,12 @@ data class DiagnoseExperimentalFlagItem(
|
||||||
enableFlags = ExperimentalFlags.SYNC_RELATED_NOTIFICATIONS,
|
enableFlags = ExperimentalFlags.SYNC_RELATED_NOTIFICATIONS,
|
||||||
disableFlags = ExperimentalFlags.SYNC_RELATED_NOTIFICATIONS,
|
disableFlags = ExperimentalFlags.SYNC_RELATED_NOTIFICATIONS,
|
||||||
enable = { true }
|
enable = { true }
|
||||||
|
),
|
||||||
|
DiagnoseExperimentalFlagItem(
|
||||||
|
label = R.string.diagnose_exf_ifd,
|
||||||
|
enableFlags = ExperimentalFlags.INSTANCE_ID_FG_APP_DETECTION,
|
||||||
|
disableFlags = ExperimentalFlags.INSTANCE_ID_FG_APP_DETECTION,
|
||||||
|
enable = { true }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -522,6 +522,7 @@
|
||||||
<string name="diagnose_exf_hmw">Manipulationswarnung in der Kategorienliste ausblenden</string>
|
<string name="diagnose_exf_hmw">Manipulationswarnung in der Kategorienliste ausblenden</string>
|
||||||
<string name="diagnose_exf_esb">Overlay und Home-Button nicht zum Sperren verwenden</string>
|
<string name="diagnose_exf_esb">Overlay und Home-Button nicht zum Sperren verwenden</string>
|
||||||
<string name="diagnose_exf_srn">Toasts zur Synchronisation anzeigen</string>
|
<string name="diagnose_exf_srn">Toasts zur Synchronisation anzeigen</string>
|
||||||
|
<string name="diagnose_exf_ifd">neue App-Erkennungs-Methode verwenden</string>
|
||||||
|
|
||||||
<string name="diagnose_bg_task_loop_ex">Hintergrundaufgabenschleifenfehler</string>
|
<string name="diagnose_bg_task_loop_ex">Hintergrundaufgabenschleifenfehler</string>
|
||||||
|
|
||||||
|
|
|
@ -575,6 +575,7 @@
|
||||||
<string name="diagnose_exf_hmw">Hide manipulation warning in the category list</string>
|
<string name="diagnose_exf_hmw">Hide manipulation warning in the category list</string>
|
||||||
<string name="diagnose_exf_esb">Do not use a overlay or the home button for blocking</string>
|
<string name="diagnose_exf_esb">Do not use a overlay or the home button for blocking</string>
|
||||||
<string name="diagnose_exf_srn">Show sync related toasts</string>
|
<string name="diagnose_exf_srn">Show sync related toasts</string>
|
||||||
|
<string name="diagnose_exf_ifd">Use new App detection method</string>
|
||||||
|
|
||||||
<string name="diagnose_bg_task_loop_ex">Background task loop exception</string>
|
<string name="diagnose_bg_task_loop_ex">Background task loop exception</string>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue