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 index 96f4b82..b9de44d 100644 --- 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 @@ -16,7 +16,6 @@ package io.timelimit.android.integration.platform.android.foregroundapp import android.annotation.TargetApi -import android.app.usage.UsageEvents import android.content.Context import android.os.Build import android.util.SparseArray @@ -24,6 +23,8 @@ 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 +import io.timelimit.android.integration.platform.android.foregroundapp.usagestats.DirectUsageStatsReader +import io.timelimit.android.integration.platform.android.foregroundapp.usagestats.UsageStatsConstants @TargetApi(Build.VERSION_CODES.Q) class InstanceIdForegroundAppHelper(context: Context): UsageStatsForegroundAppHelper(context) { @@ -35,16 +36,11 @@ class InstanceIdForegroundAppHelper(context: Context): UsageStatsForegroundAppHe private var lastQueryTime = 0L private var lastEventTimestamp = 0L private val apps = SparseArray() - private val nativeEvent = UsageEvents.Event() override suspend fun getForegroundApps( queryInterval: Long, experimentalFlags: Long ): Set { - if (Build.VERSION.SDK_INT > 32) { - throw InstanceIdException.UntestedSystemVersionException() - } - if (getPermissionStatus() != RuntimePermissionStatus.Granted) { throw SecurityException() } @@ -74,54 +70,27 @@ class InstanceIdForegroundAppHelper(context: Context): UsageStatsForegroundAppHe val queryEndTime = now + TOLERANCE usageStatsManager.queryEvents(queryStartTime, queryEndTime)?.let { nativeEvents -> - val events = TlUsageEvents.fromUsageEvents(nativeEvents) + val events = DirectUsageStatsReader(nativeEvents) try { var isFirstEvent = true - while (true) { - // loop condition with additional checks - val didReadEvent = kotlin.run { - val didReadNativeEvent = nativeEvents.getNextEvent(nativeEvent) - val didReadTlEvent = events.readNextItem() - - if (didReadNativeEvent != didReadTlEvent) { - throw InstanceIdException.NotMatchingData( - if (didReadTlEvent) "events got next event but nativeEvents not" - else "nativeEvents got next event but events not" - ) - } - - didReadTlEvent // == didReadNativeEvent - } - - if (!didReadEvent) break - + while (events.loadNextEvent()) { // check the consistency - kotlin.run { - if (events.eventType != nativeEvent.eventType) { - throw InstanceIdException.NotMatchingData("got different eventTypes: ${events.eventType} vs ${nativeEvent.eventType}") - } - - if (events.timestamp != nativeEvent.timeStamp) { - throw InstanceIdException.NotMatchingData("got different timestamps: ${events.timestamp} vs ${nativeEvent.timeStamp}") - } - - if (events.timestamp < lastEventTimestamp && !isFirstEvent) { - throw InstanceIdException.EventsNotSortedByTimestamp() - } + if (events.timestamp < lastEventTimestamp && !isFirstEvent) { + throw InstanceIdException.EventsNotSortedByTimestamp() } // process the event - if (events.eventType == TlUsageEvents.DEVICE_STARTUP) { + if (events.eventType == UsageStatsConstants.DEVICE_STARTUP) { apps.clear() - } else if (events.eventType == TlUsageEvents.MOVE_TO_FOREGROUND) { + } else if (events.eventType == UsageStatsConstants.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 + events.eventType == UsageStatsConstants.MOVE_TO_BACKGROUND || + events.eventType == UsageStatsConstants.ACTIVITY_STOPPED ) { apps.remove(events.instanceId) } @@ -132,9 +101,6 @@ class InstanceIdForegroundAppHelper(context: Context): UsageStatsForegroundAppHe } } finally { events.free() - - // the nativeEvents have no free function; but they release their data when everything was read - while (nativeEvents.getNextEvent(nativeEvent)) {/* consume all values */} } } @@ -153,8 +119,6 @@ class InstanceIdForegroundAppHelper(context: Context): UsageStatsForegroundAppHe } sealed class InstanceIdException(message: String): RuntimeException(message) { - class UntestedSystemVersionException: InstanceIdException("untested system version") - class NotMatchingData(detail: String): InstanceIdException("not matching data: $detail") class EventsNotSortedByTimestamp: InstanceIdException("events not sorted by timestamp") } } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/usagestats/DirectUsageStatsReader.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/usagestats/DirectUsageStatsReader.kt new file mode 100644 index 0000000..c482e10 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/usagestats/DirectUsageStatsReader.kt @@ -0,0 +1,49 @@ +/* + * 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.usagestats + +import android.app.usage.UsageEvents + +class DirectUsageStatsReader (private val events: UsageEvents): UsageStatsReader { + private val event = UsageEvents.Event() + + companion object { + private val getInstanceId = try { + UsageEvents.Event::class.java.getMethod("getInstanceId") + } catch (ex: NoSuchMethodException) { + null + } + + val instanceIdSupported = getInstanceId != null + } + + override val timestamp: Long get() = event.timeStamp + override val eventType: Int get() = event.eventType + override val instanceId: Int get() = if (getInstanceId != null) getInstanceId.invoke(event) as Int + else throw InstanceIdUnsupportedException() + override val packageName: String get() = event.packageName + override val className: String get() = event.className ?: "null" + + override fun loadNextEvent() = events.getNextEvent(event) + + override fun free() { + while (events.getNextEvent(event)) { + // just consume it + } + } + + class InstanceIdUnsupportedException: RuntimeException("instance id unsupported") +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/usagestats/MergedUsageStatsReader.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/usagestats/MergedUsageStatsReader.kt new file mode 100644 index 0000000..3691856 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/usagestats/MergedUsageStatsReader.kt @@ -0,0 +1,65 @@ +/* + * 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.usagestats + +import java.lang.RuntimeException + +class MergedUsageStatsReader(private val primary: UsageStatsReader, private val confirmation: UsageStatsReader): UsageStatsReader { + override val timestamp = primary.timestamp + override val eventType = primary.eventType + override val instanceId = primary.instanceId + override val packageName = primary.packageName + override val className = primary.className + + override fun loadNextEvent(): Boolean { + val didReadEvent = kotlin.run { + val didReadPrimaryEvent = primary.loadNextEvent() + val didReadConfirmationEvent = confirmation.loadNextEvent() + + if (didReadConfirmationEvent != didReadPrimaryEvent) { + throw NotMatchingDataException( + if (didReadPrimaryEvent) "primary got next event but confirmation not" + else "confirmation got next event but primary not" + ) + } + + didReadPrimaryEvent // == didReadNativeEvent + } + + if (!didReadEvent) return false + + // check the consistency + if (primary.eventType != confirmation.eventType) { + throw NotMatchingDataException("got different eventTypes: ${primary.eventType} vs ${confirmation.eventType}") + } + + if (primary.timestamp != confirmation.timestamp) { + throw NotMatchingDataException("got different timestamps: ${primary.timestamp} vs ${confirmation.timestamp}") + } + + return true + } + + override fun free() { + try { + primary.free() + } finally { + confirmation.free() + } + } + + class NotMatchingDataException(detail: String): RuntimeException("not matching data: $detail") +} \ No newline at end of file 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/usagestats/ParcelBlob.kt similarity index 99% rename from app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/ParcelBlob.kt rename to app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/usagestats/ParcelBlob.kt index 766e9ef..44dcd56 100644 --- 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/usagestats/ParcelBlob.kt @@ -13,7 +13,7 @@ * 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 +package io.timelimit.android.integration.platform.android.foregroundapp.usagestats import android.os.Parcel import java.io.FileInputStream 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/usagestats/ParcelUsageStatsReader.kt similarity index 69% rename from app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/TlUsageEvents.kt rename to app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/usagestats/ParcelUsageStatsReader.kt index 3e823f1..256b5fc 100644 --- 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/usagestats/ParcelUsageStatsReader.kt @@ -13,52 +13,33 @@ * 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 +package io.timelimit.android.integration.platform.android.foregroundapp.usagestats import android.app.usage.UsageEvents import android.content.res.Configuration +import android.os.Build import android.os.Parcel +import java.lang.RuntimeException /** * 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) { +class ParcelUsageStatsReader (private val content: Parcel): UsageStatsReader { companion object { - 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 DUMMY_STRING = "null" + private const val CONFIGURATION_CHANGE = 5 + private const val SHORTCUT_INVOCATION = 8 + private const val CHOOSER_ACTION = 9 + private const val STANDBY_BUCKET_CHANGED = 11 + private const val NOTIFICATION_INTERRUPTION = 12 + private const val LOCUS_ID_SET = 30 + private const val DUMMY_STRING = "null" fun getParcel(input: UsageEvents): Parcel { + if (Build.VERSION.SDK_INT > 32) { + throw UntestedSystemVersionException() + } + val outerParcel = Parcel.obtain() val blob = try { @@ -84,7 +65,9 @@ class TlUsageEvents (private val content: Parcel) { } } - fun fromUsageEvents(input: UsageEvents): TlUsageEvents = TlUsageEvents(getParcel(input)) + fun fromUsageEvents(input: UsageEvents): ParcelUsageStatsReader = ParcelUsageStatsReader( + getParcel(input) + ) } private var free = false @@ -110,13 +93,13 @@ class TlUsageEvents (private val content: Parcel) { 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 + override val timestamp get() = outputTimestamp + override val eventType get() = outputEventType + override val instanceId get() = outputInstanceId + override val packageName get() = outputPackageName + override val className get() = outputClassName - fun readNextItem(): Boolean { + override fun loadNextEvent(): Boolean { if (free) return false if (strings == null) throw IllegalStateException() @@ -163,11 +146,13 @@ class TlUsageEvents (private val content: Parcel) { return true } - fun free() { + override fun free() { if (!free) { content.recycle() free = true } } + + class UntestedSystemVersionException: RuntimeException("untested system version for the Parcel implementation") } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/usagestats/UsageStatsConstants.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/usagestats/UsageStatsConstants.kt new file mode 100644 index 0000000..5f67030 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/usagestats/UsageStatsConstants.kt @@ -0,0 +1,23 @@ +/* + * 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.usagestats + +object UsageStatsConstants { + const val MOVE_TO_FOREGROUND = 1 + const val MOVE_TO_BACKGROUND = 2 + const val ACTIVITY_STOPPED = 23 + const val DEVICE_STARTUP = 27 +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/usagestats/UsageStatsReader.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/usagestats/UsageStatsReader.kt new file mode 100644 index 0000000..ace2202 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/foregroundapp/usagestats/UsageStatsReader.kt @@ -0,0 +1,27 @@ +/* + * 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.usagestats + +interface UsageStatsReader { + val timestamp: Long + val eventType: Int + val instanceId: Int + val packageName: String + val className: String + + fun loadNextEvent(): Boolean + fun free() +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseForegroundAppFragment.kt b/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseForegroundAppFragment.kt index f9249bf..9142d26 100644 --- a/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseForegroundAppFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/diagnose/DiagnoseForegroundAppFragment.kt @@ -38,7 +38,7 @@ import io.timelimit.android.R import io.timelimit.android.async.Threads import io.timelimit.android.databinding.DiagnoseForegroundAppFragmentBinding import io.timelimit.android.integration.platform.android.foregroundapp.InstanceIdForegroundAppHelper -import io.timelimit.android.integration.platform.android.foregroundapp.TlUsageEvents +import io.timelimit.android.integration.platform.android.foregroundapp.usagestats.ParcelUsageStatsReader import io.timelimit.android.livedata.liveDataFromNonNullValue import io.timelimit.android.livedata.liveDataFromNullableValue import io.timelimit.android.livedata.map @@ -182,7 +182,7 @@ class DiagnoseForegroundAppFragment : Fragment(), FragmentWithCustomTitle { val currentData = service.queryEvents(now - InstanceIdForegroundAppHelper.START_QUERY_INTERVAL, now) val bytes = try { - val parcel = TlUsageEvents.getParcel(currentData) + val parcel = ParcelUsageStatsReader.getParcel(currentData) try { parcel.marshall() } finally { parcel.recycle() } } finally { val event = UsageEvents.Event() @@ -225,9 +225,9 @@ class DiagnoseForegroundAppFragment : Fragment(), FragmentWithCustomTitle { try { var nativeData = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val parcel = TlUsageEvents.getParcel(currentData) + val parcel = ParcelUsageStatsReader.getParcel(currentData) val bytes = parcel.marshall() - val reader = TlUsageEvents(parcel) + val reader = ParcelUsageStatsReader(parcel) NativeEventReader(reader, bytes, parcel) } else null @@ -266,7 +266,7 @@ class DiagnoseForegroundAppFragment : Fragment(), FragmentWithCustomTitle { val positionBefore = native.parcel.dataPosition() val result: ReadNextItemResult = try { - if (native.events.readNextItem()) ReadNextItemResult.NextItem + if (native.events.loadNextEvent()) ReadNextItemResult.NextItem else ReadNextItemResult.EndOfData } catch (ex: Exception) { ReadNextItemResult.Error(ex) @@ -349,7 +349,7 @@ class DiagnoseForegroundAppFragment : Fragment(), FragmentWithCustomTitle { } else super.onActivityResult(requestCode, resultCode, data) } - internal class NativeEventReader(val events: TlUsageEvents, val bytes: ByteArray, val parcel: Parcel /* owned by TlUsageEvents */) { + internal class NativeEventReader(val events: ParcelUsageStatsReader, val bytes: ByteArray, val parcel: Parcel /* owned by TlUsageEvents */) { fun free() { events.free() } }