diff --git a/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt b/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt index c2a23f3..faaedc9 100644 --- a/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt +++ b/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/PlatformIntegration.kt b/app/src/main/java/io/timelimit/android/integration/platform/PlatformIntegration.kt index ca1a85b..377ae6f 100644 --- a/app/src/main/java/io/timelimit/android/integration/platform/PlatformIntegration.kt +++ b/app/src/main/java/io/timelimit/android/integration/platform/PlatformIntegration.kt @@ -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 + abstract suspend fun getForegroundApps(queryInterval: Long, experimentalFlags: Long): Set abstract fun getMusicPlaybackPackage(): String? abstract fun setAppStatusMessage(message: AppStatusMessage?) abstract fun isScreenOn(): Boolean diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt index a0be3d7..2aa0ff7 100644 --- a/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/AndroidIntegration.kt @@ -150,7 +150,7 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio return AdminStatus.getAdminStatus(context, policyManager) } - override suspend fun getForegroundApps(queryInterval: Long, enableMultiAppDetection: Boolean): Set = foregroundAppHelper.getForegroundApps(queryInterval, enableMultiAppDetection) + override suspend fun getForegroundApps(queryInterval: Long, experimentalFlags: Long): Set = foregroundAppHelper.getForegroundApps(queryInterval, experimentalFlags) override fun getForegroundAppPermissionStatus(): RuntimePermissionStatus { return foregroundAppHelper.getPermissionStatus() diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/CompatForegroundAppHelper.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/CompatForegroundAppHelper.kt index 9c7746f..34b6373 100644 --- a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/CompatForegroundAppHelper.kt +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/CompatForegroundAppHelper.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * TimeLimit Copyright 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 = emptySet() private val mutex = Mutex() - override suspend fun getForegroundApps(queryInterval: Long, enableMultiAppDetection: Boolean): Set { + override suspend fun getForegroundApps(queryInterval: Long, experimentalFlags: Long): Set { mutex.withLock { try { val activity = activityManager.getRunningTasks(1)[0].topActivity!! diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/ForegroundAppHelper.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/ForegroundAppHelper.kt index a43e4c0..c410108 100644 --- a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/ForegroundAppHelper.kt +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/ForegroundAppHelper.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * TimeLimit Copyright 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 + abstract suspend fun getForegroundApps(queryInterval: Long, experimentalFlags: Long): Set 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) diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/InstanceIdForegroundAppHelper.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/InstanceIdForegroundAppHelper.kt new file mode 100644 index 0000000..21ec48d --- /dev/null +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/InstanceIdForegroundAppHelper.kt @@ -0,0 +1,110 @@ +/* + * TimeLimit Copyright 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 . + */ +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() + + override suspend fun getForegroundApps( + queryInterval: Long, + experimentalFlags: Long + ): Set { + 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() + + for (index in 0 until apps.size) { + appsSet.add(apps.valueAt(index)) + } + + appsSet + } + + return result + } + + class UntestedSystemVersionException: RuntimeException() +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/LollipopForegroundAppHelper.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/LollipopForegroundAppHelper.kt index 234c66c..0fd26ff 100644 --- a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/LollipopForegroundAppHelper.kt +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/LollipopForegroundAppHelper.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * TimeLimit Copyright 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() private val expectedStopEvents = mutableSetOf() @@ -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 { + override suspend fun getForegroundApps(queryInterval: Long, experimentalFlags: Long): Set { 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) diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/ParcelBlob.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/ParcelBlob.kt new file mode 100644 index 0000000..766e9ef --- /dev/null +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/ParcelBlob.kt @@ -0,0 +1,88 @@ +/* + * TimeLimit Copyright 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 . + */ +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() +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/QForegroundAppHelper.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/QForegroundAppHelper.kt new file mode 100644 index 0000000..2c92680 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/QForegroundAppHelper.kt @@ -0,0 +1,61 @@ +/* + * TimeLimit Copyright 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 . + */ +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 { + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/TlUsageEvents.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/TlUsageEvents.kt new file mode 100644 index 0000000..f5873b5 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/TlUsageEvents.kt @@ -0,0 +1,180 @@ +/* + * TimeLimit Copyright 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 . + */ +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() +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/UsageStatsForegroundAppHelper.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/UsageStatsForegroundAppHelper.kt new file mode 100644 index 0000000..04c23a7 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/UsageStatsForegroundAppHelper.kt @@ -0,0 +1,50 @@ +/* + * TimeLimit Copyright 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 . + */ +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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/dummy/DummyIntegration.kt b/app/src/main/java/io/timelimit/android/integration/platform/dummy/DummyIntegration.kt index ab20f17..ff94c4a 100644 --- a/app/src/main/java/io/timelimit/android/integration/platform/dummy/DummyIntegration.kt +++ b/app/src/main/java/io/timelimit/android/integration/platform/dummy/DummyIntegration.kt @@ -111,7 +111,7 @@ class DummyIntegration( } } - override suspend fun getForegroundApps(queryInterval: Long, enableMultiAppDetection: Boolean): Set { + override suspend fun getForegroundApps(queryInterval: Long, experimentalFlags: Long): Set { if (foregroundAppPermission == RuntimePermissionStatus.NotGranted) { throw SecurityException() } diff --git a/app/src/main/java/io/timelimit/android/logic/AppAffectedByPrimaryDeviceUtil.kt b/app/src/main/java/io/timelimit/android/logic/AppAffectedByPrimaryDeviceUtil.kt index 3755338..7dab759 100644 --- a/app/src/main/java/io/timelimit/android/logic/AppAffectedByPrimaryDeviceUtil.kt +++ b/app/src/main/java/io/timelimit/android/logic/AppAffectedByPrimaryDeviceUtil.kt @@ -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() diff --git a/app/src/main/java/io/timelimit/android/logic/AppLogic.kt b/app/src/main/java/io/timelimit/android/logic/AppLogic.kt index 1b8d564..cfa932f 100644 --- a/app/src/main/java/io/timelimit/android/logic/AppLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/AppLogic.kt @@ -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) diff --git a/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt b/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt index ee9e409..3bdb019 100644 --- a/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/BackgroundTaskLogic.kt @@ -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() diff --git a/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseExperimentalFlagFragment.kt b/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseExperimentalFlagFragment.kt index 6d3817e..c276200 100644 --- a/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseExperimentalFlagFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseExperimentalFlagFragment.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * TimeLimit Copyright 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 } + ) ) } } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 246b1d8..23a2ea9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -522,6 +522,7 @@ Manipulationswarnung in der Kategorienliste ausblenden Overlay und Home-Button nicht zum Sperren verwenden Toasts zur Synchronisation anzeigen + neue App-Erkennungs-Methode verwenden Hintergrundaufgabenschleifenfehler diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9aae881..2c76f97 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -575,6 +575,7 @@ Hide manipulation warning in the category list Do not use a overlay or the home button for blocking Show sync related toasts + Use new App detection method Background task loop exception