Allow configuring foreground app query interval

This commit is contained in:
Jonas L 2019-04-15 00:00:00 +00:00
parent 15976189fa
commit d3403b6866
21 changed files with 261 additions and 13 deletions

View file

@ -24,6 +24,7 @@ import io.timelimit.android.data.model.ConfigurationItemTypeConverter
import io.timelimit.android.data.model.ConfigurationItemTypeUtil import io.timelimit.android.data.model.ConfigurationItemTypeUtil
import io.timelimit.android.livedata.ignoreUnchanged import io.timelimit.android.livedata.ignoreUnchanged
import io.timelimit.android.livedata.map import io.timelimit.android.livedata.map
import java.lang.IllegalArgumentException
@Dao @Dao
@TypeConverters(ConfigurationItemTypeConverter::class) @TypeConverters(ConfigurationItemTypeConverter::class)
@ -208,4 +209,13 @@ abstract class ConfigDao {
fun getCustomServerUrlSync() = getValueOfKeySync(ConfigurationItemType.CustomServerUrl) ?: "" fun getCustomServerUrlSync() = getValueOfKeySync(ConfigurationItemType.CustomServerUrl) ?: ""
fun getCustomServerUrlAsync() = getValueOfKeyAsync(ConfigurationItemType.CustomServerUrl).map { it ?: "" } fun getCustomServerUrlAsync() = getValueOfKeyAsync(ConfigurationItemType.CustomServerUrl).map { it ?: "" }
fun setCustomServerUrlSync(url: String) = updateValueSync(ConfigurationItemType.CustomServerUrl, url) fun setCustomServerUrlSync(url: String) = updateValueSync(ConfigurationItemType.CustomServerUrl, url)
fun getForegroundAppQueryIntervalAsync(): LiveData<Long> = getValueOfKeyAsync(ConfigurationItemType.ForegroundAppQueryRange).map { (it ?: "0").toLong() }
fun setForegroundAppQueryIntervalSync(interval: Long) {
if (interval < 0) {
throw IllegalArgumentException()
}
updateValueSync(ConfigurationItemType.ForegroundAppQueryRange, interval.toString())
}
} }

View file

@ -88,7 +88,8 @@ enum class ConfigurationItemType {
LastAppVersionWhichSynced, LastAppVersionWhichSynced,
LastScreenOnTime, LastScreenOnTime,
ServerMessage, ServerMessage,
CustomServerUrl CustomServerUrl,
ForegroundAppQueryRange
} }
object ConfigurationItemTypeUtil { object ConfigurationItemTypeUtil {
@ -104,6 +105,7 @@ object ConfigurationItemTypeUtil {
private const val LAST_SCREEN_ON_TIME = 11 private const val LAST_SCREEN_ON_TIME = 11
private const val SERVER_MESSAGE = 12 private const val SERVER_MESSAGE = 12
private const val CUSTOM_SERVER_URL = 13 private const val CUSTOM_SERVER_URL = 13
private const val FOREGROUND_APP_QUERY_RANGE = 14
val TYPES = listOf( val TYPES = listOf(
ConfigurationItemType.OwnDeviceId, ConfigurationItemType.OwnDeviceId,
@ -117,7 +119,8 @@ object ConfigurationItemTypeUtil {
ConfigurationItemType.LastAppVersionWhichSynced, ConfigurationItemType.LastAppVersionWhichSynced,
ConfigurationItemType.LastScreenOnTime, ConfigurationItemType.LastScreenOnTime,
ConfigurationItemType.ServerMessage, ConfigurationItemType.ServerMessage,
ConfigurationItemType.CustomServerUrl ConfigurationItemType.CustomServerUrl,
ConfigurationItemType.ForegroundAppQueryRange
) )
fun serialize(value: ConfigurationItemType) = when(value) { fun serialize(value: ConfigurationItemType) = when(value) {
@ -133,6 +136,7 @@ object ConfigurationItemTypeUtil {
ConfigurationItemType.LastScreenOnTime -> LAST_SCREEN_ON_TIME ConfigurationItemType.LastScreenOnTime -> LAST_SCREEN_ON_TIME
ConfigurationItemType.ServerMessage -> SERVER_MESSAGE ConfigurationItemType.ServerMessage -> SERVER_MESSAGE
ConfigurationItemType.CustomServerUrl -> CUSTOM_SERVER_URL ConfigurationItemType.CustomServerUrl -> CUSTOM_SERVER_URL
ConfigurationItemType.ForegroundAppQueryRange -> FOREGROUND_APP_QUERY_RANGE
} }
fun parse(value: Int) = when(value) { fun parse(value: Int) = when(value) {
@ -148,6 +152,7 @@ object ConfigurationItemTypeUtil {
LAST_SCREEN_ON_TIME -> ConfigurationItemType.LastScreenOnTime LAST_SCREEN_ON_TIME -> ConfigurationItemType.LastScreenOnTime
SERVER_MESSAGE -> ConfigurationItemType.ServerMessage SERVER_MESSAGE -> ConfigurationItemType.ServerMessage
CUSTOM_SERVER_URL -> ConfigurationItemType.CustomServerUrl CUSTOM_SERVER_URL -> ConfigurationItemType.CustomServerUrl
FOREGROUND_APP_QUERY_RANGE -> ConfigurationItemType.ForegroundAppQueryRange
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
} }

View file

@ -43,7 +43,7 @@ abstract class PlatformIntegration(
abstract fun showAppLockScreen(currentPackageName: String, currentActivityName: String?) abstract fun showAppLockScreen(currentPackageName: String, currentActivityName: String?)
abstract fun setShowBlockingOverlay(show: Boolean) abstract fun setShowBlockingOverlay(show: Boolean)
// this should throw an SecurityException if the permission is missing // this should throw an SecurityException if the permission is missing
abstract suspend fun getForegroundApp(result: ForegroundAppSpec) abstract suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long)
abstract fun setAppStatusMessage(message: AppStatusMessage?) abstract fun setAppStatusMessage(message: AppStatusMessage?)
abstract fun isScreenOn(): Boolean abstract fun isScreenOn(): Boolean
abstract fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean) abstract fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean)

View file

@ -105,8 +105,8 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
return AdminStatus.getAdminStatus(context, policyManager) return AdminStatus.getAdminStatus(context, policyManager)
} }
override suspend fun getForegroundApp(result: ForegroundAppSpec) { override suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long) {
foregroundAppHelper.getForegroundApp(result) foregroundAppHelper.getForegroundApp(result, queryInterval)
} }
override fun getForegroundAppPermissionStatus(): RuntimePermissionStatus { override fun getForegroundAppPermissionStatus(): RuntimePermissionStatus {

View file

@ -23,7 +23,7 @@ import io.timelimit.android.integration.platform.RuntimePermissionStatus
class CompatForegroundAppHelper(context: Context) : ForegroundAppHelper() { class CompatForegroundAppHelper(context: Context) : ForegroundAppHelper() {
private val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager private val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
override suspend fun getForegroundApp(result: ForegroundAppSpec) { override suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long) {
try { try {
val activity = activityManager.getRunningTasks(1)[0].topActivity val activity = activityManager.getRunningTasks(1)[0].topActivity

View file

@ -21,7 +21,7 @@ import io.timelimit.android.integration.platform.ForegroundAppSpec
import io.timelimit.android.integration.platform.RuntimePermissionStatus import io.timelimit.android.integration.platform.RuntimePermissionStatus
abstract class ForegroundAppHelper { abstract class ForegroundAppHelper {
abstract suspend fun getForegroundApp(result: ForegroundAppSpec) abstract suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long)
abstract fun getPermissionStatus(): RuntimePermissionStatus abstract fun getPermissionStatus(): RuntimePermissionStatus
companion object { companion object {

View file

@ -43,7 +43,7 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
private val event = UsageEvents.Event() private val event = UsageEvents.Event()
@Throws(SecurityException::class) @Throws(SecurityException::class)
override suspend fun getForegroundApp(result: ForegroundAppSpec) { override suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long) {
if (getPermissionStatus() == RuntimePermissionStatus.NotGranted) { if (getPermissionStatus() == RuntimePermissionStatus.NotGranted) {
throw SecurityException() throw SecurityException()
} }
@ -51,7 +51,7 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
foregroundAppThread.executeAndWait { foregroundAppThread.executeAndWait {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
if (lastQueryTime > now) { if (lastQueryTime > now || queryInterval >= 1000 * 60 * 60 * 24 /* 1 day */) {
// if the time went backwards, forget everything // if the time went backwards, forget everything
lastQueryTime = 0 lastQueryTime = 0
lastPackage = null lastPackage = null
@ -69,7 +69,7 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
// which seems to provide all data // which seems to provide all data
// update: with 1 second, some App switching events were missed // update: with 1 second, some App switching events were missed
// it seems to always work with 1.5 seconds // it seems to always work with 1.5 seconds
lastQueryTime - 1500 lastQueryTime - Math.max(queryInterval, 1500)
} }
usageStatsManager.queryEvents(queryStartTime, now)?.let { usageEvents -> usageStatsManager.queryEvents(queryStartTime, now)?.let { usageEvents ->

View file

@ -97,7 +97,7 @@ class DummyIntegration(
} }
} }
override suspend fun getForegroundApp(result: ForegroundAppSpec) { override suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long) {
if (foregroundAppPermission == RuntimePermissionStatus.NotGranted) { if (foregroundAppPermission == RuntimePermissionStatus.NotGranted) {
throw SecurityException() throw SecurityException()
} }

View file

@ -40,7 +40,7 @@ object AppAffectedByPrimaryDeviceUtil {
val currentApp = ForegroundAppSpec.newInstance() val currentApp = ForegroundAppSpec.newInstance()
try { try {
logic.platformIntegration.getForegroundApp(currentApp) logic.platformIntegration.getForegroundApp(currentApp, logic.getForegroundAppQueryInterval())
} catch (ex: SecurityException) { } catch (ex: SecurityException) {
// ignore // ignore
} }

View file

@ -73,6 +73,9 @@ class AppLogic(
} }
}.ignoreUnchanged() }.ignoreUnchanged()
private val foregroundAppQueryInterval = database.config().getForegroundAppQueryIntervalAsync().apply { observeForever { } }
fun getForegroundAppQueryInterval() = foregroundAppQueryInterval.value ?: 0L
val serverLogic = ServerLogic(this) val serverLogic = ServerLogic(this)
val defaultUserLogic = DefaultUserLogic(this) val defaultUserLogic = DefaultUserLogic(this)
val realTimeLogic = RealTimeLogic(this) val realTimeLogic = RealTimeLogic(this)

View file

@ -249,7 +249,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
} }
} }
appLogic.platformIntegration.getForegroundApp(foregroundAppSpec) appLogic.platformIntegration.getForegroundApp(foregroundAppSpec, appLogic.getForegroundAppQueryInterval())
val foregroundAppPackageName = foregroundAppSpec.packageName val foregroundAppPackageName = foregroundAppSpec.packageName
val foregroundAppActivityName = foregroundAppSpec.activityName val foregroundAppActivityName = foregroundAppSpec.activityName
val activityLevelBlocking = appLogic.deviceEntry.value?.enableActivityLevelBlocking ?: false val activityLevelBlocking = appLogic.deviceEntry.value?.enableActivityLevelBlocking ?: false

View file

@ -0,0 +1,113 @@
/*
* TimeLimit Copyright <C> 2019 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.ui.diagnose
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RadioButton
import androidx.lifecycle.Observer
import io.timelimit.android.R
import io.timelimit.android.async.Threads
import io.timelimit.android.databinding.DiagnoseForegroundAppFragmentBinding
import io.timelimit.android.livedata.liveDataFromValue
import io.timelimit.android.livedata.map
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.AuthenticationFab
import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.util.TimeTextUtil
class DiagnoseForegroundAppFragment : Fragment() {
companion object {
private val buttonIntervals = listOf(
0,
5 * 1000,
30 * 1000,
60 * 1000,
15 * 60 * 1000,
60 * 60 * 1000,
24 * 60 * 60 * 1000,
7 * 24 * 60 * 60 * 1000
)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val activity: ActivityViewModelHolder = activity as ActivityViewModelHolder
val binding = DiagnoseForegroundAppFragmentBinding.inflate(inflater, container, false)
val auth = activity.getActivityViewModel()
val logic = DefaultAppLogic.with(context!!)
val currentValue = logic.database.config().getForegroundAppQueryIntervalAsync()
val currentId = currentValue.map {
val res = buttonIntervals.indexOf(it.toInt())
if (res == -1)
0
else
res
}
AuthenticationFab.manageAuthenticationFab(
fab = binding.fab,
shouldHighlight = auth.shouldHighlightAuthenticationButton,
authenticatedUser = auth.authenticatedUser,
doesSupportAuth = liveDataFromValue(true),
fragment = this
)
binding.fab.setOnClickListener { activity.showAuthenticationScreen() }
val allButtons = buttonIntervals.mapIndexed { index, interval ->
RadioButton(context!!).apply {
id = index
if (interval == 0) {
setText(R.string.diagnose_fga_query_range_min)
} else if (interval < 60 * 1000) {
text = TimeTextUtil.seconds(interval / 1000, context!!)
} else {
text = TimeTextUtil.time(interval, context!!)
}
}
}
allButtons.forEach { binding.radioGroup.addView(it) }
currentId.observe(this, Observer {
binding.radioGroup.check(it)
})
binding.radioGroup.setOnCheckedChangeListener { _, checkedId ->
val oldId = currentId.value
if (oldId != null && checkedId != oldId) {
if (auth.requestAuthenticationOrReturnTrue()) {
val newValue = buttonIntervals[checkedId]
Threads.database.execute {
logic.database.config().setForegroundAppQueryIntervalSync(newValue.toLong())
}
} else {
binding.radioGroup.check(oldId)
}
}
}
return binding.root
}
}

View file

@ -51,6 +51,13 @@ class DiagnoseMainFragment : Fragment() {
) )
} }
binding.diagnoseFgaButton.setOnClickListener {
navigation.safeNavigate(
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseForegroundAppFragment(),
R.id.diagnoseMainFragment
)
}
return binding.root return binding.root
} }
} }

View file

@ -29,6 +29,10 @@ object TimeTextUtil {
return context.resources.getQuantityString(R.plurals.util_time_minutes, minutes, minutes) return context.resources.getQuantityString(R.plurals.util_time_minutes, minutes, minutes)
} }
fun seconds(seconds: Int, context: Context): String {
return context.resources.getQuantityString(R.plurals.util_time_seconds, seconds, seconds)
}
fun days(days: Int, context: Context): String { fun days(days: Int, context: Context): String {
return context.resources.getQuantityString(R.plurals.util_time_days, days, days) return context.resources.getQuantityString(R.plurals.util_time_days, days, days)
} }

View file

@ -0,0 +1,70 @@
<!--
TimeLimit Copyright <C> 2019 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/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="io.timelimit.android.ui.diagnose.DiagnoseForegroundAppFragment">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/scroll"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:padding="8dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
app:cardUseCompatPadding="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:padding="8dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:textAppearance="?android:textAppearanceLarge"
android:text="@string/diagnose_fga_query_range"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<RadioGroup
android:id="@+id/radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:fabSize="normal"
android:src="@drawable/ic_lock_open_white_24dp"
android:layout_margin="16dp"
android:layout_gravity="end|bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -44,6 +44,12 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<Button
android:id="@+id/diagnose_fga_button"
android:text="@string/diagnose_fga_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View file

@ -290,6 +290,13 @@
app:exitAnim="@anim/nav_default_exit_anim" app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim" app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" /> app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_diagnoseMainFragment_to_diagnoseForegroundAppFragment"
app:destination="@id/diagnoseForegroundAppFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/diagnoseClockFragment" android:id="@+id/diagnoseClockFragment"
@ -311,4 +318,9 @@
android:name="io.timelimit.android.ui.migrate_to_connected.MigrateToConnectedModeFragment" android:name="io.timelimit.android.ui.migrate_to_connected.MigrateToConnectedModeFragment"
android:label="migrate_to_connected_mode_fragment" android:label="migrate_to_connected_mode_fragment"
tools:layout="@layout/migrate_to_connected_mode_fragment" /> tools:layout="@layout/migrate_to_connected_mode_fragment" />
<fragment
android:id="@+id/diagnoseForegroundAppFragment"
android:name="io.timelimit.android.ui.diagnose.DiagnoseForegroundAppFragment"
android:label="diagnose_foreground_app_fragment"
tools:layout="@layout/diagnose_foreground_app_fragment" />
</navigation> </navigation>

View file

@ -43,4 +43,8 @@
<string name="diagnose_sync_btn_clear_cache_toast">Cache geleert</string> <string name="diagnose_sync_btn_clear_cache_toast">Cache geleert</string>
<string name="diagnose_sync_btn_request_sync">Synchronisation anfordern</string> <string name="diagnose_sync_btn_request_sync">Synchronisation anfordern</string>
<string name="diagnose_sync_btn_request_sync_toast">Synchronisation wurde angefordert</string> <string name="diagnose_sync_btn_request_sync_toast">Synchronisation wurde angefordert</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_min">Minimal (Standard)</string>
</resources> </resources>

View file

@ -14,6 +14,11 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="util_time_seconds">
<item quantity="one">%d Sekunde</item>
<item quantity="other">%d Sekunden</item>
</plurals>
<plurals name="util_time_minutes"> <plurals name="util_time_minutes">
<item quantity="one">%d Minute</item> <item quantity="one">%d Minute</item>
<item quantity="other">%d Minuten</item> <item quantity="other">%d Minuten</item>

View file

@ -43,4 +43,8 @@
<string name="diagnose_sync_btn_clear_cache_toast">Cache cleared</string> <string name="diagnose_sync_btn_clear_cache_toast">Cache cleared</string>
<string name="diagnose_sync_btn_request_sync">Request sync</string> <string name="diagnose_sync_btn_request_sync">Request sync</string>
<string name="diagnose_sync_btn_request_sync_toast">sync was requested</string> <string name="diagnose_sync_btn_request_sync_toast">sync was requested</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_min">Minimum (Default)</string>
</resources> </resources>

View file

@ -14,6 +14,11 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
--> -->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="util_time_seconds">
<item quantity="one">%d second</item>
<item quantity="other">%d seconds</item>
</plurals>
<plurals name="util_time_minutes"> <plurals name="util_time_minutes">
<item quantity="one">%d minute</item> <item quantity="one">%d minute</item>
<item quantity="other">%d minutes</item> <item quantity="other">%d minutes</item>