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 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
} }

View file

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

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, 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 }
) )
) )
} }

View file

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

View file

@ -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>

View file

@ -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>

View file

@ -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>