mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 01:39:22 +02:00
Disable fallback and enable new app detection by random
This commit is contained in:
parent
72c3da3d0d
commit
c6e1a2b7c1
10 changed files with 34 additions and 532 deletions
|
@ -249,5 +249,5 @@ object ExperimentalFlags {
|
||||||
const val ENABLE_SOFT_BLOCKING = 16384L
|
const val ENABLE_SOFT_BLOCKING = 16384L
|
||||||
const val SYNC_RELATED_NOTIFICATIONS = 32768L
|
const val SYNC_RELATED_NOTIFICATIONS = 32768L
|
||||||
const val INSTANCE_ID_FG_APP_DETECTION = 65536L
|
const val INSTANCE_ID_FG_APP_DETECTION = 65536L
|
||||||
const val DISABLE_FG_APP_DETECTION_FALLBACK = 131072L
|
// private const val OBSOLETE_DISABLE_FG_APP_DETECTION_FALLBACK = 131072L
|
||||||
}
|
}
|
|
@ -16,49 +16,25 @@
|
||||||
package io.timelimit.android.integration.platform.android.foregroundapp
|
package io.timelimit.android.integration.platform.android.foregroundapp
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
import io.timelimit.android.BuildConfig
|
|
||||||
import io.timelimit.android.data.model.ExperimentalFlags
|
import io.timelimit.android.data.model.ExperimentalFlags
|
||||||
import io.timelimit.android.integration.platform.ForegroundApp
|
import io.timelimit.android.integration.platform.ForegroundApp
|
||||||
|
import io.timelimit.android.integration.platform.android.foregroundapp.usagestats.DirectUsageStatsReader
|
||||||
|
import java.security.SecureRandom
|
||||||
|
|
||||||
class QForegroundAppHelper(context: Context): UsageStatsForegroundAppHelper(context) {
|
class QForegroundAppHelper(context: Context): UsageStatsForegroundAppHelper(context) {
|
||||||
companion object {
|
|
||||||
private const val LOG_TAG = "QForegroundAppHelper"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val legacy = LollipopForegroundAppHelper(context)
|
private val legacy = LollipopForegroundAppHelper(context)
|
||||||
private val modern = InstanceIdForegroundAppHelper(context)
|
private val modern = InstanceIdForegroundAppHelper(context)
|
||||||
private var fallbackCounter = 0
|
private val forceNewMethod = SecureRandom().nextBoolean()
|
||||||
|
|
||||||
override suspend fun getForegroundApps(
|
override suspend fun getForegroundApps(
|
||||||
queryInterval: Long,
|
queryInterval: Long,
|
||||||
experimentalFlags: Long
|
experimentalFlags: Long
|
||||||
): Set<ForegroundApp> {
|
): Set<ForegroundApp> {
|
||||||
val useInstanceIdForegroundAppDetection = experimentalFlags and ExperimentalFlags.INSTANCE_ID_FG_APP_DETECTION == ExperimentalFlags.INSTANCE_ID_FG_APP_DETECTION
|
val canUseModern = DirectUsageStatsReader.instanceIdSupported
|
||||||
val disableFallback = experimentalFlags and ExperimentalFlags.DISABLE_FG_APP_DETECTION_FALLBACK == ExperimentalFlags.DISABLE_FG_APP_DETECTION_FALLBACK
|
val didUserRequestModern = experimentalFlags and ExperimentalFlags.INSTANCE_ID_FG_APP_DETECTION == ExperimentalFlags.INSTANCE_ID_FG_APP_DETECTION
|
||||||
|
val useModern = forceNewMethod || didUserRequestModern
|
||||||
|
|
||||||
val result = if (useInstanceIdForegroundAppDetection && disableFallback) {
|
return if (canUseModern && useModern) modern.getForegroundApps(queryInterval, experimentalFlags)
|
||||||
modern.getForegroundApps(queryInterval, experimentalFlags)
|
else legacy.getForegroundApps(queryInterval, experimentalFlags)
|
||||||
} else 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,65 +0,0 @@
|
||||||
/*
|
|
||||||
* 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")
|
|
||||||
}
|
|
|
@ -1,88 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.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()
|
|
||||||
}
|
|
|
@ -1,158 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
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 ParcelUsageStatsReader (private val content: Parcel): UsageStatsReader {
|
|
||||||
companion object {
|
|
||||||
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 {
|
|
||||||
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 innerParcel
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
innerParcel.recycle()
|
|
||||||
|
|
||||||
throw ex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fromUsageEvents(input: UsageEvents): ParcelUsageStatsReader = ParcelUsageStatsReader(
|
|
||||||
getParcel(input)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
override fun loadNextEvent(): 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()
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun free() {
|
|
||||||
if (!free) {
|
|
||||||
content.recycle()
|
|
||||||
|
|
||||||
free = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UntestedSystemVersionException: RuntimeException("untested system version for the Parcel implementation")
|
|
||||||
}
|
|
|
@ -201,12 +201,6 @@ data class DiagnoseExperimentalFlagItem(
|
||||||
enableFlags = ExperimentalFlags.INSTANCE_ID_FG_APP_DETECTION,
|
enableFlags = ExperimentalFlags.INSTANCE_ID_FG_APP_DETECTION,
|
||||||
disableFlags = ExperimentalFlags.INSTANCE_ID_FG_APP_DETECTION,
|
disableFlags = ExperimentalFlags.INSTANCE_ID_FG_APP_DETECTION,
|
||||||
enable = { true }
|
enable = { true }
|
||||||
),
|
|
||||||
DiagnoseExperimentalFlagItem(
|
|
||||||
label = R.string.diagnose_exf_fda,
|
|
||||||
enableFlags = ExperimentalFlags.DISABLE_FG_APP_DETECTION_FALLBACK,
|
|
||||||
disableFlags = ExperimentalFlags.DISABLE_FG_APP_DETECTION_FALLBACK,
|
|
||||||
enable = { true }
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,14 +16,11 @@
|
||||||
package io.timelimit.android.ui.diagnose
|
package io.timelimit.android.ui.diagnose
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.usage.UsageEvents
|
|
||||||
import android.app.usage.UsageStatsManager
|
import android.app.usage.UsageStatsManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcel
|
|
||||||
import android.util.Base64
|
|
||||||
import android.util.JsonWriter
|
import android.util.JsonWriter
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -38,7 +35,7 @@ import io.timelimit.android.R
|
||||||
import io.timelimit.android.async.Threads
|
import io.timelimit.android.async.Threads
|
||||||
import io.timelimit.android.databinding.DiagnoseForegroundAppFragmentBinding
|
import io.timelimit.android.databinding.DiagnoseForegroundAppFragmentBinding
|
||||||
import io.timelimit.android.integration.platform.android.foregroundapp.InstanceIdForegroundAppHelper
|
import io.timelimit.android.integration.platform.android.foregroundapp.InstanceIdForegroundAppHelper
|
||||||
import io.timelimit.android.integration.platform.android.foregroundapp.usagestats.ParcelUsageStatsReader
|
import io.timelimit.android.integration.platform.android.foregroundapp.usagestats.DirectUsageStatsReader
|
||||||
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
||||||
import io.timelimit.android.livedata.liveDataFromNullableValue
|
import io.timelimit.android.livedata.liveDataFromNullableValue
|
||||||
import io.timelimit.android.livedata.map
|
import io.timelimit.android.livedata.map
|
||||||
|
@ -53,7 +50,6 @@ import java.io.OutputStreamWriter
|
||||||
class DiagnoseForegroundAppFragment : Fragment(), FragmentWithCustomTitle {
|
class DiagnoseForegroundAppFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
companion object {
|
companion object {
|
||||||
private const val LOG_TAG = "DiagnoseForegroundApp"
|
private const val LOG_TAG = "DiagnoseForegroundApp"
|
||||||
private const val REQ_EXPORT_BINARY = 1
|
|
||||||
private const val REQ_EXPORT_TEXT = 2
|
private const val REQ_EXPORT_TEXT = 2
|
||||||
|
|
||||||
private val buttonIntervals = listOf(
|
private val buttonIntervals = listOf(
|
||||||
|
@ -144,71 +140,13 @@ class DiagnoseForegroundAppFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.osUsageStatsBinaryExportButton.isEnabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
|
||||||
binding.osUsageStatsBinaryExportButton.setOnClickListener {
|
|
||||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
|
||||||
try {
|
|
||||||
startActivityForResult(
|
|
||||||
Intent(Intent.ACTION_CREATE_DOCUMENT)
|
|
||||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
|
||||||
.setType("application/octet-stream")
|
|
||||||
.putExtra(Intent.EXTRA_TITLE, "timelimit-usage-stats-export.bin"),
|
|
||||||
REQ_EXPORT_BINARY
|
|
||||||
)
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
Toast.makeText(context, R.string.error_general, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCustomTitle(): LiveData<String?> = liveDataFromNullableValue("${getString(R.string.diagnose_fga_title)} < ${getString(R.string.about_diagnose_title)} < ${getString(R.string.main_tab_overview)}")
|
override fun getCustomTitle(): LiveData<String?> = liveDataFromNullableValue("${getString(R.string.diagnose_fga_title)} < ${getString(R.string.about_diagnose_title)} < ${getString(R.string.main_tab_overview)}")
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
if (requestCode == REQ_EXPORT_BINARY) {
|
if (requestCode == REQ_EXPORT_TEXT) {
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
|
||||||
val context = requireContext().applicationContext
|
|
||||||
|
|
||||||
Thread {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
||||||
throw RuntimeException("unsupported os version")
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
val service = context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
|
|
||||||
val currentData = service.queryEvents(now - InstanceIdForegroundAppHelper.START_QUERY_INTERVAL, now)
|
|
||||||
|
|
||||||
val bytes = try {
|
|
||||||
val parcel = ParcelUsageStatsReader.getParcel(currentData)
|
|
||||||
try { parcel.marshall() } finally { parcel.recycle() }
|
|
||||||
} finally {
|
|
||||||
val event = UsageEvents.Event()
|
|
||||||
|
|
||||||
while (currentData.getNextEvent(event)) {/* consume all items */}
|
|
||||||
}
|
|
||||||
|
|
||||||
context.contentResolver.openOutputStream(data!!.data!!)!!.use { stream ->
|
|
||||||
stream.write(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
Threads.mainThreadHandler.post {
|
|
||||||
Toast.makeText(context, R.string.diagnose_fga_export_toast_done, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.w(LOG_TAG, "could not do export", ex)
|
|
||||||
}
|
|
||||||
|
|
||||||
Threads.mainThreadHandler.post {
|
|
||||||
Toast.makeText(context, R.string.error_general, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
} else if (requestCode == REQ_EXPORT_TEXT) {
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
val context = requireContext().applicationContext
|
val context = requireContext().applicationContext
|
||||||
|
|
||||||
|
@ -221,115 +159,41 @@ class DiagnoseForegroundAppFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val service = context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
|
val service = context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager
|
||||||
val currentData = service.queryEvents(now - InstanceIdForegroundAppHelper.START_QUERY_INTERVAL, now)
|
val currentData = service.queryEvents(now - InstanceIdForegroundAppHelper.START_QUERY_INTERVAL, now)
|
||||||
val event = UsageEvents.Event()
|
val reader = DirectUsageStatsReader(currentData)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var nativeData = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
JsonWriter(
|
||||||
val parcel = ParcelUsageStatsReader.getParcel(currentData)
|
BufferedWriter(
|
||||||
val bytes = parcel.marshall()
|
OutputStreamWriter(
|
||||||
val reader = ParcelUsageStatsReader(parcel)
|
context.contentResolver.openOutputStream(
|
||||||
|
data!!.data!!
|
||||||
NativeEventReader(reader, bytes, parcel)
|
)!!
|
||||||
} else null
|
|
||||||
|
|
||||||
try {
|
|
||||||
JsonWriter(
|
|
||||||
BufferedWriter(
|
|
||||||
OutputStreamWriter(
|
|
||||||
context.contentResolver.openOutputStream(
|
|
||||||
data!!.data!!
|
|
||||||
)!!
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
).use { writer ->
|
)
|
||||||
writer.setIndent(" ")
|
).use { writer ->
|
||||||
|
writer.setIndent(" ")
|
||||||
|
|
||||||
|
writer.beginArray()
|
||||||
|
|
||||||
|
while (reader.loadNextEvent()) {
|
||||||
writer.beginObject()
|
writer.beginObject()
|
||||||
|
|
||||||
nativeData?.let { native ->
|
writer.name("timestamp").value(reader.timestamp)
|
||||||
writer.name("header").value(Base64.encodeToString(
|
writer.name("type").value(reader.eventType)
|
||||||
native.bytes, 0, native.parcel.dataPosition(), Base64.NO_WRAP or Base64.NO_PADDING
|
writer.name("packageName").value(reader.packageName)
|
||||||
))
|
writer.name("className").value(reader.className)
|
||||||
}
|
|
||||||
|
|
||||||
writer.name("events").also {
|
val instanceId = try { reader.instanceId } catch (ex: Exception) { null }
|
||||||
writer.beginArray()
|
|
||||||
|
|
||||||
while (currentData.getNextEvent(event)) {
|
if (instanceId != null) writer.name("instanceId").value(instanceId)
|
||||||
writer.beginObject()
|
|
||||||
|
|
||||||
writer.name("timestamp").value(event.timeStamp)
|
|
||||||
writer.name("type").value(event.eventType)
|
|
||||||
writer.name("packageName").value(event.packageName)
|
|
||||||
writer.name("className").value(event.className)
|
|
||||||
|
|
||||||
nativeData?.let { native ->
|
|
||||||
val positionBefore = native.parcel.dataPosition()
|
|
||||||
|
|
||||||
val result: ReadNextItemResult = try {
|
|
||||||
if (native.events.loadNextEvent()) ReadNextItemResult.NextItem
|
|
||||||
else ReadNextItemResult.EndOfData
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
ReadNextItemResult.Error(ex)
|
|
||||||
}
|
|
||||||
|
|
||||||
val positionAfter = native.parcel.dataPosition().let {
|
|
||||||
if (it < positionBefore) native.bytes.size else it
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.name("custom")
|
|
||||||
|
|
||||||
when (result) {
|
|
||||||
ReadNextItemResult.NextItem -> {
|
|
||||||
writer.beginObject()
|
|
||||||
|
|
||||||
writer.name("timestamp").value(native.events.timestamp)
|
|
||||||
writer.name("type").value(native.events.eventType)
|
|
||||||
writer.name("instanceId").value(native.events.instanceId)
|
|
||||||
writer.name("packageName").value(native.events.packageName)
|
|
||||||
writer.name("className").value(native.events.className)
|
|
||||||
writer.name("binary").value(Base64.encodeToString(
|
|
||||||
native.bytes, positionBefore, positionAfter - positionBefore,
|
|
||||||
Base64.NO_WRAP or Base64.NO_PADDING
|
|
||||||
))
|
|
||||||
|
|
||||||
writer.endObject()
|
|
||||||
}
|
|
||||||
ReadNextItemResult.EndOfData -> {
|
|
||||||
writer.value("end of data reached")
|
|
||||||
|
|
||||||
native.free()
|
|
||||||
nativeData = null
|
|
||||||
}
|
|
||||||
is ReadNextItemResult.Error -> {
|
|
||||||
writer.beginObject()
|
|
||||||
|
|
||||||
writer.name("error message").value("${result.err}")
|
|
||||||
writer.name("remaining binary data").value(Base64.encodeToString(
|
|
||||||
native.bytes, positionBefore, native.bytes.size - positionBefore,
|
|
||||||
Base64.NO_WRAP or Base64.NO_PADDING
|
|
||||||
))
|
|
||||||
|
|
||||||
writer.endObject()
|
|
||||||
|
|
||||||
native.free()
|
|
||||||
nativeData = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.endObject()
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.endArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.endObject()
|
writer.endObject()
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
nativeData?.free()
|
writer.endArray()
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
while (currentData.getNextEvent(event)) {/* consume all items */}
|
reader.free()
|
||||||
}
|
}
|
||||||
|
|
||||||
Threads.mainThreadHandler.post {
|
Threads.mainThreadHandler.post {
|
||||||
|
@ -348,14 +212,4 @@ class DiagnoseForegroundAppFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
}
|
}
|
||||||
} else super.onActivityResult(requestCode, resultCode, data)
|
} else super.onActivityResult(requestCode, resultCode, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class NativeEventReader(val events: ParcelUsageStatsReader, val bytes: ByteArray, val parcel: Parcel /* owned by TlUsageEvents */) {
|
|
||||||
fun free() { events.free() }
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class ReadNextItemResult {
|
|
||||||
object EndOfData: ReadNextItemResult()
|
|
||||||
object NextItem: ReadNextItemResult()
|
|
||||||
class Error(val err: Exception): ReadNextItemResult()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,13 +71,6 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/os_usage_stats_binary_export_button"
|
|
||||||
style="?materialButtonOutlinedStyle"
|
|
||||||
android:text="@string/diagnose_fga_bin_export_btn"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
|
|
@ -512,7 +512,6 @@
|
||||||
<string name="diagnose_fga_title">Erkennung der aktiven App</string>
|
<string name="diagnose_fga_title">Erkennung der aktiven App</string>
|
||||||
<string name="diagnose_fga_query_range">Abfragezeitraum</string>
|
<string name="diagnose_fga_query_range">Abfragezeitraum</string>
|
||||||
<string name="diagnose_fga_query_range_min">Minimal (Standard)</string>
|
<string name="diagnose_fga_query_range_min">Minimal (Standard)</string>
|
||||||
<string name="diagnose_fga_bin_export_btn">binäre Nutzungsstatistik vom System exportieren</string>
|
|
||||||
<string name="diagnose_fga_txt_export_btn">Nutzungsstatistik vom System als Text exportieren</string>
|
<string name="diagnose_fga_txt_export_btn">Nutzungsstatistik vom System als Text exportieren</string>
|
||||||
<string name="diagnose_fga_export_toast_done">Export abgeschlossen</string>
|
<string name="diagnose_fga_export_toast_done">Export abgeschlossen</string>
|
||||||
|
|
||||||
|
@ -531,7 +530,6 @@
|
||||||
<string name="diagnose_exf_esb">Overlay und Home-Button nicht zum Sperren verwenden</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_srn">Toasts zur Synchronisation anzeigen</string>
|
||||||
<string name="diagnose_exf_ifd">neue App-Erkennungs-Methode verwenden</string>
|
<string name="diagnose_exf_ifd">neue App-Erkennungs-Methode verwenden</string>
|
||||||
<string name="diagnose_exf_fda">Fallback bei der App-Erkennung deaktivieren</string>
|
|
||||||
|
|
||||||
<string name="diagnose_bg_task_loop_ex">Hintergrundaufgabenschleifenfehler</string>
|
<string name="diagnose_bg_task_loop_ex">Hintergrundaufgabenschleifenfehler</string>
|
||||||
|
|
||||||
|
|
|
@ -565,7 +565,6 @@
|
||||||
<string name="diagnose_fga_title">Foreground-App-Detection</string>
|
<string name="diagnose_fga_title">Foreground-App-Detection</string>
|
||||||
<string name="diagnose_fga_query_range">Requested time range</string>
|
<string name="diagnose_fga_query_range">Requested time range</string>
|
||||||
<string name="diagnose_fga_query_range_min">Minimum (Default)</string>
|
<string name="diagnose_fga_query_range_min">Minimum (Default)</string>
|
||||||
<string name="diagnose_fga_bin_export_btn">Export binary OS usage stats</string>
|
|
||||||
<string name="diagnose_fga_txt_export_btn">Export text OS usage stats</string>
|
<string name="diagnose_fga_txt_export_btn">Export text OS usage stats</string>
|
||||||
<string name="diagnose_fga_export_toast_done">Export finished</string>
|
<string name="diagnose_fga_export_toast_done">Export finished</string>
|
||||||
|
|
||||||
|
@ -584,7 +583,6 @@
|
||||||
<string name="diagnose_exf_esb">Do not use a overlay or the home button for blocking</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_srn">Show sync related toasts</string>
|
||||||
<string name="diagnose_exf_ifd">Use new App detection method</string>
|
<string name="diagnose_exf_ifd">Use new App detection method</string>
|
||||||
<string name="diagnose_exf_fda">Disable fallback at the App detection</string>
|
|
||||||
|
|
||||||
<string name="diagnose_bg_task_loop_ex">Background task loop exception</string>
|
<string name="diagnose_bg_task_loop_ex">Background task loop exception</string>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue