Refactor InstanceIdForegroundAppHelper

This commit is contained in:
Jonas Lochmann 2022-03-21 01:00:00 +01:00
parent 8b0463c6d6
commit 72c3da3d0d
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
8 changed files with 208 additions and 95 deletions

View file

@ -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<ForegroundApp>()
private val nativeEvent = UsageEvents.Event()
override suspend fun getForegroundApps(
queryInterval: Long,
experimentalFlags: Long
): Set<ForegroundApp> {
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()
}
}
// 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")
}
}

View file

@ -0,0 +1,49 @@
/*
* 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.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")
}

View file

@ -0,0 +1,65 @@
/*
* 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.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")
}

View file

@ -13,7 +13,7 @@
* 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
package io.timelimit.android.integration.platform.android.foregroundapp.usagestats
import android.os.Parcel
import java.io.FileInputStream

View file

@ -13,52 +13,33 @@
* 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
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")
}

View file

@ -0,0 +1,23 @@
/*
* 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.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
}

View file

@ -0,0 +1,27 @@
/*
* 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.usagestats
interface UsageStatsReader {
val timestamp: Long
val eventType: Int
val instanceId: Int
val packageName: String
val className: String
fun loadNextEvent(): Boolean
fun free()
}

View file

@ -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() }
}