Adjust for GPlay policies related to the list of installed Apps

This commit is contained in:
Jonas Lochmann 2022-05-23 02:00:00 +02:00
parent d16117f885
commit fd5ffe73ce
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
21 changed files with 1911 additions and 261 deletions

File diff suppressed because it is too large Load diff

View file

@ -21,37 +21,37 @@ import io.timelimit.android.data.model.TimeLimitRule
import io.timelimit.android.extensions.MinuteOfDay
object DatabaseMigrations {
val MIGRATE_TO_V2 = object: Migration(1, 2) {
private val MIGRATE_TO_V2 = object: Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE device ADD COLUMN did_report_uninstall INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V3 = object: Migration(2, 3) {
private val MIGRATE_TO_V3 = object: Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE device ADD COLUMN is_user_kept_signed_in INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V4 = object: Migration(3, 4) {
private val MIGRATE_TO_V4 = object: Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `category_for_not_assigned_apps` TEXT NOT NULL DEFAULT \"\"")
}
}
val MIGRATE_TO_V5 = object: Migration(4, 5) {
private val MIGRATE_TO_V5 = object: Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `parent_category_id` TEXT NOT NULL DEFAULT \"\"")
}
}
val MIGRATE_TO_V6 = object: Migration(5, 6) {
private val MIGRATE_TO_V6 = object: Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `show_device_connected` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V7 = object: Migration(6, 7) {
private val MIGRATE_TO_V7 = object: Migration(6, 7) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `default_user` TEXT NOT NULL DEFAULT \"\"")
database.execSQL("ALTER TABLE `device` ADD COLUMN `default_user_timeout` INTEGER NOT NULL DEFAULT 0")
@ -59,7 +59,7 @@ object DatabaseMigrations {
}
}
val MIGRATE_TO_V8 = object: Migration(7, 8) {
private val MIGRATE_TO_V8 = object: Migration(7, 8) {
override fun migrate(database: SupportSQLiteDatabase) {
// this is empty
//
@ -67,14 +67,14 @@ object DatabaseMigrations {
}
}
val MIGRATE_TO_V9 = object: Migration(8, 9) {
private val MIGRATE_TO_V9 = object: Migration(8, 9) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `did_reboot` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE `device` ADD COLUMN `consider_reboot_manipulation` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V10 = object: Migration(9, 10) {
private val MIGRATE_TO_V10 = object: Migration(9, 10) {
override fun migrate(database: SupportSQLiteDatabase) {
// this is empty
//
@ -82,40 +82,40 @@ object DatabaseMigrations {
}
}
val MIGRATE_TO_V11 = object: Migration(10, 11) {
private val MIGRATE_TO_V11 = object: Migration(10, 11) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `mail_notification_flags` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V12 = object: Migration(11, 12) {
private val MIGRATE_TO_V12 = object: Migration(11, 12) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `block_all_notifications` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V13 = object: Migration(12, 13) {
private val MIGRATE_TO_V13 = object: Migration(12, 13) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `current_overlay_permission` TEXT NOT NULL DEFAULT \"not granted\"")
database.execSQL("ALTER TABLE `device` ADD COLUMN `highest_overlay_permission` TEXT NOT NULL DEFAULT \"not granted\"")
}
}
val MIGRATE_TO_V14 = object: Migration(13, 14) {
private val MIGRATE_TO_V14 = object: Migration(13, 14) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `current_accessibility_service_permission` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE `device` ADD COLUMN `was_accessibility_service_permission` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V15 = object: Migration(14, 15) {
private val MIGRATE_TO_V15 = object: Migration(14, 15) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE `app_activity` (`device_id` TEXT NOT NULL, `app_package_name` TEXT NOT NULL, `activity_class_name` TEXT NOT NULL, `activity_title` TEXT NOT NULL, PRIMARY KEY(`device_id`, `app_package_name`, `activity_class_name`))")
database.execSQL("ALTER TABLE `device` ADD COLUMN `enable_activity_level_blocking` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V16 = object: Migration(15, 16) {
private val MIGRATE_TO_V16 = object: Migration(15, 16) {
override fun migrate(database: SupportSQLiteDatabase) {
// this is empty
//
@ -123,20 +123,20 @@ object DatabaseMigrations {
}
}
val MIGRATE_TO_V17 = object: Migration(16, 17) {
private val MIGRATE_TO_V17 = object: Migration(16, 17) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `notification` (`type` INTEGER NOT NULL, `id` TEXT NOT NULL, `first_notify_time` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, PRIMARY KEY(`type`, `id`))")
}
}
val MIGRATE_TO_V18 = object: Migration(17, 18) {
private val MIGRATE_TO_V18 = object: Migration(17, 18) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `q_or_later` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE `category` ADD COLUMN `time_warnings` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V19 = object: Migration(18, 19) {
private val MIGRATE_TO_V19 = object: Migration(18, 19) {
override fun migrate(database: SupportSQLiteDatabase) {
// this is empty
//
@ -144,44 +144,44 @@ object DatabaseMigrations {
}
}
val MIGRATE_TO_V20 = object: Migration(19, 20) {
private val MIGRATE_TO_V20 = object: Migration(19, 20) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `allowed_contact` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `phone` TEXT NOT NULL)")
}
}
val MIGRATE_TO_V21 = object: Migration(20, 21) {
private val MIGRATE_TO_V21 = object: Migration(20, 21) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `had_manipulation_flags` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V22 = object: Migration(21, 22) {
private val MIGRATE_TO_V22 = object: Migration(21, 22) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `blocked_times` TEXT NOT NULL DEFAULT \"\"")
}
}
val MIGRATE_TO_V23 = object: Migration(22, 23) {
private val MIGRATE_TO_V23 = object: Migration(22, 23) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `min_battery_charging` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE `category` ADD COLUMN `min_battery_mobile` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V24 = object: Migration(23, 24) {
private val MIGRATE_TO_V24 = object: Migration(23, 24) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `temporarily_blocked_end_time` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V25 = object: Migration(24, 25) {
private val MIGRATE_TO_V25 = object: Migration(24, 25) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `sort` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V26 = object: Migration(25, 26) {
private val MIGRATE_TO_V26 = object: Migration(25, 26) {
override fun migrate(database: SupportSQLiteDatabase) {
// this is empty
//
@ -189,20 +189,20 @@ object DatabaseMigrations {
}
}
val MIGRATE_TO_V27 = object: Migration(26, 27) {
private val MIGRATE_TO_V27 = object: Migration(26, 27) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `extra_time_day` INTEGER NOT NULL DEFAULT -1")
}
}
val MIGRATE_TO_V28 = object: Migration(27, 28) {
private val MIGRATE_TO_V28 = object: Migration(27, 28) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `user_key` (`user_id` TEXT NOT NULL, `key` BLOB NOT NULL, `last_use` INTEGER NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_user_key_key` ON `user_key` (`key`)")
}
}
val MIGRATE_TO_V29 = object: Migration(28, 29) {
private val MIGRATE_TO_V29 = object: Migration(28, 29) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `start_minute_of_day` INTEGER NOT NULL DEFAULT ${TimeLimitRule.MIN_START_MINUTE}")
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `end_minute_of_day` INTEGER NOT NULL DEFAULT ${TimeLimitRule.MAX_END_MINUTE}")
@ -219,71 +219,120 @@ object DatabaseMigrations {
}
}
val MIGRATE_TO_V30 = object: Migration(29, 30) {
private val MIGRATE_TO_V30 = object: Migration(29, 30) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V31 = object: Migration(30, 31) {
private val MIGRATE_TO_V31 = object: Migration(30, 31) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `user_limit_login_category` (`user_id` TEXT NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
database.execSQL("CREATE INDEX IF NOT EXISTS `user_limit_login_category_index_category_id` ON `user_limit_login_category` (`category_id`)")
}
}
val MIGRATE_TO_V32 = object: Migration(31, 32) {
private val MIGRATE_TO_V32 = object: Migration(31, 32) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `category_network_id` (`category_id` TEXT NOT NULL, `network_item_id` TEXT NOT NULL, `hashed_network_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `network_item_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
}
}
val MIGRATE_TO_V33 = object: Migration(32, 33) {
private val MIGRATE_TO_V33 = object: Migration(32, 33) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `disable_limits_until` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V34 = object: Migration(33, 34) {
private val MIGRATE_TO_V34 = object: Migration(33, 34) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `child_task` (`task_id` TEXT NOT NULL, `category_id` TEXT NOT NULL, `task_title` TEXT NOT NULL, `extra_time_duration` INTEGER NOT NULL, `pending_request` INTEGER NOT NULL, `last_grant_timestamp` INTEGER NOT NULL, PRIMARY KEY(`task_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
database.execSQL("ALTER TABLE `category` ADD COLUMN `tasks_version` TEXT NOT NULL DEFAULT ''")
}
}
val MIGRATE_TO_V35 = object: Migration(34, 35) {
private val MIGRATE_TO_V35 = object: Migration(34, 35) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `per_day` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V36 = object: Migration(35, 36) {
private val MIGRATE_TO_V36 = object: Migration(35, 36) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user_limit_login_category` ADD COLUMN pre_block_duration INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V37 = object: Migration(36, 37) {
private val MIGRATE_TO_V37 = object: Migration(36, 37) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V38 = object: Migration(37, 38) {
private val MIGRATE_TO_V38 = object: Migration(37, 38) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE category ADD COLUMN block_notification_delay INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATE_TO_V39 = object: Migration(38, 39) {
private val MIGRATE_TO_V39 = object: Migration(38, 39) {
override fun migrate(database: SupportSQLiteDatabase) {
// nothing to do, there was just a new config item type added
}
}
val MIGRATE_TO_V40 = object: Migration(39, 40) {
private val MIGRATE_TO_V40 = object: Migration(39, 40) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `category_time_warning` (`category_id` TEXT NOT NULL, `minutes` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `minutes`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
}
}
private val MIGRATE_TO_V41 = object: Migration(40, 41) {
override fun migrate(database: SupportSQLiteDatabase) {
// nothing to do, there was just a new config item type added
}
}
val ALL = arrayOf(
MIGRATE_TO_V2,
MIGRATE_TO_V3,
MIGRATE_TO_V4,
MIGRATE_TO_V5,
MIGRATE_TO_V6,
MIGRATE_TO_V7,
MIGRATE_TO_V8,
MIGRATE_TO_V9,
MIGRATE_TO_V10,
MIGRATE_TO_V11,
MIGRATE_TO_V12,
MIGRATE_TO_V13,
MIGRATE_TO_V14,
MIGRATE_TO_V15,
MIGRATE_TO_V16,
MIGRATE_TO_V17,
MIGRATE_TO_V18,
MIGRATE_TO_V19,
MIGRATE_TO_V20,
MIGRATE_TO_V21,
MIGRATE_TO_V22,
MIGRATE_TO_V23,
MIGRATE_TO_V24,
MIGRATE_TO_V25,
MIGRATE_TO_V26,
MIGRATE_TO_V27,
MIGRATE_TO_V28,
MIGRATE_TO_V29,
MIGRATE_TO_V30,
MIGRATE_TO_V31,
MIGRATE_TO_V32,
MIGRATE_TO_V33,
MIGRATE_TO_V34,
MIGRATE_TO_V35,
MIGRATE_TO_V36,
MIGRATE_TO_V37,
MIGRATE_TO_V38,
MIGRATE_TO_V39,
MIGRATE_TO_V40,
MIGRATE_TO_V41
)
}

View file

@ -21,6 +21,7 @@ import androidx.room.Database
import androidx.room.InvalidationTracker
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import io.timelimit.android.async.Threads
import io.timelimit.android.data.dao.DerivedDataDao
@ -52,7 +53,7 @@ import java.util.concurrent.TimeUnit
CategoryNetworkId::class,
ChildTask::class,
CategoryTimeWarning::class
], version = 40)
], version = 41)
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
companion object {
private val lock = Object()
@ -87,47 +88,7 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
)
.setJournalMode(JournalMode.TRUNCATE)
.fallbackToDestructiveMigrationOnDowngrade()
.addMigrations(
DatabaseMigrations.MIGRATE_TO_V2,
DatabaseMigrations.MIGRATE_TO_V3,
DatabaseMigrations.MIGRATE_TO_V4,
DatabaseMigrations.MIGRATE_TO_V5,
DatabaseMigrations.MIGRATE_TO_V6,
DatabaseMigrations.MIGRATE_TO_V7,
DatabaseMigrations.MIGRATE_TO_V8,
DatabaseMigrations.MIGRATE_TO_V9,
DatabaseMigrations.MIGRATE_TO_V10,
DatabaseMigrations.MIGRATE_TO_V11,
DatabaseMigrations.MIGRATE_TO_V12,
DatabaseMigrations.MIGRATE_TO_V13,
DatabaseMigrations.MIGRATE_TO_V14,
DatabaseMigrations.MIGRATE_TO_V15,
DatabaseMigrations.MIGRATE_TO_V16,
DatabaseMigrations.MIGRATE_TO_V17,
DatabaseMigrations.MIGRATE_TO_V18,
DatabaseMigrations.MIGRATE_TO_V19,
DatabaseMigrations.MIGRATE_TO_V20,
DatabaseMigrations.MIGRATE_TO_V21,
DatabaseMigrations.MIGRATE_TO_V22,
DatabaseMigrations.MIGRATE_TO_V23,
DatabaseMigrations.MIGRATE_TO_V24,
DatabaseMigrations.MIGRATE_TO_V25,
DatabaseMigrations.MIGRATE_TO_V26,
DatabaseMigrations.MIGRATE_TO_V27,
DatabaseMigrations.MIGRATE_TO_V28,
DatabaseMigrations.MIGRATE_TO_V29,
DatabaseMigrations.MIGRATE_TO_V30,
DatabaseMigrations.MIGRATE_TO_V31,
DatabaseMigrations.MIGRATE_TO_V32,
DatabaseMigrations.MIGRATE_TO_V33,
DatabaseMigrations.MIGRATE_TO_V34,
DatabaseMigrations.MIGRATE_TO_V35,
DatabaseMigrations.MIGRATE_TO_V36,
DatabaseMigrations.MIGRATE_TO_V37,
DatabaseMigrations.MIGRATE_TO_V38,
DatabaseMigrations.MIGRATE_TO_V39,
DatabaseMigrations.MIGRATE_TO_V40
)
.addMigrations(*DatabaseMigrations.ALL)
.setQueryExecutor(Threads.database)
.addCallback(object: Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {

View file

@ -330,4 +330,28 @@ abstract class ConfigDao {
fun getAnnoyManualUnblockCounter() = getValueOfKeySync(ConfigurationItemType.AnnoyManualUnblockCounter).let { it?.toInt() ?: 0 }
fun setAnoyManualUnblockCounterSync(counter: Int) { updateValueSync(ConfigurationItemType.AnnoyManualUnblockCounter, counter.toString()) }
private val consentFlags: LiveData<Long> by lazy {
getValueOfKeyAsync(ConfigurationItemType.ConsentFlags).map {
it?.toLong(16) ?: 0
}
}
fun getConsentFlagsSync(): Long = getValueOfKeySync(ConfigurationItemType.ConsentFlags).let {
it?.toLong(16) ?: 0
}
fun isConsentFlagSetAsync(flags: Long) = consentFlags.map {
(it and flags) == flags
}.ignoreUnchanged()
fun setConsentFlagSync(flags: Long, enable: Boolean) {
updateValueSync(
ConfigurationItemType.ConsentFlags,
if (enable)
(getConsentFlagsSync() or flags).toString(16)
else
(getConsentFlagsSync() and (flags.inv())).toString(16)
)
}
}

View file

@ -101,6 +101,7 @@ enum class ConfigurationItemType {
CustomOrganizationName,
ServerApiLevel,
AnnoyManualUnblockCounter,
ConsentFlags,
}
object ConfigurationItemTypeUtil {
@ -128,6 +129,7 @@ object ConfigurationItemTypeUtil {
private const val CUSTOM_ORGANIZATION_NAME = 23
private const val SERVER_API_LEVEL = 24
private const val ANNOY_MANUAL_UNBLOCK_COUNTER = 25
private const val CONSENT_FLAGS = 26
val TYPES = listOf(
ConfigurationItemType.OwnDeviceId,
@ -153,7 +155,8 @@ object ConfigurationItemTypeUtil {
ConfigurationItemType.UpdateStatus,
ConfigurationItemType.CustomOrganizationName,
ConfigurationItemType.ServerApiLevel,
ConfigurationItemType.AnnoyManualUnblockCounter
ConfigurationItemType.AnnoyManualUnblockCounter,
ConfigurationItemType.ConsentFlags
)
fun serialize(value: ConfigurationItemType) = when(value) {
@ -181,6 +184,7 @@ object ConfigurationItemTypeUtil {
ConfigurationItemType.CustomOrganizationName -> CUSTOM_ORGANIZATION_NAME
ConfigurationItemType.ServerApiLevel -> SERVER_API_LEVEL
ConfigurationItemType.AnnoyManualUnblockCounter -> ANNOY_MANUAL_UNBLOCK_COUNTER
ConfigurationItemType.ConsentFlags -> CONSENT_FLAGS
}
fun parse(value: Int) = when(value) {
@ -208,6 +212,7 @@ object ConfigurationItemTypeUtil {
CUSTOM_ORGANIZATION_NAME -> ConfigurationItemType.CustomOrganizationName
SERVER_API_LEVEL -> ConfigurationItemType.ServerApiLevel
ANNOY_MANUAL_UNBLOCK_COUNTER -> ConfigurationItemType.AnnoyManualUnblockCounter
CONSENT_FLAGS -> ConfigurationItemType.ConsentFlags
else -> throw IllegalArgumentException()
}
}
@ -252,3 +257,7 @@ object ExperimentalFlags {
// private const val OBSOLETE_DISABLE_FG_APP_DETECTION_FALLBACK = 131072L
const val STRICT_OVERLAY_CHECKING = 0x40000L
}
object ConsentFlags {
const val APP_LIST_SYNC = 1L
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* 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
@ -28,7 +28,8 @@ data class DeviceRelatedData (
val isLocalMode: Boolean,
val hasValidDefaultUser: Boolean,
val temporarilyAllowedApps: Set<String>,
val experimentalFlags: Long
val experimentalFlags: Long,
val consentFlags: Long
): Observer {
companion object {
private val relatedTables = arrayOf(Table.ConfigurationItem, Table.Device, Table.User, Table.TemporarilyAllowedApp)
@ -41,6 +42,7 @@ data class DeviceRelatedData (
val hasValidDefaultUser = database.user().getUserByIdSync(deviceEntry.defaultUser) != null
val temporarilyAllowedApps = database.temporarilyAllowedApp().getTemporarilyAllowedAppsSync().toSet()
val experimentalFlags = database.config().getExperimentalFlagsSync()
val consentFlags = database.config().getConsentFlagsSync()
DeviceRelatedData(
deviceEntry = deviceEntry,
@ -48,7 +50,8 @@ data class DeviceRelatedData (
isLocalMode = isLocalMode,
hasValidDefaultUser = hasValidDefaultUser,
temporarilyAllowedApps = temporarilyAllowedApps,
experimentalFlags = experimentalFlags
experimentalFlags = experimentalFlags,
consentFlags = consentFlags
).also {
database.registerWeakObserver(relatedTables, WeakReference(it))
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
* 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
@ -238,3 +238,48 @@ fun <T1, T2, T3, T4> mergeLiveDataWaitForValues(d1: LiveData<T1>, d2: LiveData<T
return result
}
fun <T1, T2, T3, T4, T5> mergeLiveDataWaitForValues(d1: LiveData<T1>, d2: LiveData<T2>, d3: LiveData<T3>, d4: LiveData<T4>, d5: LiveData<T5>): LiveData<FiveTuple<T1, T2, T3, T4, T5>> {
val result = MediatorLiveData<FiveTuple<T1, T2, T3, T4, T5>>()
var state = FiveTuple<Option<T1>, Option<T2>, Option<T3>, Option<T4>, Option<T5>>(Option.None(), Option.None(), Option.None(), Option.None(), Option.None())
fun update() {
val (a, b, c, d, e) = state
if (a is Option.Some && b is Option.Some && c is Option.Some && d is Option.Some && e is Option.Some) {
result.value = FiveTuple(a.value, b.value, c.value, d.value, e.value)
}
}
result.addSource(d1) {
state = state.copy(first = Option.Some(it))
update()
}
result.addSource(d2) {
state = state.copy(second = Option.Some(it))
update()
}
result.addSource(d3) {
state = state.copy(third = Option.Some(it))
update()
}
result.addSource(d4) {
state = state.copy(forth = Option.Some(it))
update()
}
result.addSource(d5) {
state = state.copy(fifth = Option.Some(it))
update()
}
return result
}

View file

@ -54,7 +54,7 @@ class AppLogic(
}
}.ignoreUnchanged()
val deviceEntryIfEnabled = enable.switchMap {
val deviceEntryIfEnabled: LiveData<Device?> = enable.switchMap {
if (it == null || it == false) {
liveDataFromNullableValue(null as Device?)
} else {
@ -95,8 +95,9 @@ class AppLogic(
websocketClientCreator = websocketClientCreator
)
val syncAppsLogic = SyncInstalledAppsLogic(this)
init {
SyncInstalledAppsLogic(this)
WatchdogLogic(this)
}

View file

@ -25,6 +25,7 @@ import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.coroutines.runAsyncExpectForever
import io.timelimit.android.data.model.App
import io.timelimit.android.data.model.AppActivity
import io.timelimit.android.data.model.ConsentFlags
import io.timelimit.android.data.model.UserType
import io.timelimit.android.integration.platform.ProtectionLevel
import io.timelimit.android.livedata.*
@ -45,17 +46,53 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
requestSync.value = true
}
private val deviceStateLive = mergeLiveDataWaitForValues(
appLogic.deviceEntryIfEnabled,
appLogic.database.config().isConsentFlagSetAsync(ConsentFlags.APP_LIST_SYNC),
appLogic.database.config().getDeviceAuthTokenAsync().map { it.isEmpty() },
appLogic.deviceUserEntry,
appLogic.deviceEntryIfEnabled.switchMap { deviceEntry ->
val defaultUser = deviceEntry?.defaultUser
if (defaultUser.isNullOrEmpty()) liveDataFromNullableValue(null)
else appLogic.database.user().getUserByIdLive(defaultUser)
}
).map { (deviceEntry, hasSyncConsent, isLocalMode, deviceUser, deviceDefaultUser) ->
deviceEntry?.let { device ->
DeviceState(
id = device.id,
isCurrentUserChild = deviceUser?.type == UserType.Child,
isDefaultUserChild = deviceDefaultUser?.type == UserType.Child,
enableActivityLevelBlocking = device.enableActivityLevelBlocking,
isDeviceOwner = device.currentProtectionLevel == ProtectionLevel.DeviceOwner,
hasSyncConsent = hasSyncConsent,
isLocalMode = isLocalMode
)
}
}.ignoreUnchanged()
val shouldAskForConsent = deviceStateLive.map { it?.shouldAskForConsent ?: false }.ignoreUnchanged()
private fun getDeviceStateSync(): DeviceState? {
val userAndDeviceData = appLogic.database.derivedDataDao().getUserAndDeviceRelatedDataSync() ?: return null
val deviceRelatedData = userAndDeviceData.deviceRelatedData
val device = deviceRelatedData.deviceEntry
val defaultUser = if (device.defaultUser.isNotEmpty()) appLogic.database.user().getUserByIdSync(device.defaultUser) else null
return DeviceState(
id = device.id,
isCurrentUserChild = userAndDeviceData.userRelatedData?.user?.type == UserType.Child,
isDefaultUserChild = defaultUser?.type == UserType.Child,
enableActivityLevelBlocking = device.enableActivityLevelBlocking,
isDeviceOwner = device.currentProtectionLevel == ProtectionLevel.DeviceOwner,
hasSyncConsent = deviceRelatedData.consentFlags and ConsentFlags.APP_LIST_SYNC == ConsentFlags.APP_LIST_SYNC,
isLocalMode = deviceRelatedData.isLocalMode
)
}
init {
appLogic.platformIntegration.installedAppsChangeListener = Runnable { requestSync() }
appLogic.deviceEntryIfEnabled.map { device ->
device?.let { DeviceState(
id = device.id,
currentUserId = device.currentUserId,
defaultUser = device.defaultUser,
enableActivityLevelBlocking = device.enableActivityLevelBlocking,
isDeviceOwner = device.currentProtectionLevel == ProtectionLevel.DeviceOwner
) }
}.ignoreUnchanged().observeForever { requestSync() }
deviceStateLive.observeForever { requestSync() }
runAsyncExpectForever { syncLoop() }
}
@ -88,26 +125,17 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
private suspend fun doSyncNow() {
doSyncLock.withLock {
val deviceEntry = appLogic.deviceEntryIfEnabled.waitForNullableValue()
val deviceState = Threads.database.executeAndWait { getDeviceStateSync() } ?: return
if (deviceEntry == null) {
return
}
if (appLogic.database.config().getDeviceAuthTokenAsync().waitForNullableValue().isNullOrEmpty()) {
if (deviceState.isLocalMode) {
// local mode -> sync always
} else {
// connected mode -> don't sync always
val userEntry = appLogic.deviceUserEntry.waitForNullableValue()
val defaultUserEntry = appLogic.database.user().getUserByIdLive(deviceEntry.defaultUser).waitForNullableValue()
if (userEntry?.type != UserType.Child && defaultUserEntry?.type != UserType.Child) {
return@withLock
}
if (!deviceState.hasSyncConsent) return@withLock
if (!deviceState.hasAnyChildUser) return@withLock
}
val deviceId = deviceEntry.id
val deviceId = deviceState.id
val currentlyInstalledApps = getCurrentApps(deviceId)
@ -153,7 +181,7 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
run {
fun buildKey(activity: AppActivity) = "${activity.appPackageName}:${activity.activityClassName}"
val currentlyInstalled = if (deviceEntry.enableActivityLevelBlocking)
val currentlyInstalled = if (deviceState.enableActivityLevelBlocking)
Threads.backgroundOSInteraction.executeAndWait {
val realActivities = appLogic.platformIntegration.getLocalAppActivities(deviceId = deviceId)
val dummyActivities = currentlyInstalledApps.keys.map { packageName ->
@ -220,9 +248,14 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
internal data class DeviceState(
val id: String,
val currentUserId: String,
val defaultUser: String,
val isCurrentUserChild: Boolean,
val isDefaultUserChild: Boolean,
val enableActivityLevelBlocking: Boolean,
val isDeviceOwner: Boolean
)
val isDeviceOwner: Boolean,
val hasSyncConsent: Boolean,
val isLocalMode: Boolean
) {
val hasAnyChildUser = isCurrentUserChild || isDefaultUserChild
val shouldAskForConsent = hasAnyChildUser && !isLocalMode && !hasSyncConsent
}
}

View file

@ -0,0 +1,59 @@
/*
* 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.ui.consent
import android.app.Dialog
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import io.timelimit.android.BuildConfig
import io.timelimit.android.R
import io.timelimit.android.async.Threads
import io.timelimit.android.data.model.ConsentFlags
import io.timelimit.android.extensions.showSafe
import io.timelimit.android.logic.DefaultAppLogic
class SyncAppListConsentDialogFragment: DialogFragment() {
companion object {
private const val DIALOG_TAG = "SyncAppListConsentDialogFragment"
private const val LOG_TAG = "SyncAppListConsent"
fun newInstance() = SyncAppListConsentDialogFragment()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = AlertDialog.Builder(requireContext(), theme)
.setTitle(R.string.consent_app_list_sync_dialog_title)
.setMessage(R.string.consent_app_list_sync_dialog_text)
.setNegativeButton(R.string.generic_reject, null)
.setPositiveButton(R.string.generic_accept) { _, _ ->
val database = DefaultAppLogic.with(requireContext()).database
Threads.database.execute {
try {
database.config().setConsentFlagSync(ConsentFlags.APP_LIST_SYNC, true)
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.w(LOG_TAG, "Could not save consent", ex)
}
}
}
}
.create()
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* 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
@ -23,6 +23,7 @@ import androidx.recyclerview.widget.RecyclerView
import io.timelimit.android.R
import io.timelimit.android.data.model.Category
import io.timelimit.android.databinding.AddItemViewBinding
import io.timelimit.android.databinding.AppListSyncPermissionRequestCardBinding
import io.timelimit.android.databinding.CategoryRichCardBinding
import io.timelimit.android.databinding.IntroCardBinding
import io.timelimit.android.ui.util.DateUtil
@ -35,6 +36,7 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
private const val TYPE_ADD = 1
private const val TYPE_INTRO = 2
private const val TYPE_MANIPULATION_WARNING = 3
private const val TYPE_APP_LIST_BANNER = 4
}
var categories: List<ManageChildCategoriesListItem>? by Delegates.observable(null as List<ManageChildCategoriesListItem>?) { _, _, _ -> notifyDataSetChanged() }
@ -53,6 +55,7 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
CreateCategoryItem -> item.hashCode()
CategoriesIntroductionHeader -> item.hashCode()
ManipulationWarningCategoryItem -> item.hashCode()
ManageChildCategoriesListItem.SyncAppListBanner -> item.hashCode()
}.toLong()
}
@ -61,6 +64,7 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
CreateCategoryItem -> TYPE_ADD
CategoriesIntroductionHeader -> TYPE_INTRO
ManipulationWarningCategoryItem -> TYPE_MANIPULATION_WARNING
ManageChildCategoriesListItem.SyncAppListBanner -> TYPE_APP_LIST_BANNER
}
override fun getItemCount() = categories?.size ?: 0
@ -104,6 +108,13 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
.inflate(R.layout.manage_child_manipulation_warning, parent, false)
)
TYPE_APP_LIST_BANNER ->
SyncAppListViewHolder(
AppListSyncPermissionRequestCardBinding.inflate(LayoutInflater.from(parent.context), parent, false).also {
it.detailButton.setOnClickListener { handlers?.onRequestAppListSyncConsentClicked() }
}.root
)
else -> throw IllegalStateException()
}
@ -163,6 +174,9 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
ManipulationWarningCategoryItem -> {
// nothing to do
}
ManageChildCategoriesListItem.SyncAppListBanner -> {
// nothing to do
}
}.let { }
}
}
@ -171,10 +185,12 @@ sealed class ViewHolder(view: View): RecyclerView.ViewHolder(view)
class AddViewHolder(view: View): ViewHolder(view)
class IntroViewHolder(view: View): ViewHolder(view)
class ManipulationWarningViewHolder(view: View): ViewHolder(view)
class SyncAppListViewHolder(view: View): ViewHolder(view)
class ItemViewHolder(val binding: CategoryRichCardBinding): ViewHolder(binding.root)
interface Handlers {
fun onCategoryClicked(category: Category)
fun onCreateCategoryClicked()
fun onCategorySwitched(category: CategoryItem, isChecked: Boolean): Boolean
fun onRequestAppListSyncConsentClicked()
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* 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
@ -17,7 +17,10 @@ package io.timelimit.android.ui.manage.child.category
import io.timelimit.android.data.model.Category
sealed class ManageChildCategoriesListItem
sealed class ManageChildCategoriesListItem {
object SyncAppListBanner: ManageChildCategoriesListItem()
}
object CategoriesIntroductionHeader: ManageChildCategoriesListItem()
object CreateCategoryItem: ManageChildCategoriesListItem()
object ManipulationWarningCategoryItem: ManageChildCategoriesListItem()

View file

@ -37,6 +37,7 @@ import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.sync.actions.UpdateCategoryDisableLimitsAction
import io.timelimit.android.sync.actions.UpdateCategorySortingAction
import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction
import io.timelimit.android.ui.consent.SyncAppListConsentDialogFragment
import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs
@ -58,7 +59,7 @@ class ManageChildCategoriesFragment : Fragment() {
private val model: ManageChildCategoriesModel by viewModels()
private lateinit var binding: RecyclerFragmentBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = RecyclerFragmentBinding.inflate(inflater, container, false)
return binding.root
@ -136,6 +137,10 @@ class ManageChildCategoriesFragment : Fragment() {
false
}
}
override fun onRequestAppListSyncConsentClicked() {
SyncAppListConsentDialogFragment.newInstance().show(parentFragmentManager)
}
}
binding.recycler.adapter = adapter

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
* 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
@ -144,23 +144,25 @@ class ManageChildCategoriesModel(application: Application): AndroidViewModel(app
private val hasShownHint = logic.database.config().wereHintsShown(HintsToShow.CATEGORIES_INTRODUCTION)
private val listContentStep1 = hasShownHint.switchMap { hasShownHint ->
categoryItems.map { categoryItems ->
if (hasShownHint) {
categoryItems + listOf(CreateCategoryItem)
} else {
listOf(CategoriesIntroductionHeader) + categoryItems + listOf(CreateCategoryItem)
}
}
}
private val showSyncConsentBanner = logic.syncAppsLogic.shouldAskForConsent
val listContent = hasNotSuppressedChildDeviceManipulation.switchMap { hasChildDevicesWithManipulation ->
listContentStep1.map { listContent ->
if (hasChildDevicesWithManipulation) {
listOf(ManipulationWarningCategoryItem) + listContent
} else {
listContent
}
}
val listContent = mergeLiveDataWaitForValues(
categoryItems,
hasShownHint,
showSyncConsentBanner,
hasNotSuppressedChildDeviceManipulation
).map { (categoryItems, hasShownHint, showSyncConsentBanner, hasChildDevicesWithManipulation) ->
val headers1 = emptyList<ManageChildCategoriesListItem>()
val headers2 = if (hasShownHint) headers1
else headers1 + listOf(CategoriesIntroductionHeader)
val headers3 = if (showSyncConsentBanner) headers2 + listOf(ManageChildCategoriesListItem.SyncAppListBanner)
else headers2
val headers4 = if (hasChildDevicesWithManipulation) headers3 + listOf(ManipulationWarningCategoryItem)
else headers3
headers4 + categoryItems + listOf(CreateCategoryItem)
}
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
* 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
@ -24,10 +24,10 @@ import android.view.ViewGroup
import android.widget.CheckBox
import androidx.appcompat.widget.AppCompatRadioButton
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation
import io.timelimit.android.R
import io.timelimit.android.coroutines.runAsync
@ -60,6 +60,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
private const val STATUS_ALLOWED_APPS_CATEGORY = "c"
}
private val model: SetupDeviceModel by viewModels()
private val selectedUser = MutableLiveData<String>()
private val selectedAppsToNotWhitelist = mutableSetOf<String>()
private var allowedAppsCategory = ""
@ -89,11 +90,10 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
outState.putString(STATUS_ALLOWED_APPS_CATEGORY, allowedAppsCategory)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val binding = FragmentSetupDeviceBinding.inflate(inflater, container, false)
val logic = DefaultAppLogic.with(requireContext())
val activity = activity as ActivityViewModelHolder
val model = ViewModelProviders.of(this).get(SetupDeviceModel::class.java)
val navigation = Navigation.findNavController(container!!)
binding.needsParent.authBtn.setOnClickListener {
@ -136,7 +136,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
}
})
logic.database.user().getAllUsersLive().observe(this, Observer { users ->
logic.database.user().getAllUsersLive().observe(viewLifecycleOwner) { users ->
// ID to label
val items = mutableListOf<Pair<String, String>>()
@ -150,7 +150,9 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
// select the first item if nothing is selected currently
if (items.find { (id) -> id == selectedUser.value } == null) {
selectedUser.value = items.first().first
items.firstOrNull()?.first?.let {
selectedUser.value = it
}
}
// build the views
@ -166,7 +168,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
binding.selectUserRadioGroup.removeAllViews()
views.forEach { view -> binding.selectUserRadioGroup.addView(view) }
views.find { it.tag == selectedUser.value }?.isChecked = true
})
}
val isNewUser = selectedUser.map { NEW_USER.contains(it) }
val isParentUser = selectedUser.switchMap {
@ -181,8 +183,8 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
}
}
isNewUser.observe(this, Observer { binding.isAddingNewUser = it })
isParentUser.observe(this, Observer { binding.isAddingChild = !it })
isNewUser.observe(viewLifecycleOwner) { binding.isAddingNewUser = it }
isParentUser.observe(viewLifecycleOwner) { binding.isAddingChild = !it }
val categoriesOfTheSelectedUser = selectedUser.switchMap { user ->
if (NEW_USER.contains(user)) {
@ -204,7 +206,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
recommendWhitelistLocalApps.filterNot { app -> assignedApps.contains(app.packageName) }
}
appsToWhitelist.observe(this, Observer { apps ->
appsToWhitelist.observe(viewLifecycleOwner) { apps ->
binding.areThereAnyApps = apps.isNotEmpty()
binding.suggestedAllowedApps.removeAllViews()
@ -225,9 +227,9 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
}
)
}
})
}
categoriesOfTheSelectedUser.observe(this, Observer { categories ->
categoriesOfTheSelectedUser.observe(viewLifecycleOwner) { categories ->
// id to title
val items = mutableListOf<Pair<String, String>>()
@ -241,7 +243,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
} else {
if (items.find { (id) -> id == allowedAppsCategory } == null) {
// use the one with the lowest blocked times
allowedAppsCategory = categories.sortedBy { it.blockedMinutesInWeek.dataNotToModify.cardinality() }.first().id
allowedAppsCategory = categories.minByOrNull { it.blockedMinutesInWeek.dataNotToModify.cardinality() }!!.id
}
binding.areThereAnyCategories = true
@ -258,7 +260,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
views.forEach { view -> binding.allowedAppsCategory.addView(view) }
views.find { it.tag == allowedAppsCategory }?.isChecked = true
}
})
}
val selectedName = MutableLiveData<String>().apply { value = binding.newUserName.text.toString() }
binding.newUserName.addTextChangedListener(object: TextWatcher {
@ -281,9 +283,9 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
)
val validationOfAll = (validationOfName.and(validationOfPassword)).or(isNewUser.invert())
validationOfAll.observe(this, Observer { binding.confirmBtn.isEnabled = it })
validationOfAll.observe(viewLifecycleOwner) { binding.confirmBtn.isEnabled = it }
isPasswordRequired.observe(this, Observer { binding.setPasswordView.allowNoPassword.value = !it })
isPasswordRequired.observe(viewLifecycleOwner) { binding.setPasswordView.allowNoPassword.value = !it }
ManageDeviceBackgroundSync.bind(
view = binding.backgroundSync,
@ -302,7 +304,8 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
appsToNotWhitelist = selectedAppsToNotWhitelist,
model = activity.getActivityViewModel(),
networkTime = SetupNetworkTimeVerification.readSelection(binding.networkTimeVerification),
enableUpdateChecks = binding.update.enableSwitch.isChecked
enableUpdateChecks = binding.update.enableSwitch.isChecked,
enableAppListSync = binding.appListSync.enableSwitch.isChecked && (isParentUser.value == false)
)
}

View file

@ -16,13 +16,18 @@
package io.timelimit.android.ui.setup.device
import android.app.Application
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import io.timelimit.android.BuildConfig
import io.timelimit.android.R
import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.IdGenerator
import io.timelimit.android.data.model.AppRecommendation
import io.timelimit.android.data.model.ConsentFlags
import io.timelimit.android.data.model.NetworkTime
import io.timelimit.android.data.model.UserType
import io.timelimit.android.livedata.castDown
@ -35,6 +40,10 @@ import io.timelimit.android.ui.user.create.DefaultCategories
import io.timelimit.android.update.UpdateUtil
class SetupDeviceModel(application: Application): AndroidViewModel(application) {
companion object {
private const val LOG_TAG = "SetupDeviceModel"
}
private val logic = DefaultAppLogic.with(application)
private val statusInternal = MutableLiveData<SetupDeviceModelStatus>().apply { value = SetupDeviceModelStatus.Ready }
@ -48,7 +57,8 @@ class SetupDeviceModel(application: Application): AndroidViewModel(application)
appsToNotWhitelist: Set<String>,
model: ActivityViewModel,
networkTime: NetworkTime,
enableUpdateChecks: Boolean
enableUpdateChecks: Boolean,
enableAppListSync: Boolean
) {
if (statusInternal.value != SetupDeviceModelStatus.Ready) {
return
@ -57,123 +67,158 @@ class SetupDeviceModel(application: Application): AndroidViewModel(application)
statusInternal.value = SetupDeviceModelStatus.Working
runAsync {
val actions = mutableListOf<ParentAction>()
var realUserId = userId
var realAllowedAppsCategory = allowedAppsCategory
val defaultCategories = DefaultCategories.with(getApplication())
try {
val actions = mutableListOf<ParentAction>()
var realUserId = userId
var realAllowedAppsCategory = allowedAppsCategory
val defaultCategories = DefaultCategories.with(getApplication())
val isUserAnChild = when (userId) {
SetupDeviceFragment.NEW_PARENT -> {
// generate user id
realUserId = IdGenerator.generateId()
val isUserAnChild = when (userId) {
SetupDeviceFragment.NEW_PARENT -> {
// generate user id
realUserId = IdGenerator.generateId()
// create parent
actions.add(AddUserAction(
userId = realUserId,
name = username,
timeZone = logic.timeApi.getSystemTimeZone().id,
userType = UserType.Parent,
password = ParentPassword.createCoroutine(password)
))
// create parent
actions.add(
AddUserAction(
userId = realUserId,
name = username,
timeZone = logic.timeApi.getSystemTimeZone().id,
userType = UserType.Parent,
password = ParentPassword.createCoroutine(password)
)
)
false
false
}
SetupDeviceFragment.NEW_CHILD -> {
// generate user id
realUserId = IdGenerator.generateId()
// create child
actions.add(
AddUserAction(
userId = realUserId,
name = username,
timeZone = logic.timeApi.getSystemTimeZone().id,
userType = UserType.Child,
password = if (password.isEmpty()) null else ParentPassword.createCoroutine(
password
)
)
)
// create default categories
realAllowedAppsCategory = IdGenerator.generateId()
val allowedGamesCategory = IdGenerator.generateId()
actions.add(
CreateCategoryAction(
childId = realUserId,
categoryId = realAllowedAppsCategory,
title = defaultCategories.allowedAppsTitle
)
)
actions.add(
CreateCategoryAction(
childId = realUserId,
categoryId = allowedGamesCategory,
title = defaultCategories.allowedGamesTitle
)
)
defaultCategories.generateGamesTimeLimitRules(allowedGamesCategory)
.forEach { rule ->
actions.add(CreateTimeLimitRuleAction(rule))
}
true
}
else -> {
logic.database.user().getUserByIdLive(userId)
.waitForNullableValue()!!.type == UserType.Child
}
}
SetupDeviceFragment.NEW_CHILD -> {
// generate user id
realUserId = IdGenerator.generateId()
// create child
actions.add(AddUserAction(
userId = realUserId,
name = username,
timeZone = logic.timeApi.getSystemTimeZone().id,
userType = UserType.Child,
password = if (password.isEmpty()) null else ParentPassword.createCoroutine(password)
))
if (isUserAnChild) {
if (realAllowedAppsCategory == "") {
// create allowed apps category if none was specified and overwrite its id
realAllowedAppsCategory = IdGenerator.generateId()
// create default categories
realAllowedAppsCategory = IdGenerator.generateId()
val allowedGamesCategory = IdGenerator.generateId()
actions.add(CreateCategoryAction(
childId = realUserId,
categoryId = realAllowedAppsCategory,
title = defaultCategories.allowedAppsTitle
))
actions.add(CreateCategoryAction(
childId = realUserId,
categoryId = allowedGamesCategory,
title = defaultCategories.allowedGamesTitle
))
defaultCategories.generateGamesTimeLimitRules(allowedGamesCategory).forEach { rule ->
actions.add(CreateTimeLimitRuleAction(rule))
actions.add(
CreateCategoryAction(
childId = realUserId,
categoryId = realAllowedAppsCategory,
title = defaultCategories.allowedAppsTitle
)
)
}
true
}
else -> {
logic.database.user().getUserByIdLive(userId).waitForNullableValue()!!.type == UserType.Child
}
}
val alreadyAssignedApps = Threads.database.executeAndWait {
logic.database.categoryApp().getCategoryAppsByUserIdSync(realUserId)
.filter { it.appSpecifier.deviceId == null }
.map { it.appSpecifier.packageName }
.toSet()
}
if (isUserAnChild) {
if (realAllowedAppsCategory == "") {
// create allowed apps category if none was specified and overwrite its id
realAllowedAppsCategory = IdGenerator.generateId()
// add allowed apps
val allowedAppsPackages =
logic.platformIntegration.getLocalApps(IdGenerator.generateId())
.filter { app -> app.recommendation == AppRecommendation.Whitelist }
.map { app -> app.packageName }
.toMutableSet().apply {
removeAll(appsToNotWhitelist)
removeAll(alreadyAssignedApps)
}.toList()
actions.add(CreateCategoryAction(
childId = realUserId,
categoryId = realAllowedAppsCategory,
title = defaultCategories.allowedAppsTitle
))
if (allowedAppsPackages.isNotEmpty()) {
actions.add(
AddCategoryAppsAction(
categoryId = realAllowedAppsCategory,
packageNames = allowedAppsPackages
)
)
}
}
val alreadyAssignedApps = Threads.database.executeAndWait {
logic.database.categoryApp().getCategoryAppsByUserIdSync(realUserId)
.filter { it.appSpecifier.deviceId == null }
.map { it.appSpecifier.packageName }
.toSet()
// apply the network time mode
val deviceId = logic.deviceId.waitForNullableValue()!!
actions.add(
UpdateNetworkTimeVerificationAction(
deviceId = deviceId,
mode = networkTime
)
)
// assign user to this device
actions.add(
SetDeviceUserAction(
deviceId = deviceId,
userId = realUserId
)
)
// configure update check
UpdateUtil.setEnableChecks(getApplication(), enableUpdateChecks)
Threads.database.executeAndWait {
DefaultAppLogic.with(getApplication()).database.config().setConsentFlagSync(ConsentFlags.APP_LIST_SYNC, enableAppListSync)
}
// add allowed apps
val allowedAppsPackages = logic.platformIntegration.getLocalApps(IdGenerator.generateId())
.filter { app -> app.recommendation == AppRecommendation.Whitelist }
.map { app -> app.packageName }
.toMutableSet().apply {
removeAll(appsToNotWhitelist)
removeAll(alreadyAssignedApps)
}.toList()
if (allowedAppsPackages.isNotEmpty()) {
actions.add(AddCategoryAppsAction(
categoryId = realAllowedAppsCategory,
packageNames = allowedAppsPackages
))
if (model.tryDispatchParentActions(actions)) {
statusInternal.value = SetupDeviceModelStatus.Done
} else {
statusInternal.value = SetupDeviceModelStatus.Ready
}
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "could not setup device", ex)
}
}
// apply the network time mode
val deviceId = logic.deviceId.waitForNullableValue()!!
Toast.makeText(getApplication(), R.string.error_general, Toast.LENGTH_SHORT).show()
actions.add(UpdateNetworkTimeVerificationAction(
deviceId = deviceId,
mode = networkTime
))
// assign user to this device
actions.add(SetDeviceUserAction(
deviceId = deviceId,
userId = realUserId
))
// configure update check
UpdateUtil.setEnableChecks(getApplication(), enableUpdateChecks)
if (model.tryDispatchParentActions(actions)) {
statusInternal.value = SetupDeviceModelStatus.Done
} else {
statusInternal.value = SetupDeviceModelStatus.Ready
}
}

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardUseCompatPadding="true">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp">
<TextView
android:textAppearance="?android:textAppearanceLarge"
android:text="@string/consent_app_list_sync_dialog_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/consent_app_list_sync_card_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:layout_marginTop="16dp"
android:id="@+id/detail_button"
android:layout_gravity="end"
android:text="@string/generic_show_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</layout>

View file

@ -1,5 +1,5 @@
<!--
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
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.
@ -203,6 +203,10 @@
<include android:id="@+id/network_time_verification"
layout="@layout/setup_network_time_verification" />
<include android:id="@+id/app_list_sync"
android:visibility="@{isAddingChild ? View.VISIBLE : View.GONE}"
layout="@layout/setup_device_app_list_sync" />
<include android:id="@+id/background_sync"
layout="@layout/manage_device_background_sync_view" />

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardUseCompatPadding="true">
<LinearLayout
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:textAppearance="?android:textAppearanceLarge"
android:text="@string/consent_app_list_sync_dialog_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/consent_app_list_sync_dialog_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.appcompat.widget.SwitchCompat
android:text="@string/consent_app_list_sync_switch_label"
android:id="@+id/enable_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</layout>

View file

@ -30,6 +30,8 @@
<string name="generic_yes">Ja</string>
<string name="generic_skip">Überspringen</string>
<string name="generic_show_details">Details anzeigen</string>
<string name="generic_accept">Akzeptieren</string>
<string name="generic_reject">Ablehnen</string>
<string name="generic_swipe_to_dismiss">Sie können diesen Hinweis entfernen, indem Sie ihn zur Seite wischen</string>
<string name="generic_runtime_permission_rejected">Berechtigung abgelehnt; Sie können die Berechtigungen in den Systemeinstellungen verwalten</string>
@ -275,7 +277,8 @@
und daher auch keine Apps auf Geräten, die dem Benutzer zugeordnet wurden
</string>
<string name="category_apps_add_empty_child_devices_no_apps">Es gibt mindestens ein Gerät für diesen Benutzer,
aber es sind keine Apps von diesem Gerät bekannt. Ist die Benutzerzuordnung am entsprechenden Gerät angekommen?
aber es sind keine Apps von diesem Gerät bekannt. Ist die Benutzerzuordnung am entsprechenden Gerät angekommen
und wurde dort die Synchronisation der App-Liste aktiviert?
</string>
<string name="category_apps_add_empty_all_devices_no_apps_no_childs">Es gibt kein Gerät, das einem Kind zugeordnet wurde.
Daher wurden keine Apps erfasst und dadurch sind jetzt keine Apps bekannt, die zugerodnet werden können.
@ -1391,7 +1394,7 @@
<string name="setup_privacy_connected_title">Vernetzung und Datenschutz</string>
<string name="setup_privacy_connected_text_general_intro">Für den vernetzten Modus
gibt es eine Zentrale - den Server. Auf diesen werden die Einstellungen,
die Nutzungsdauern und die Gerätezustände (erteilte Berechtigungen, installierte Apps und die App- und System-Version)
die Nutzungsdauern und die Gerätezustände (erteilte Berechtigungen und die TimeLimit- und System-Version)
gespeichert.
</string>
<string name="setup_privacy_connected_text_default_server">Sie verwenden - da Sie Nichts anderes gewählt haben -
@ -1404,6 +1407,17 @@
verhindern, indem Sie den lokalen Modus verwenden.
</string>
<string name="consent_app_list_sync_dialog_title">App-Listen-Synchronisation</string>
<string name="consent_app_list_sync_dialog_text">TimeLimit möchte die Liste
der installierten Apps synchronisieren, um das Freigeben von Apps mittels Fernsteuerung von anderen
verknüpften Geräten aus zu ermöglichen. Dafür wird die Liste auf dem TimeLimit-Server
gespeichert. Diese Daten werden für keine anderen Zwecke verwendet.
Alternativ können Sie auch den lokalen Modus verwenden.
</string>
<string name="consent_app_list_sync_switch_label">Synchronisation aktivieren</string>
<string name="consent_app_list_sync_card_text">TimeLimit möchte die Liste der installierten Apps
für die Fernsteuerungsfunktion synchronisieren.</string>
<string name="setup_remote_child_code_invalid">Dieser Code ist ungültig</string>
<string name="setup_remote_child_title">Dieses Gerät verknüpfen</string>
<string name="setup_remote_child_text">

View file

@ -33,6 +33,8 @@
<string name="generic_yes">Yes</string>
<string name="generic_skip">Skip</string>
<string name="generic_show_details">Show details</string>
<string name="generic_accept">Accept</string>
<string name="generic_reject">Reject</string>
<string name="generic_swipe_to_dismiss">Swipe to the side to remove this message</string>
<string name="generic_runtime_permission_rejected">Permission rejected; You can manage permissions in the system settings</string>
@ -315,7 +317,8 @@
</string>
<string name="category_apps_add_empty_child_devices_no_apps">
There is a device assigned to this user, but there are no know Apps.
Did the device receive the user assignment?
Did the device receive the user assignment and was the App List
Synchronisation enabled at this device?
</string>
<string name="category_apps_add_empty_all_devices_no_apps_no_childs">There is no device with any child assigned.
Due to that, no Apps were recorded and thus no assignable Apps are known.
@ -1427,7 +1430,7 @@
<string name="setup_privacy_connected_title">Networking and Privacy</string>
<string name="setup_privacy_connected_text_general_intro">For the connected mode,
there is a central unit - the server. This server saves the settings, the usage durations
and the status of the devices (granted permissions, installed Apps, App and OS version).
and the status of the devices (granted permissions, TimeLimit version and OS version).
</string>
<string name="setup_privacy_connected_text_default_server">Because you have not selected anything different,
you are using the default server which is provided by the developer of the App.
@ -1440,6 +1443,15 @@
the data transmission by using the local mode.
</string>
<string name="consent_app_list_sync_dialog_title">App List Synchronization</string>
<string name="consent_app_list_sync_dialog_text">TimeLimit would like to sync the
list of the installed Apps to make it possible to allow Apps remotely. For this,
the list is saved at the TimeLimit server. This data is not used for any other purposes.
Alternatively, you can use the local mode.
</string>
<string name="consent_app_list_sync_switch_label">Enable Synchronization</string>
<string name="consent_app_list_sync_card_text">TimeLimit would like to sync the list of installed Apps for the remote control.</string>
<string name="setup_remote_child_code_invalid">This code is invalid</string>
<string name="setup_remote_child_title">Link this device</string>
<string name="setup_remote_child_text">