Disable fallback and enable new app detection by random

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

View file

@ -249,5 +249,5 @@ object ExperimentalFlags {
const val ENABLE_SOFT_BLOCKING = 16384L
const val SYNC_RELATED_NOTIFICATIONS = 32768L
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
}

View file

@ -16,49 +16,25 @@
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
import io.timelimit.android.integration.platform.android.foregroundapp.usagestats.DirectUsageStatsReader
import java.security.SecureRandom
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
private val forceNewMethod = SecureRandom().nextBoolean()
override suspend fun getForegroundApps(
queryInterval: Long,
experimentalFlags: Long
): Set<ForegroundApp> {
val useInstanceIdForegroundAppDetection = experimentalFlags and ExperimentalFlags.INSTANCE_ID_FG_APP_DETECTION == ExperimentalFlags.INSTANCE_ID_FG_APP_DETECTION
val disableFallback = experimentalFlags and ExperimentalFlags.DISABLE_FG_APP_DETECTION_FALLBACK == ExperimentalFlags.DISABLE_FG_APP_DETECTION_FALLBACK
val canUseModern = DirectUsageStatsReader.instanceIdSupported
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) {
modern.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
return if (canUseModern && useModern) modern.getForegroundApps(queryInterval, experimentalFlags)
else legacy.getForegroundApps(queryInterval, experimentalFlags)
}
}

View file

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

View file

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

View file

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

View file

@ -201,12 +201,6 @@ data class DiagnoseExperimentalFlagItem(
enableFlags = ExperimentalFlags.INSTANCE_ID_FG_APP_DETECTION,
disableFlags = ExperimentalFlags.INSTANCE_ID_FG_APP_DETECTION,
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 }
)
)
}

View file

@ -16,14 +16,11 @@
package io.timelimit.android.ui.diagnose
import android.app.Activity
import android.app.usage.UsageEvents
import android.app.usage.UsageStatsManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Parcel
import android.util.Base64
import android.util.JsonWriter
import android.util.Log
import android.view.LayoutInflater
@ -38,7 +35,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.usagestats.ParcelUsageStatsReader
import io.timelimit.android.integration.platform.android.foregroundapp.usagestats.DirectUsageStatsReader
import io.timelimit.android.livedata.liveDataFromNonNullValue
import io.timelimit.android.livedata.liveDataFromNullableValue
import io.timelimit.android.livedata.map
@ -53,7 +50,6 @@ import java.io.OutputStreamWriter
class DiagnoseForegroundAppFragment : Fragment(), FragmentWithCustomTitle {
companion object {
private const val LOG_TAG = "DiagnoseForegroundApp"
private const val REQ_EXPORT_BINARY = 1
private const val REQ_EXPORT_TEXT = 2
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
}
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?) {
if (requestCode == REQ_EXPORT_BINARY) {
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 (requestCode == REQ_EXPORT_TEXT) {
if (resultCode == Activity.RESULT_OK) {
val context = requireContext().applicationContext
@ -221,16 +159,7 @@ class DiagnoseForegroundAppFragment : Fragment(), FragmentWithCustomTitle {
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 event = UsageEvents.Event()
try {
var nativeData = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val parcel = ParcelUsageStatsReader.getParcel(currentData)
val bytes = parcel.marshall()
val reader = ParcelUsageStatsReader(parcel)
NativeEventReader(reader, bytes, parcel)
} else null
val reader = DirectUsageStatsReader(currentData)
try {
JsonWriter(
@ -243,93 +172,28 @@ class DiagnoseForegroundAppFragment : Fragment(), FragmentWithCustomTitle {
)
).use { writer ->
writer.setIndent(" ")
writer.beginObject()
nativeData?.let { native ->
writer.name("header").value(Base64.encodeToString(
native.bytes, 0, native.parcel.dataPosition(), Base64.NO_WRAP or Base64.NO_PADDING
))
}
writer.name("events").also {
writer.beginArray()
while (currentData.getNextEvent(event)) {
while (reader.loadNextEvent()) {
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)
writer.name("timestamp").value(reader.timestamp)
writer.name("type").value(reader.eventType)
writer.name("packageName").value(reader.packageName)
writer.name("className").value(reader.className)
nativeData?.let { native ->
val positionBefore = native.parcel.dataPosition()
val instanceId = try { reader.instanceId } catch (ex: Exception) { null }
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
}
}
}
if (instanceId != null) writer.name("instanceId").value(instanceId)
writer.endObject()
}
writer.endArray()
}
writer.endObject()
}
} finally {
nativeData?.free()
}
} finally {
while (currentData.getNextEvent(event)) {/* consume all items */}
reader.free()
}
Threads.mainThreadHandler.post {
@ -348,14 +212,4 @@ class DiagnoseForegroundAppFragment : Fragment(), FragmentWithCustomTitle {
}
} 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()
}
}

View file

@ -71,13 +71,6 @@
android:layout_width="match_parent"
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>
</androidx.cardview.widget.CardView>

View file

@ -512,7 +512,6 @@
<string name="diagnose_fga_title">Erkennung der aktiven App</string>
<string name="diagnose_fga_query_range">Abfragezeitraum</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_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_srn">Toasts zur Synchronisation anzeigen</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>

View file

@ -565,7 +565,6 @@
<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_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_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_srn">Show sync related toasts</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>