mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 17:59:51 +02:00
Refactor InstanceIdForegroundAppHelper
This commit is contained in:
parent
8b0463c6d6
commit
72c3da3d0d
8 changed files with 208 additions and 95 deletions
|
@ -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()
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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() }
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue