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 import io.timelimit.android.extensions.MinuteOfDay
object DatabaseMigrations { object DatabaseMigrations {
val MIGRATE_TO_V2 = object: Migration(1, 2) { private val MIGRATE_TO_V2 = object: Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE device ADD COLUMN did_report_uninstall INTEGER NOT NULL DEFAULT 0") 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE device ADD COLUMN is_user_kept_signed_in INTEGER NOT NULL DEFAULT 0") 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `category_for_not_assigned_apps` TEXT NOT NULL DEFAULT \"\"") 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `parent_category_id` TEXT NOT NULL DEFAULT \"\"") 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `show_device_connected` INTEGER NOT NULL DEFAULT 0") 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) { 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` TEXT NOT NULL DEFAULT \"\"")
database.execSQL("ALTER TABLE `device` ADD COLUMN `default_user_timeout` INTEGER NOT NULL DEFAULT 0") 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) { override fun migrate(database: SupportSQLiteDatabase) {
// this is empty // 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) { 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 `did_reboot` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE `device` ADD COLUMN `consider_reboot_manipulation` 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) { override fun migrate(database: SupportSQLiteDatabase) {
// this is empty // 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `mail_notification_flags` INTEGER NOT NULL DEFAULT 0") 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `block_all_notifications` INTEGER NOT NULL DEFAULT 0") 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) { 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 `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\"") 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) { 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 `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") 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) { 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("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") 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) { override fun migrate(database: SupportSQLiteDatabase) {
// this is empty // 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) { 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`))") 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `q_or_later` INTEGER NOT NULL DEFAULT 0") 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") 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) { override fun migrate(database: SupportSQLiteDatabase) {
// this is empty // 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) { 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)") 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `had_manipulation_flags` INTEGER NOT NULL DEFAULT 0") 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `blocked_times` TEXT NOT NULL DEFAULT \"\"") 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) { 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_charging` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE `category` ADD COLUMN `min_battery_mobile` 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `temporarily_blocked_end_time` INTEGER NOT NULL DEFAULT 0") 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `sort` INTEGER NOT NULL DEFAULT 0") 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) { override fun migrate(database: SupportSQLiteDatabase) {
// this is empty // 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `extra_time_day` INTEGER NOT NULL DEFAULT -1") 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) { 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 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`)") 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) { 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 `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}") 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0") 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) { 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 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`)") 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) { 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 )") 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `disable_limits_until` INTEGER NOT NULL DEFAULT 0") 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) { 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("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 ''") 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `per_day` INTEGER NOT NULL DEFAULT 0") 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `user_limit_login_category` ADD COLUMN pre_block_duration INTEGER NOT NULL DEFAULT 0") 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `category` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0") 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) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE category ADD COLUMN block_notification_delay INTEGER NOT NULL DEFAULT 0") 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) { override fun migrate(database: SupportSQLiteDatabase) {
// nothing to do, there was just a new config item type added // 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) { 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 )") 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.InvalidationTracker
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import io.timelimit.android.async.Threads import io.timelimit.android.async.Threads
import io.timelimit.android.data.dao.DerivedDataDao import io.timelimit.android.data.dao.DerivedDataDao
@ -52,7 +53,7 @@ import java.util.concurrent.TimeUnit
CategoryNetworkId::class, CategoryNetworkId::class,
ChildTask::class, ChildTask::class,
CategoryTimeWarning::class CategoryTimeWarning::class
], version = 40) ], version = 41)
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database { abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
companion object { companion object {
private val lock = Object() private val lock = Object()
@ -87,47 +88,7 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
) )
.setJournalMode(JournalMode.TRUNCATE) .setJournalMode(JournalMode.TRUNCATE)
.fallbackToDestructiveMigrationOnDowngrade() .fallbackToDestructiveMigrationOnDowngrade()
.addMigrations( .addMigrations(*DatabaseMigrations.ALL)
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
)
.setQueryExecutor(Threads.database) .setQueryExecutor(Threads.database)
.addCallback(object: Callback() { .addCallback(object: Callback() {
override fun onOpen(db: SupportSQLiteDatabase) { override fun onOpen(db: SupportSQLiteDatabase) {

View file

@ -330,4 +330,28 @@ abstract class ConfigDao {
fun getAnnoyManualUnblockCounter() = getValueOfKeySync(ConfigurationItemType.AnnoyManualUnblockCounter).let { it?.toInt() ?: 0 } fun getAnnoyManualUnblockCounter() = getValueOfKeySync(ConfigurationItemType.AnnoyManualUnblockCounter).let { it?.toInt() ?: 0 }
fun setAnoyManualUnblockCounterSync(counter: Int) { updateValueSync(ConfigurationItemType.AnnoyManualUnblockCounter, counter.toString()) } 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, CustomOrganizationName,
ServerApiLevel, ServerApiLevel,
AnnoyManualUnblockCounter, AnnoyManualUnblockCounter,
ConsentFlags,
} }
object ConfigurationItemTypeUtil { object ConfigurationItemTypeUtil {
@ -128,6 +129,7 @@ object ConfigurationItemTypeUtil {
private const val CUSTOM_ORGANIZATION_NAME = 23 private const val CUSTOM_ORGANIZATION_NAME = 23
private const val SERVER_API_LEVEL = 24 private const val SERVER_API_LEVEL = 24
private const val ANNOY_MANUAL_UNBLOCK_COUNTER = 25 private const val ANNOY_MANUAL_UNBLOCK_COUNTER = 25
private const val CONSENT_FLAGS = 26
val TYPES = listOf( val TYPES = listOf(
ConfigurationItemType.OwnDeviceId, ConfigurationItemType.OwnDeviceId,
@ -153,7 +155,8 @@ object ConfigurationItemTypeUtil {
ConfigurationItemType.UpdateStatus, ConfigurationItemType.UpdateStatus,
ConfigurationItemType.CustomOrganizationName, ConfigurationItemType.CustomOrganizationName,
ConfigurationItemType.ServerApiLevel, ConfigurationItemType.ServerApiLevel,
ConfigurationItemType.AnnoyManualUnblockCounter ConfigurationItemType.AnnoyManualUnblockCounter,
ConfigurationItemType.ConsentFlags
) )
fun serialize(value: ConfigurationItemType) = when(value) { fun serialize(value: ConfigurationItemType) = when(value) {
@ -181,6 +184,7 @@ object ConfigurationItemTypeUtil {
ConfigurationItemType.CustomOrganizationName -> CUSTOM_ORGANIZATION_NAME ConfigurationItemType.CustomOrganizationName -> CUSTOM_ORGANIZATION_NAME
ConfigurationItemType.ServerApiLevel -> SERVER_API_LEVEL ConfigurationItemType.ServerApiLevel -> SERVER_API_LEVEL
ConfigurationItemType.AnnoyManualUnblockCounter -> ANNOY_MANUAL_UNBLOCK_COUNTER ConfigurationItemType.AnnoyManualUnblockCounter -> ANNOY_MANUAL_UNBLOCK_COUNTER
ConfigurationItemType.ConsentFlags -> CONSENT_FLAGS
} }
fun parse(value: Int) = when(value) { fun parse(value: Int) = when(value) {
@ -208,6 +212,7 @@ object ConfigurationItemTypeUtil {
CUSTOM_ORGANIZATION_NAME -> ConfigurationItemType.CustomOrganizationName CUSTOM_ORGANIZATION_NAME -> ConfigurationItemType.CustomOrganizationName
SERVER_API_LEVEL -> ConfigurationItemType.ServerApiLevel SERVER_API_LEVEL -> ConfigurationItemType.ServerApiLevel
ANNOY_MANUAL_UNBLOCK_COUNTER -> ConfigurationItemType.AnnoyManualUnblockCounter ANNOY_MANUAL_UNBLOCK_COUNTER -> ConfigurationItemType.AnnoyManualUnblockCounter
CONSENT_FLAGS -> ConfigurationItemType.ConsentFlags
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
} }
@ -252,3 +257,7 @@ object ExperimentalFlags {
// private const val OBSOLETE_DISABLE_FG_APP_DETECTION_FALLBACK = 131072L // private const val OBSOLETE_DISABLE_FG_APP_DETECTION_FALLBACK = 131072L
const val STRICT_OVERLAY_CHECKING = 0x40000L 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 * 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 * it under the terms of the GNU General Public License as published by
@ -28,7 +28,8 @@ data class DeviceRelatedData (
val isLocalMode: Boolean, val isLocalMode: Boolean,
val hasValidDefaultUser: Boolean, val hasValidDefaultUser: Boolean,
val temporarilyAllowedApps: Set<String>, val temporarilyAllowedApps: Set<String>,
val experimentalFlags: Long val experimentalFlags: Long,
val consentFlags: Long
): Observer { ): Observer {
companion object { companion object {
private val relatedTables = arrayOf(Table.ConfigurationItem, Table.Device, Table.User, Table.TemporarilyAllowedApp) 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 hasValidDefaultUser = database.user().getUserByIdSync(deviceEntry.defaultUser) != null
val temporarilyAllowedApps = database.temporarilyAllowedApp().getTemporarilyAllowedAppsSync().toSet() val temporarilyAllowedApps = database.temporarilyAllowedApp().getTemporarilyAllowedAppsSync().toSet()
val experimentalFlags = database.config().getExperimentalFlagsSync() val experimentalFlags = database.config().getExperimentalFlagsSync()
val consentFlags = database.config().getConsentFlagsSync()
DeviceRelatedData( DeviceRelatedData(
deviceEntry = deviceEntry, deviceEntry = deviceEntry,
@ -48,7 +50,8 @@ data class DeviceRelatedData (
isLocalMode = isLocalMode, isLocalMode = isLocalMode,
hasValidDefaultUser = hasValidDefaultUser, hasValidDefaultUser = hasValidDefaultUser,
temporarilyAllowedApps = temporarilyAllowedApps, temporarilyAllowedApps = temporarilyAllowedApps,
experimentalFlags = experimentalFlags experimentalFlags = experimentalFlags,
consentFlags = consentFlags
).also { ).also {
database.registerWeakObserver(relatedTables, WeakReference(it)) 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 * 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 * 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 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() }.ignoreUnchanged()
val deviceEntryIfEnabled = enable.switchMap { val deviceEntryIfEnabled: LiveData<Device?> = enable.switchMap {
if (it == null || it == false) { if (it == null || it == false) {
liveDataFromNullableValue(null as Device?) liveDataFromNullableValue(null as Device?)
} else { } else {
@ -95,8 +95,9 @@ class AppLogic(
websocketClientCreator = websocketClientCreator websocketClientCreator = websocketClientCreator
) )
val syncAppsLogic = SyncInstalledAppsLogic(this)
init { init {
SyncInstalledAppsLogic(this)
WatchdogLogic(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.coroutines.runAsyncExpectForever
import io.timelimit.android.data.model.App import io.timelimit.android.data.model.App
import io.timelimit.android.data.model.AppActivity 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.data.model.UserType
import io.timelimit.android.integration.platform.ProtectionLevel import io.timelimit.android.integration.platform.ProtectionLevel
import io.timelimit.android.livedata.* import io.timelimit.android.livedata.*
@ -45,17 +46,53 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
requestSync.value = true 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 { init {
appLogic.platformIntegration.installedAppsChangeListener = Runnable { requestSync() } appLogic.platformIntegration.installedAppsChangeListener = Runnable { requestSync() }
appLogic.deviceEntryIfEnabled.map { device -> deviceStateLive.observeForever { requestSync() }
device?.let { DeviceState(
id = device.id,
currentUserId = device.currentUserId,
defaultUser = device.defaultUser,
enableActivityLevelBlocking = device.enableActivityLevelBlocking,
isDeviceOwner = device.currentProtectionLevel == ProtectionLevel.DeviceOwner
) }
}.ignoreUnchanged().observeForever { requestSync() }
runAsyncExpectForever { syncLoop() } runAsyncExpectForever { syncLoop() }
} }
@ -88,26 +125,17 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
private suspend fun doSyncNow() { private suspend fun doSyncNow() {
doSyncLock.withLock { doSyncLock.withLock {
val deviceEntry = appLogic.deviceEntryIfEnabled.waitForNullableValue() val deviceState = Threads.database.executeAndWait { getDeviceStateSync() } ?: return
if (deviceEntry == null) { if (deviceState.isLocalMode) {
return
}
if (appLogic.database.config().getDeviceAuthTokenAsync().waitForNullableValue().isNullOrEmpty()) {
// local mode -> sync always // local mode -> sync always
} else { } else {
// connected mode -> don't sync always // connected mode -> don't sync always
if (!deviceState.hasSyncConsent) return@withLock
val userEntry = appLogic.deviceUserEntry.waitForNullableValue() if (!deviceState.hasAnyChildUser) return@withLock
val defaultUserEntry = appLogic.database.user().getUserByIdLive(deviceEntry.defaultUser).waitForNullableValue()
if (userEntry?.type != UserType.Child && defaultUserEntry?.type != UserType.Child) {
return@withLock
}
} }
val deviceId = deviceEntry.id val deviceId = deviceState.id
val currentlyInstalledApps = getCurrentApps(deviceId) val currentlyInstalledApps = getCurrentApps(deviceId)
@ -153,7 +181,7 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
run { run {
fun buildKey(activity: AppActivity) = "${activity.appPackageName}:${activity.activityClassName}" fun buildKey(activity: AppActivity) = "${activity.appPackageName}:${activity.activityClassName}"
val currentlyInstalled = if (deviceEntry.enableActivityLevelBlocking) val currentlyInstalled = if (deviceState.enableActivityLevelBlocking)
Threads.backgroundOSInteraction.executeAndWait { Threads.backgroundOSInteraction.executeAndWait {
val realActivities = appLogic.platformIntegration.getLocalAppActivities(deviceId = deviceId) val realActivities = appLogic.platformIntegration.getLocalAppActivities(deviceId = deviceId)
val dummyActivities = currentlyInstalledApps.keys.map { packageName -> val dummyActivities = currentlyInstalledApps.keys.map { packageName ->
@ -220,9 +248,14 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
internal data class DeviceState( internal data class DeviceState(
val id: String, val id: String,
val currentUserId: String, val isCurrentUserChild: Boolean,
val defaultUser: String, val isDefaultUserChild: Boolean,
val enableActivityLevelBlocking: 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 * 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 * 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.R
import io.timelimit.android.data.model.Category import io.timelimit.android.data.model.Category
import io.timelimit.android.databinding.AddItemViewBinding import io.timelimit.android.databinding.AddItemViewBinding
import io.timelimit.android.databinding.AppListSyncPermissionRequestCardBinding
import io.timelimit.android.databinding.CategoryRichCardBinding import io.timelimit.android.databinding.CategoryRichCardBinding
import io.timelimit.android.databinding.IntroCardBinding import io.timelimit.android.databinding.IntroCardBinding
import io.timelimit.android.ui.util.DateUtil 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_ADD = 1
private const val TYPE_INTRO = 2 private const val TYPE_INTRO = 2
private const val TYPE_MANIPULATION_WARNING = 3 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() } var categories: List<ManageChildCategoriesListItem>? by Delegates.observable(null as List<ManageChildCategoriesListItem>?) { _, _, _ -> notifyDataSetChanged() }
@ -53,6 +55,7 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
CreateCategoryItem -> item.hashCode() CreateCategoryItem -> item.hashCode()
CategoriesIntroductionHeader -> item.hashCode() CategoriesIntroductionHeader -> item.hashCode()
ManipulationWarningCategoryItem -> item.hashCode() ManipulationWarningCategoryItem -> item.hashCode()
ManageChildCategoriesListItem.SyncAppListBanner -> item.hashCode()
}.toLong() }.toLong()
} }
@ -61,6 +64,7 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
CreateCategoryItem -> TYPE_ADD CreateCategoryItem -> TYPE_ADD
CategoriesIntroductionHeader -> TYPE_INTRO CategoriesIntroductionHeader -> TYPE_INTRO
ManipulationWarningCategoryItem -> TYPE_MANIPULATION_WARNING ManipulationWarningCategoryItem -> TYPE_MANIPULATION_WARNING
ManageChildCategoriesListItem.SyncAppListBanner -> TYPE_APP_LIST_BANNER
} }
override fun getItemCount() = categories?.size ?: 0 override fun getItemCount() = categories?.size ?: 0
@ -104,6 +108,13 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
.inflate(R.layout.manage_child_manipulation_warning, parent, false) .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() else -> throw IllegalStateException()
} }
@ -163,6 +174,9 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
ManipulationWarningCategoryItem -> { ManipulationWarningCategoryItem -> {
// nothing to do // nothing to do
} }
ManageChildCategoriesListItem.SyncAppListBanner -> {
// nothing to do
}
}.let { } }.let { }
} }
} }
@ -171,10 +185,12 @@ sealed class ViewHolder(view: View): RecyclerView.ViewHolder(view)
class AddViewHolder(view: View): ViewHolder(view) class AddViewHolder(view: View): ViewHolder(view)
class IntroViewHolder(view: View): ViewHolder(view) class IntroViewHolder(view: View): ViewHolder(view)
class ManipulationWarningViewHolder(view: View): ViewHolder(view) class ManipulationWarningViewHolder(view: View): ViewHolder(view)
class SyncAppListViewHolder(view: View): ViewHolder(view)
class ItemViewHolder(val binding: CategoryRichCardBinding): ViewHolder(binding.root) class ItemViewHolder(val binding: CategoryRichCardBinding): ViewHolder(binding.root)
interface Handlers { interface Handlers {
fun onCategoryClicked(category: Category) fun onCategoryClicked(category: Category)
fun onCreateCategoryClicked() fun onCreateCategoryClicked()
fun onCategorySwitched(category: CategoryItem, isChecked: Boolean): Boolean 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 * 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 * 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 import io.timelimit.android.data.model.Category
sealed class ManageChildCategoriesListItem sealed class ManageChildCategoriesListItem {
object SyncAppListBanner: ManageChildCategoriesListItem()
}
object CategoriesIntroductionHeader: ManageChildCategoriesListItem() object CategoriesIntroductionHeader: ManageChildCategoriesListItem()
object CreateCategoryItem: ManageChildCategoriesListItem() object CreateCategoryItem: ManageChildCategoriesListItem()
object ManipulationWarningCategoryItem: 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.UpdateCategoryDisableLimitsAction
import io.timelimit.android.sync.actions.UpdateCategorySortingAction import io.timelimit.android.sync.actions.UpdateCategorySortingAction
import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction 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.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs
@ -58,7 +59,7 @@ class ManageChildCategoriesFragment : Fragment() {
private val model: ManageChildCategoriesModel by viewModels() private val model: ManageChildCategoriesModel by viewModels()
private lateinit var binding: RecyclerFragmentBinding 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) binding = RecyclerFragmentBinding.inflate(inflater, container, false)
return binding.root return binding.root
@ -136,6 +137,10 @@ class ManageChildCategoriesFragment : Fragment() {
false false
} }
} }
override fun onRequestAppListSyncConsentClicked() {
SyncAppListConsentDialogFragment.newInstance().show(parentFragmentManager)
}
} }
binding.recycler.adapter = adapter 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 * 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 * 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 hasShownHint = logic.database.config().wereHintsShown(HintsToShow.CATEGORIES_INTRODUCTION)
private val listContentStep1 = hasShownHint.switchMap { hasShownHint -> private val showSyncConsentBanner = logic.syncAppsLogic.shouldAskForConsent
categoryItems.map { categoryItems ->
if (hasShownHint) {
categoryItems + listOf(CreateCategoryItem)
} else {
listOf(CategoriesIntroductionHeader) + categoryItems + listOf(CreateCategoryItem)
}
}
}
val listContent = hasNotSuppressedChildDeviceManipulation.switchMap { hasChildDevicesWithManipulation -> val listContent = mergeLiveDataWaitForValues(
listContentStep1.map { listContent -> categoryItems,
if (hasChildDevicesWithManipulation) { hasShownHint,
listOf(ManipulationWarningCategoryItem) + listContent showSyncConsentBanner,
} else { hasNotSuppressedChildDeviceManipulation
listContent ).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 * 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 * 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 android.widget.CheckBox
import androidx.appcompat.widget.AppCompatRadioButton import androidx.appcompat.widget.AppCompatRadioButton
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.Navigation import androidx.navigation.Navigation
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.coroutines.runAsync import io.timelimit.android.coroutines.runAsync
@ -60,6 +60,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
private const val STATUS_ALLOWED_APPS_CATEGORY = "c" private const val STATUS_ALLOWED_APPS_CATEGORY = "c"
} }
private val model: SetupDeviceModel by viewModels()
private val selectedUser = MutableLiveData<String>() private val selectedUser = MutableLiveData<String>()
private val selectedAppsToNotWhitelist = mutableSetOf<String>() private val selectedAppsToNotWhitelist = mutableSetOf<String>()
private var allowedAppsCategory = "" private var allowedAppsCategory = ""
@ -89,11 +90,10 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
outState.putString(STATUS_ALLOWED_APPS_CATEGORY, allowedAppsCategory) 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 binding = FragmentSetupDeviceBinding.inflate(inflater, container, false)
val logic = DefaultAppLogic.with(requireContext()) val logic = DefaultAppLogic.with(requireContext())
val activity = activity as ActivityViewModelHolder val activity = activity as ActivityViewModelHolder
val model = ViewModelProviders.of(this).get(SetupDeviceModel::class.java)
val navigation = Navigation.findNavController(container!!) val navigation = Navigation.findNavController(container!!)
binding.needsParent.authBtn.setOnClickListener { 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 // ID to label
val items = mutableListOf<Pair<String, String>>() val items = mutableListOf<Pair<String, String>>()
@ -150,7 +150,9 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
// select the first item if nothing is selected currently // select the first item if nothing is selected currently
if (items.find { (id) -> id == selectedUser.value } == null) { if (items.find { (id) -> id == selectedUser.value } == null) {
selectedUser.value = items.first().first items.firstOrNull()?.first?.let {
selectedUser.value = it
}
} }
// build the views // build the views
@ -166,7 +168,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
binding.selectUserRadioGroup.removeAllViews() binding.selectUserRadioGroup.removeAllViews()
views.forEach { view -> binding.selectUserRadioGroup.addView(view) } views.forEach { view -> binding.selectUserRadioGroup.addView(view) }
views.find { it.tag == selectedUser.value }?.isChecked = true views.find { it.tag == selectedUser.value }?.isChecked = true
}) }
val isNewUser = selectedUser.map { NEW_USER.contains(it) } val isNewUser = selectedUser.map { NEW_USER.contains(it) }
val isParentUser = selectedUser.switchMap { val isParentUser = selectedUser.switchMap {
@ -181,8 +183,8 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
} }
} }
isNewUser.observe(this, Observer { binding.isAddingNewUser = it }) isNewUser.observe(viewLifecycleOwner) { binding.isAddingNewUser = it }
isParentUser.observe(this, Observer { binding.isAddingChild = !it }) isParentUser.observe(viewLifecycleOwner) { binding.isAddingChild = !it }
val categoriesOfTheSelectedUser = selectedUser.switchMap { user -> val categoriesOfTheSelectedUser = selectedUser.switchMap { user ->
if (NEW_USER.contains(user)) { if (NEW_USER.contains(user)) {
@ -204,7 +206,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
recommendWhitelistLocalApps.filterNot { app -> assignedApps.contains(app.packageName) } recommendWhitelistLocalApps.filterNot { app -> assignedApps.contains(app.packageName) }
} }
appsToWhitelist.observe(this, Observer { apps -> appsToWhitelist.observe(viewLifecycleOwner) { apps ->
binding.areThereAnyApps = apps.isNotEmpty() binding.areThereAnyApps = apps.isNotEmpty()
binding.suggestedAllowedApps.removeAllViews() binding.suggestedAllowedApps.removeAllViews()
@ -225,9 +227,9 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
} }
) )
} }
}) }
categoriesOfTheSelectedUser.observe(this, Observer { categories -> categoriesOfTheSelectedUser.observe(viewLifecycleOwner) { categories ->
// id to title // id to title
val items = mutableListOf<Pair<String, String>>() val items = mutableListOf<Pair<String, String>>()
@ -241,7 +243,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
} else { } else {
if (items.find { (id) -> id == allowedAppsCategory } == null) { if (items.find { (id) -> id == allowedAppsCategory } == null) {
// use the one with the lowest blocked times // 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 binding.areThereAnyCategories = true
@ -258,7 +260,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
views.forEach { view -> binding.allowedAppsCategory.addView(view) } views.forEach { view -> binding.allowedAppsCategory.addView(view) }
views.find { it.tag == allowedAppsCategory }?.isChecked = true views.find { it.tag == allowedAppsCategory }?.isChecked = true
} }
}) }
val selectedName = MutableLiveData<String>().apply { value = binding.newUserName.text.toString() } val selectedName = MutableLiveData<String>().apply { value = binding.newUserName.text.toString() }
binding.newUserName.addTextChangedListener(object: TextWatcher { binding.newUserName.addTextChangedListener(object: TextWatcher {
@ -281,9 +283,9 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
) )
val validationOfAll = (validationOfName.and(validationOfPassword)).or(isNewUser.invert()) 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( ManageDeviceBackgroundSync.bind(
view = binding.backgroundSync, view = binding.backgroundSync,
@ -302,7 +304,8 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
appsToNotWhitelist = selectedAppsToNotWhitelist, appsToNotWhitelist = selectedAppsToNotWhitelist,
model = activity.getActivityViewModel(), model = activity.getActivityViewModel(),
networkTime = SetupNetworkTimeVerification.readSelection(binding.networkTimeVerification), 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 package io.timelimit.android.ui.setup.device
import android.app.Application import android.app.Application
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.timelimit.android.BuildConfig
import io.timelimit.android.R
import io.timelimit.android.async.Threads import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.coroutines.runAsync import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.IdGenerator import io.timelimit.android.data.IdGenerator
import io.timelimit.android.data.model.AppRecommendation 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.NetworkTime
import io.timelimit.android.data.model.UserType import io.timelimit.android.data.model.UserType
import io.timelimit.android.livedata.castDown import io.timelimit.android.livedata.castDown
@ -35,6 +40,10 @@ import io.timelimit.android.ui.user.create.DefaultCategories
import io.timelimit.android.update.UpdateUtil import io.timelimit.android.update.UpdateUtil
class SetupDeviceModel(application: Application): AndroidViewModel(application) { class SetupDeviceModel(application: Application): AndroidViewModel(application) {
companion object {
private const val LOG_TAG = "SetupDeviceModel"
}
private val logic = DefaultAppLogic.with(application) private val logic = DefaultAppLogic.with(application)
private val statusInternal = MutableLiveData<SetupDeviceModelStatus>().apply { value = SetupDeviceModelStatus.Ready } private val statusInternal = MutableLiveData<SetupDeviceModelStatus>().apply { value = SetupDeviceModelStatus.Ready }
@ -48,7 +57,8 @@ class SetupDeviceModel(application: Application): AndroidViewModel(application)
appsToNotWhitelist: Set<String>, appsToNotWhitelist: Set<String>,
model: ActivityViewModel, model: ActivityViewModel,
networkTime: NetworkTime, networkTime: NetworkTime,
enableUpdateChecks: Boolean enableUpdateChecks: Boolean,
enableAppListSync: Boolean
) { ) {
if (statusInternal.value != SetupDeviceModelStatus.Ready) { if (statusInternal.value != SetupDeviceModelStatus.Ready) {
return return
@ -57,123 +67,158 @@ class SetupDeviceModel(application: Application): AndroidViewModel(application)
statusInternal.value = SetupDeviceModelStatus.Working statusInternal.value = SetupDeviceModelStatus.Working
runAsync { runAsync {
val actions = mutableListOf<ParentAction>() try {
var realUserId = userId val actions = mutableListOf<ParentAction>()
var realAllowedAppsCategory = allowedAppsCategory var realUserId = userId
val defaultCategories = DefaultCategories.with(getApplication()) var realAllowedAppsCategory = allowedAppsCategory
val defaultCategories = DefaultCategories.with(getApplication())
val isUserAnChild = when (userId) { val isUserAnChild = when (userId) {
SetupDeviceFragment.NEW_PARENT -> { SetupDeviceFragment.NEW_PARENT -> {
// generate user id // generate user id
realUserId = IdGenerator.generateId() realUserId = IdGenerator.generateId()
// create parent // create parent
actions.add(AddUserAction( actions.add(
userId = realUserId, AddUserAction(
name = username, userId = realUserId,
timeZone = logic.timeApi.getSystemTimeZone().id, name = username,
userType = UserType.Parent, timeZone = logic.timeApi.getSystemTimeZone().id,
password = ParentPassword.createCoroutine(password) 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 if (isUserAnChild) {
actions.add(AddUserAction( if (realAllowedAppsCategory == "") {
userId = realUserId, // create allowed apps category if none was specified and overwrite its id
name = username, realAllowedAppsCategory = IdGenerator.generateId()
timeZone = logic.timeApi.getSystemTimeZone().id,
userType = UserType.Child,
password = if (password.isEmpty()) null else ParentPassword.createCoroutine(password)
))
// create default categories actions.add(
realAllowedAppsCategory = IdGenerator.generateId() CreateCategoryAction(
val allowedGamesCategory = IdGenerator.generateId() childId = realUserId,
categoryId = realAllowedAppsCategory,
actions.add(CreateCategoryAction( title = defaultCategories.allowedAppsTitle
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 val alreadyAssignedApps = Threads.database.executeAndWait {
} logic.database.categoryApp().getCategoryAppsByUserIdSync(realUserId)
else -> { .filter { it.appSpecifier.deviceId == null }
logic.database.user().getUserByIdLive(userId).waitForNullableValue()!!.type == UserType.Child .map { it.appSpecifier.packageName }
} .toSet()
} }
if (isUserAnChild) { // add allowed apps
if (realAllowedAppsCategory == "") { val allowedAppsPackages =
// create allowed apps category if none was specified and overwrite its id logic.platformIntegration.getLocalApps(IdGenerator.generateId())
realAllowedAppsCategory = IdGenerator.generateId() .filter { app -> app.recommendation == AppRecommendation.Whitelist }
.map { app -> app.packageName }
.toMutableSet().apply {
removeAll(appsToNotWhitelist)
removeAll(alreadyAssignedApps)
}.toList()
actions.add(CreateCategoryAction( if (allowedAppsPackages.isNotEmpty()) {
childId = realUserId, actions.add(
categoryId = realAllowedAppsCategory, AddCategoryAppsAction(
title = defaultCategories.allowedAppsTitle categoryId = realAllowedAppsCategory,
)) packageNames = allowedAppsPackages
)
)
}
} }
val alreadyAssignedApps = Threads.database.executeAndWait { // apply the network time mode
logic.database.categoryApp().getCategoryAppsByUserIdSync(realUserId) val deviceId = logic.deviceId.waitForNullableValue()!!
.filter { it.appSpecifier.deviceId == null }
.map { it.appSpecifier.packageName } actions.add(
.toSet() 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 if (model.tryDispatchParentActions(actions)) {
val allowedAppsPackages = logic.platformIntegration.getLocalApps(IdGenerator.generateId()) statusInternal.value = SetupDeviceModelStatus.Done
.filter { app -> app.recommendation == AppRecommendation.Whitelist } } else {
.map { app -> app.packageName } statusInternal.value = SetupDeviceModelStatus.Ready
.toMutableSet().apply { }
removeAll(appsToNotWhitelist) } catch (ex: Exception) {
removeAll(alreadyAssignedApps) if (BuildConfig.DEBUG) {
}.toList() Log.d(LOG_TAG, "could not setup device", ex)
if (allowedAppsPackages.isNotEmpty()) {
actions.add(AddCategoryAppsAction(
categoryId = realAllowedAppsCategory,
packageNames = allowedAppsPackages
))
} }
}
// apply the network time mode Toast.makeText(getApplication(), R.string.error_general, Toast.LENGTH_SHORT).show()
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)
if (model.tryDispatchParentActions(actions)) {
statusInternal.value = SetupDeviceModelStatus.Done
} else {
statusInternal.value = SetupDeviceModelStatus.Ready 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 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 it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License. the Free Software Foundation version 3 of the License.
@ -203,6 +203,10 @@
<include android:id="@+id/network_time_verification" <include android:id="@+id/network_time_verification"
layout="@layout/setup_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" <include android:id="@+id/background_sync"
layout="@layout/manage_device_background_sync_view" /> 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_yes">Ja</string>
<string name="generic_skip">Überspringen</string> <string name="generic_skip">Überspringen</string>
<string name="generic_show_details">Details anzeigen</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_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> <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 und daher auch keine Apps auf Geräten, die dem Benutzer zugeordnet wurden
</string> </string>
<string name="category_apps_add_empty_child_devices_no_apps">Es gibt mindestens ein Gerät für diesen Benutzer, <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>
<string name="category_apps_add_empty_all_devices_no_apps_no_childs">Es gibt kein Gerät, das einem Kind zugeordnet wurde. <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. 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_title">Vernetzung und Datenschutz</string>
<string name="setup_privacy_connected_text_general_intro">Für den vernetzten Modus <string name="setup_privacy_connected_text_general_intro">Für den vernetzten Modus
gibt es eine Zentrale - den Server. Auf diesen werden die Einstellungen, 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. gespeichert.
</string> </string>
<string name="setup_privacy_connected_text_default_server">Sie verwenden - da Sie Nichts anderes gewählt haben - <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. verhindern, indem Sie den lokalen Modus verwenden.
</string> </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_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_title">Dieses Gerät verknüpfen</string>
<string name="setup_remote_child_text"> <string name="setup_remote_child_text">

View file

@ -33,6 +33,8 @@
<string name="generic_yes">Yes</string> <string name="generic_yes">Yes</string>
<string name="generic_skip">Skip</string> <string name="generic_skip">Skip</string>
<string name="generic_show_details">Show details</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_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> <string name="generic_runtime_permission_rejected">Permission rejected; You can manage permissions in the system settings</string>
@ -315,7 +317,8 @@
</string> </string>
<string name="category_apps_add_empty_child_devices_no_apps"> <string name="category_apps_add_empty_child_devices_no_apps">
There is a device assigned to this user, but there are no know 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>
<string name="category_apps_add_empty_all_devices_no_apps_no_childs">There is no device with any child assigned. <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. 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_title">Networking and Privacy</string>
<string name="setup_privacy_connected_text_general_intro">For the connected mode, <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 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>
<string name="setup_privacy_connected_text_default_server">Because you have not selected anything different, <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. 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. the data transmission by using the local mode.
</string> </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_code_invalid">This code is invalid</string>
<string name="setup_remote_child_title">Link this device</string> <string name="setup_remote_child_title">Link this device</string>
<string name="setup_remote_child_text"> <string name="setup_remote_child_text">