mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +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 ENABLE_SOFT_BLOCKING = 16384L
|
||||
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 fun setShowBlockingOverlay(show: Boolean, blockedElement: String? = null)
|
||||
// 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 setAppStatusMessage(message: AppStatusMessage?)
|
||||
abstract fun isScreenOn(): Boolean
|
||||
|
|
|
@ -150,7 +150,7 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
|||
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 {
|
||||
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
|
||||
* 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 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 {
|
||||
try {
|
||||
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
|
||||
* 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
|
||||
|
||||
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
|
||||
|
||||
companion object {
|
||||
|
@ -32,7 +32,9 @@ abstract class ForegroundAppHelper {
|
|||
if (instance == null) {
|
||||
synchronized(lock) {
|
||||
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)
|
||||
} else {
|
||||
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
|
||||
* 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
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.AppOpsManager
|
||||
import android.app.usage.UsageEvents
|
||||
import android.app.usage.UsageStatsManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
|
@ -27,13 +25,14 @@ import android.util.Log
|
|||
import android.util.SparseIntArray
|
||||
import io.timelimit.android.BuildConfig
|
||||
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.RuntimePermissionStatus
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppHelper() {
|
||||
class LollipopForegroundAppHelper(context: Context) : UsageStatsForegroundAppHelper(context) {
|
||||
companion object {
|
||||
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 val currentForegroundApps = mutableMapOf<ForegroundApp, Int>()
|
||||
private val expectedStopEvents = mutableSetOf<ForegroundApp>()
|
||||
|
@ -66,11 +61,12 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
|
|||
private var lastEnableMultiAppDetection = false
|
||||
|
||||
@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) {
|
||||
throw SecurityException()
|
||||
}
|
||||
|
||||
val enableMultiAppDetection = experimentalFlags and ExperimentalFlags.MULTI_APP_DETECTION == ExperimentalFlags.MULTI_APP_DETECTION
|
||||
val effectiveEnableMultiAppDetection = enableMultiAppDetection && enableMultiAppDetectionGeneral
|
||||
|
||||
foregroundAppThread.executeAndWait {
|
||||
|
@ -249,20 +245,6 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
|
|||
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
|
||||
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) {
|
||||
throw SecurityException()
|
||||
}
|
||||
|
|
|
@ -41,8 +41,8 @@ object AppAffectedByPrimaryDeviceUtil {
|
|||
|
||||
val currentApps = try {
|
||||
logic.platformIntegration.getForegroundApps(
|
||||
logic.getForegroundAppQueryInterval(),
|
||||
logic.getEnableMultiAppDetection()
|
||||
logic.getForegroundAppQueryInterval(),
|
||||
deviceAndUserRelatedData.deviceRelatedData.experimentalFlags
|
||||
)
|
||||
} catch (ex: SecurityException) {
|
||||
emptySet<ForegroundApp>()
|
||||
|
|
|
@ -73,12 +73,8 @@ class AppLogic(
|
|||
}.ignoreUnchanged()
|
||||
|
||||
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 getEnableMultiAppDetection() = enableMultiAppDetection.value ?: false
|
||||
|
||||
val serverLogic = ServerLogic(this)
|
||||
val defaultUserLogic = DefaultUserLogic(this)
|
||||
|
|
|
@ -159,8 +159,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
if (blockedAppPackageName != appLogic.platformIntegration.getLauncherAppPackageName()) {
|
||||
AccessibilityService.instance?.showHomescreen()
|
||||
delay(100)
|
||||
AccessibilityService.instance?.showHomescreen()
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -295,8 +293,8 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
}
|
||||
|
||||
val foregroundApps = appLogic.platformIntegration.getForegroundApps(
|
||||
appLogic.getForegroundAppQueryInterval(),
|
||||
appLogic.getEnableMultiAppDetection()
|
||||
appLogic.getForegroundAppQueryInterval(),
|
||||
deviceRelatedData.experimentalFlags
|
||||
)
|
||||
|
||||
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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -195,7 +195,13 @@ data class DiagnoseExperimentalFlagItem(
|
|||
enableFlags = ExperimentalFlags.SYNC_RELATED_NOTIFICATIONS,
|
||||
disableFlags = ExperimentalFlags.SYNC_RELATED_NOTIFICATIONS,
|
||||
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_esb">Overlay und Home-Button nicht zum Sperren verwenden</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>
|
||||
|
||||
|
|
|
@ -575,6 +575,7 @@
|
|||
<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_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>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue