From fd5ffe73ce624fb6a26d4e55f4622848df5ef6d7 Mon Sep 17 00:00:00 2001 From: Jonas Lochmann Date: Mon, 23 May 2022 02:00:00 +0200 Subject: [PATCH] Adjust for GPlay policies related to the list of installed Apps --- .../41.json | 1264 +++++++++++++++++ .../io/timelimit/android/data/Migrations.kt | 127 +- .../io/timelimit/android/data/RoomDatabase.kt | 45 +- .../timelimit/android/data/dao/ConfigDao.kt | 24 + .../android/data/model/ConfigurationItem.kt | 11 +- .../data/model/derived/DeviceRelatedData.kt | 9 +- .../android/livedata/MergeLiveData.kt | 47 +- .../io/timelimit/android/logic/AppLogic.kt | 5 +- .../android/logic/SyncInstalledAppsLogic.kt | 89 +- .../SyncAppListConsentDialogFragment.kt | 59 + .../ui/manage/child/category/Adapter.kt | 18 +- .../android/ui/manage/child/category/Items.kt | 7 +- .../category/ManageChildCategoriesFragment.kt | 7 +- .../category/ManageChildCategoriesModel.kt | 38 +- .../ui/setup/device/SetupDeviceFragment.kt | 37 +- .../ui/setup/device/SetupDeviceModel.kt | 247 ++-- .../app_list_sync_permission_request_card.xml | 50 + .../main/res/layout/fragment_setup_device.xml | 6 +- .../res/layout/setup_device_app_list_sync.xml | 48 + app/src/main/res/values-de/strings.xml | 18 +- app/src/main/res/values/strings.xml | 16 +- 21 files changed, 1911 insertions(+), 261 deletions(-) create mode 100644 app/schemas/io.timelimit.android.data.RoomDatabase/41.json create mode 100644 app/src/main/java/io/timelimit/android/ui/consent/SyncAppListConsentDialogFragment.kt create mode 100644 app/src/main/res/layout/app_list_sync_permission_request_card.xml create mode 100644 app/src/main/res/layout/setup_device_app_list_sync.xml diff --git a/app/schemas/io.timelimit.android.data.RoomDatabase/41.json b/app/schemas/io.timelimit.android.data.RoomDatabase/41.json new file mode 100644 index 0000000..e06fee8 --- /dev/null +++ b/app/schemas/io.timelimit.android.data.RoomDatabase/41.json @@ -0,0 +1,1264 @@ +{ + "formatVersion": 1, + "database": { + "version": 41, + "identityHash": "4b90ef1fc32072c93f68fecee1bd7864", + "entities": [ + { + "tableName": "user", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `password` TEXT NOT NULL, `second_password_salt` TEXT NOT NULL, `type` TEXT NOT NULL, `timezone` TEXT NOT NULL, `disable_limits_until` INTEGER NOT NULL, `mail` TEXT NOT NULL, `current_device` TEXT NOT NULL, `category_for_not_assigned_apps` TEXT NOT NULL, `relax_primary_device` INTEGER NOT NULL, `mail_notification_flags` INTEGER NOT NULL, `blocked_times` TEXT NOT NULL, `flags` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "secondPasswordSalt", + "columnName": "second_password_salt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeZone", + "columnName": "timezone", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "disableLimitsUntil", + "columnName": "disable_limits_until", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mail", + "columnName": "mail", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentDevice", + "columnName": "current_device", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryForNotAssignedApps", + "columnName": "category_for_not_assigned_apps", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relaxPrimaryDevice", + "columnName": "relax_primary_device", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mailNotificationFlags", + "columnName": "mail_notification_flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "obsoleteBlockedTimes", + "columnName": "blocked_times", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "device", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `model` TEXT NOT NULL, `added_at` INTEGER NOT NULL, `current_user_id` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `network_time` TEXT NOT NULL, `current_protection_level` TEXT NOT NULL, `highest_permission_level` TEXT NOT NULL, `current_usage_stats_permission` TEXT NOT NULL, `highest_usage_stats_permission` TEXT NOT NULL, `current_notification_access_permission` TEXT NOT NULL, `highest_notification_access_permission` TEXT NOT NULL, `current_app_version` INTEGER NOT NULL, `highest_app_version` INTEGER NOT NULL, `tried_disabling_device_admin` INTEGER NOT NULL, `did_reboot` INTEGER NOT NULL, `had_manipulation` INTEGER NOT NULL, `had_manipulation_flags` INTEGER NOT NULL, `did_report_uninstall` INTEGER NOT NULL, `is_user_kept_signed_in` INTEGER NOT NULL, `show_device_connected` INTEGER NOT NULL, `default_user` TEXT NOT NULL, `default_user_timeout` INTEGER NOT NULL, `consider_reboot_manipulation` INTEGER NOT NULL, `current_overlay_permission` TEXT NOT NULL, `highest_overlay_permission` TEXT NOT NULL, `current_accessibility_service_permission` INTEGER NOT NULL, `was_accessibility_service_permission` INTEGER NOT NULL, `enable_activity_level_blocking` INTEGER NOT NULL, `q_or_later` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addedAt", + "columnName": "added_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentUserId", + "columnName": "current_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installedAppsVersion", + "columnName": "apps_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "networkTime", + "columnName": "network_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentProtectionLevel", + "columnName": "current_protection_level", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highestProtectionLevel", + "columnName": "highest_permission_level", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentUsageStatsPermission", + "columnName": "current_usage_stats_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highestUsageStatsPermission", + "columnName": "highest_usage_stats_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentNotificationAccessPermission", + "columnName": "current_notification_access_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highestNotificationAccessPermission", + "columnName": "highest_notification_access_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentAppVersion", + "columnName": "current_app_version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "highestAppVersion", + "columnName": "highest_app_version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "manipulationTriedDisablingDeviceAdmin", + "columnName": "tried_disabling_device_admin", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "manipulationDidReboot", + "columnName": "did_reboot", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hadManipulation", + "columnName": "had_manipulation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hadManipulationFlags", + "columnName": "had_manipulation_flags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "didReportUninstall", + "columnName": "did_report_uninstall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUserKeptSignedIn", + "columnName": "is_user_kept_signed_in", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showDeviceConnected", + "columnName": "show_device_connected", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultUser", + "columnName": "default_user", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultUserTimeout", + "columnName": "default_user_timeout", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "considerRebootManipulation", + "columnName": "consider_reboot_manipulation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentOverlayPermission", + "columnName": "current_overlay_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highestOverlayPermission", + "columnName": "highest_overlay_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessibilityServiceEnabled", + "columnName": "current_accessibility_service_permission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wasAccessibilityServiceEnabled", + "columnName": "was_accessibility_service_permission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableActivityLevelBlocking", + "columnName": "enable_activity_level_blocking", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "qOrLater", + "columnName": "q_or_later", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, `title` TEXT NOT NULL, `launchable` INTEGER NOT NULL, `recommendation` TEXT NOT NULL, PRIMARY KEY(`device_id`, `package_name`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isLaunchable", + "columnName": "launchable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "device_id", + "package_name" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_app_device_id", + "unique": false, + "columnNames": [ + "device_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_app_device_id` ON `${TABLE_NAME}` (`device_id`)" + }, + { + "name": "index_app_package_name", + "unique": false, + "columnNames": [ + "package_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_app_package_name` ON `${TABLE_NAME}` (`package_name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "category_app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`category_id`, `package_name`))", + "fields": [ + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appSpecifierString", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "category_id", + "package_name" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_category_app_category_id", + "unique": false, + "columnNames": [ + "category_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_category_app_category_id` ON `${TABLE_NAME}` (`category_id`)" + }, + { + "name": "index_category_app_package_name", + "unique": false, + "columnNames": [ + "package_name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_category_app_package_name` ON `${TABLE_NAME}` (`package_name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "category", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `child_id` TEXT NOT NULL, `title` TEXT NOT NULL, `blocked_times` TEXT NOT NULL, `extra_time` INTEGER NOT NULL, `extra_time_day` INTEGER NOT NULL, `temporarily_blocked` INTEGER NOT NULL, `temporarily_blocked_end_time` INTEGER NOT NULL, `base_version` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `rules_version` TEXT NOT NULL, `usedtimes_version` TEXT NOT NULL, `tasks_version` TEXT NOT NULL DEFAULT '', `parent_category_id` TEXT NOT NULL, `block_all_notifications` INTEGER NOT NULL, `time_warnings` INTEGER NOT NULL, `min_battery_charging` INTEGER NOT NULL, `min_battery_mobile` INTEGER NOT NULL, `sort` INTEGER NOT NULL, `disable_limits_until` INTEGER NOT NULL, `flags` INTEGER NOT NULL DEFAULT 0, `block_notification_delay` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "childId", + "columnName": "child_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockedMinutesInWeek", + "columnName": "blocked_times", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extraTimeInMillis", + "columnName": "extra_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "extraTimeDay", + "columnName": "extra_time_day", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temporarilyBlocked", + "columnName": "temporarily_blocked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temporarilyBlockedEndTime", + "columnName": "temporarily_blocked_end_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseVersion", + "columnName": "base_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "assignedAppsVersion", + "columnName": "apps_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeLimitRulesVersion", + "columnName": "rules_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "usedTimesVersion", + "columnName": "usedtimes_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tasksVersion", + "columnName": "tasks_version", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "parentCategoryId", + "columnName": "parent_category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockAllNotifications", + "columnName": "block_all_notifications", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timeWarnings", + "columnName": "time_warnings", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minBatteryLevelWhileCharging", + "columnName": "min_battery_charging", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minBatteryLevelMobile", + "columnName": "min_battery_mobile", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sort", + "columnName": "sort", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "disableLimitsUntil", + "columnName": "disable_limits_until", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "blockNotificationDelay", + "columnName": "block_notification_delay", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "used_time", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`day_of_epoch` INTEGER NOT NULL, `used_time` INTEGER NOT NULL, `category_id` TEXT NOT NULL, `start_time_of_day` INTEGER NOT NULL, `end_time_of_day` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `day_of_epoch`, `start_time_of_day`, `end_time_of_day`))", + "fields": [ + { + "fieldPath": "dayOfEpoch", + "columnName": "day_of_epoch", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "usedMillis", + "columnName": "used_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startTimeOfDay", + "columnName": "start_time_of_day", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endTimeOfDay", + "columnName": "end_time_of_day", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "category_id", + "day_of_epoch", + "start_time_of_day", + "end_time_of_day" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "time_limit_rule", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `category_id` TEXT NOT NULL, `apply_to_extra_time_usage` INTEGER NOT NULL, `day_mask` INTEGER NOT NULL, `max_time` INTEGER NOT NULL, `start_minute_of_day` INTEGER NOT NULL, `end_minute_of_day` INTEGER NOT NULL, `session_duration_milliseconds` INTEGER NOT NULL, `session_pause_milliseconds` INTEGER NOT NULL, `per_day` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "applyToExtraTimeUsage", + "columnName": "apply_to_extra_time_usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dayMask", + "columnName": "day_mask", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maximumTimeInMillis", + "columnName": "max_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startMinuteOfDay", + "columnName": "start_minute_of_day", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endMinuteOfDay", + "columnName": "end_minute_of_day", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sessionDurationMilliseconds", + "columnName": "session_duration_milliseconds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sessionPauseMilliseconds", + "columnName": "session_pause_milliseconds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "perDay", + "columnName": "per_day", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "config", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "temporarily_allowed_app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`device_id`, `package_name`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "device_id", + "package_name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "pending_sync_action", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sequence_number` INTEGER NOT NULL, `action` TEXT NOT NULL, `integrity` TEXT NOT NULL, `scheduled_for_upload` INTEGER NOT NULL, `type` TEXT NOT NULL, `user_id` TEXT NOT NULL, PRIMARY KEY(`sequence_number`))", + "fields": [ + { + "fieldPath": "sequenceNumber", + "columnName": "sequence_number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encodedAction", + "columnName": "action", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "integrity", + "columnName": "integrity", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scheduledForUpload", + "columnName": "scheduled_for_upload", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "sequence_number" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_pending_sync_action_scheduled_for_upload", + "unique": false, + "columnNames": [ + "scheduled_for_upload" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_pending_sync_action_scheduled_for_upload` ON `${TABLE_NAME}` (`scheduled_for_upload`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "app_activity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "appPackageName", + "columnName": "app_package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activityClassName", + "columnName": "activity_class_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "activity_title", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "device_id", + "app_package_name", + "activity_class_name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL, `id` TEXT NOT NULL, `first_notify_time` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, PRIMARY KEY(`type`, `id`))", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "firstNotifyTime", + "columnName": "first_notify_time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDismissed", + "columnName": "dismissed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "type", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "allowed_contact", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `phone` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phone", + "columnName": "phone", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "user_key", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "key", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "lastUse", + "columnName": "last_use", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "user_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_user_key_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_key_key` ON `${TABLE_NAME}` (`key`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "session_duration", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` TEXT NOT NULL, `max_session_duration` INTEGER NOT NULL, `session_pause_duration` INTEGER NOT NULL, `start_minute_of_day` INTEGER NOT NULL, `end_minute_of_day` INTEGER NOT NULL, `last_usage` INTEGER NOT NULL, `last_session_duration` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `max_session_duration`, `session_pause_duration`, `start_minute_of_day`, `end_minute_of_day`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maxSessionDuration", + "columnName": "max_session_duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sessionPauseDuration", + "columnName": "session_pause_duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startMinuteOfDay", + "columnName": "start_minute_of_day", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endMinuteOfDay", + "columnName": "end_minute_of_day", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUsage", + "columnName": "last_usage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSessionDuration", + "columnName": "last_session_duration", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "category_id", + "max_session_duration", + "session_pause_duration", + "start_minute_of_day", + "end_minute_of_day" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "session_duration_index_category_id", + "unique": false, + "columnNames": [ + "category_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `session_duration_index_category_id` ON `${TABLE_NAME}` (`category_id`)" + } + ], + "foreignKeys": [ + { + "table": "category", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "category_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_limit_login_category", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `category_id` TEXT NOT NULL, `pre_block_duration` INTEGER NOT NULL DEFAULT 0, 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 )", + "fields": [ + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "preBlockDuration", + "columnName": "pre_block_duration", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "user_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "user_limit_login_category_index_category_id", + "unique": false, + "columnNames": [ + "category_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `user_limit_login_category_index_category_id` ON `${TABLE_NAME}` (`category_id`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "category", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "category_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "category_network_id", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )", + "fields": [ + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "networkItemId", + "columnName": "network_item_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hashedNetworkId", + "columnName": "hashed_network_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "category_id", + "network_item_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "category", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "category_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "child_task", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )", + "fields": [ + { + "fieldPath": "taskId", + "columnName": "task_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "taskTitle", + "columnName": "task_title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extraTimeDuration", + "columnName": "extra_time_duration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pendingRequest", + "columnName": "pending_request", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastGrantTimestamp", + "columnName": "last_grant_timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "task_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "category", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "category_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "category_time_warning", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )", + "fields": [ + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "minutes", + "columnName": "minutes", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "category_id", + "minutes" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "category", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "category_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4b90ef1fc32072c93f68fecee1bd7864')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/data/Migrations.kt b/app/src/main/java/io/timelimit/android/data/Migrations.kt index 55f7009..be0d621 100644 --- a/app/src/main/java/io/timelimit/android/data/Migrations.kt +++ b/app/src/main/java/io/timelimit/android/data/Migrations.kt @@ -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 + ) } diff --git a/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt b/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt index 9986b43..4c6dd13 100644 --- a/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt +++ b/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt @@ -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) { diff --git a/app/src/main/java/io/timelimit/android/data/dao/ConfigDao.kt b/app/src/main/java/io/timelimit/android/data/dao/ConfigDao.kt index 1989728..11f467a 100644 --- a/app/src/main/java/io/timelimit/android/data/dao/ConfigDao.kt +++ b/app/src/main/java/io/timelimit/android/data/dao/ConfigDao.kt @@ -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 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) + ) + } } diff --git a/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt b/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt index ca3a351..adfa220 100644 --- a/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt +++ b/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt @@ -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() } } @@ -251,4 +256,8 @@ object ExperimentalFlags { // const val INSTANCE_ID_FG_APP_DETECTION = 65536L // private const val OBSOLETE_DISABLE_FG_APP_DETECTION_FALLBACK = 131072L const val STRICT_OVERLAY_CHECKING = 0x40000L +} + +object ConsentFlags { + const val APP_LIST_SYNC = 1L } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/data/model/derived/DeviceRelatedData.kt b/app/src/main/java/io/timelimit/android/data/model/derived/DeviceRelatedData.kt index 6bee055..e168684 100644 --- a/app/src/main/java/io/timelimit/android/data/model/derived/DeviceRelatedData.kt +++ b/app/src/main/java/io/timelimit/android/data/model/derived/DeviceRelatedData.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * TimeLimit Copyright 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, - 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)) } diff --git a/app/src/main/java/io/timelimit/android/livedata/MergeLiveData.kt b/app/src/main/java/io/timelimit/android/livedata/MergeLiveData.kt index fff94d7..39473ec 100644 --- a/app/src/main/java/io/timelimit/android/livedata/MergeLiveData.kt +++ b/app/src/main/java/io/timelimit/android/livedata/MergeLiveData.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2021 Jonas Lochmann + * TimeLimit Copyright 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 @@ -236,5 +236,50 @@ fun mergeLiveDataWaitForValues(d1: LiveData, d2: LiveData mergeLiveDataWaitForValues(d1: LiveData, d2: LiveData, d3: LiveData, d4: LiveData, d5: LiveData): LiveData> { + val result = MediatorLiveData>() + var state = FiveTuple, Option, Option, Option, Option>(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 } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/logic/AppLogic.kt b/app/src/main/java/io/timelimit/android/logic/AppLogic.kt index cfa932f..9ab487e 100644 --- a/app/src/main/java/io/timelimit/android/logic/AppLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/AppLogic.kt @@ -54,7 +54,7 @@ class AppLogic( } }.ignoreUnchanged() - val deviceEntryIfEnabled = enable.switchMap { + val deviceEntryIfEnabled: LiveData = 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) } diff --git a/app/src/main/java/io/timelimit/android/logic/SyncInstalledAppsLogic.kt b/app/src/main/java/io/timelimit/android/logic/SyncInstalledAppsLogic.kt index 6ffae55..9eee578 100644 --- a/app/src/main/java/io/timelimit/android/logic/SyncInstalledAppsLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/SyncInstalledAppsLogic.kt @@ -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 + } } diff --git a/app/src/main/java/io/timelimit/android/ui/consent/SyncAppListConsentDialogFragment.kt b/app/src/main/java/io/timelimit/android/ui/consent/SyncAppListConsentDialogFragment.kt new file mode 100644 index 0000000..6317ca3 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/consent/SyncAppListConsentDialogFragment.kt @@ -0,0 +1,59 @@ +/* + * TimeLimit Copyright 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 . + */ +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) +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/category/Adapter.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/category/Adapter.kt index 4a52856..dfad4ff 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/child/category/Adapter.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/category/Adapter.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * TimeLimit Copyright 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() { 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? by Delegates.observable(null as List?) { _, _, _ -> notifyDataSetChanged() } @@ -53,6 +55,7 @@ class Adapter: RecyclerView.Adapter() { CreateCategoryItem -> item.hashCode() CategoriesIntroductionHeader -> item.hashCode() ManipulationWarningCategoryItem -> item.hashCode() + ManageChildCategoriesListItem.SyncAppListBanner -> item.hashCode() }.toLong() } @@ -61,6 +64,7 @@ class Adapter: RecyclerView.Adapter() { 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() { .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() { 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() } diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/category/Items.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/category/Items.kt index 4d34c8c..ec6cb25 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/child/category/Items.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/category/Items.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * TimeLimit Copyright 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() diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/category/ManageChildCategoriesFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/category/ManageChildCategoriesFragment.kt index 1e7942c..3b1539f 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/child/category/ManageChildCategoriesFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/category/ManageChildCategoriesFragment.kt @@ -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 diff --git a/app/src/main/java/io/timelimit/android/ui/manage/child/category/ManageChildCategoriesModel.kt b/app/src/main/java/io/timelimit/android/ui/manage/child/category/ManageChildCategoriesModel.kt index 455b3df..b2c436d 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/child/category/ManageChildCategoriesModel.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/child/category/ManageChildCategoriesModel.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2020 Jonas Lochmann + * TimeLimit Copyright 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() + + 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) } } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/setup/device/SetupDeviceFragment.kt b/app/src/main/java/io/timelimit/android/ui/setup/device/SetupDeviceFragment.kt index 22fb7ac..e37a589 100644 --- a/app/src/main/java/io/timelimit/android/ui/setup/device/SetupDeviceFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/setup/device/SetupDeviceFragment.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 - 2021 Jonas Lochmann + * TimeLimit Copyright 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() private val selectedAppsToNotWhitelist = mutableSetOf() 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>() @@ -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>() @@ -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().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) ) } diff --git a/app/src/main/java/io/timelimit/android/ui/setup/device/SetupDeviceModel.kt b/app/src/main/java/io/timelimit/android/ui/setup/device/SetupDeviceModel.kt index 6d32761..a432e2d 100644 --- a/app/src/main/java/io/timelimit/android/ui/setup/device/SetupDeviceModel.kt +++ b/app/src/main/java/io/timelimit/android/ui/setup/device/SetupDeviceModel.kt @@ -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().apply { value = SetupDeviceModelStatus.Ready } @@ -48,7 +57,8 @@ class SetupDeviceModel(application: Application): AndroidViewModel(application) appsToNotWhitelist: Set, 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() - var realUserId = userId - var realAllowedAppsCategory = allowedAppsCategory - val defaultCategories = DefaultCategories.with(getApplication()) + try { + val actions = mutableListOf() + 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 } } diff --git a/app/src/main/res/layout/app_list_sync_permission_request_card.xml b/app/src/main/res/layout/app_list_sync_permission_request_card.xml new file mode 100644 index 0000000..e255b92 --- /dev/null +++ b/app/src/main/res/layout/app_list_sync_permission_request_card.xml @@ -0,0 +1,50 @@ + + + + + + + + + + +