diff --git a/app/schemas/io.timelimit.android.data.RoomDatabase/45.json b/app/schemas/io.timelimit.android.data.RoomDatabase/45.json new file mode 100644 index 0000000..fb0185c --- /dev/null +++ b/app/schemas/io.timelimit.android.data.RoomDatabase/45.json @@ -0,0 +1,1734 @@ +{ + "formatVersion": 1, + "database": { + "version": 45, + "identityHash": "cd8c9703a2467f95e13b2e79b1117bde", + "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, `manipulation_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": "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 + }, + { + "fieldPath": "manipulationFlags", + "columnName": "manipulation_flags", + "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" + ] + } + ] + }, + { + "tableName": "crypt_container_metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`crypt_container_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `device_id` TEXT, `category_id` TEXT, `type` INTEGER NOT NULL, `server_version` TEXT NOT NULL, `current_generation` INTEGER NOT NULL, `current_generation_first_timestamp` INTEGER NOT NULL, `next_counter` INTEGER NOT NULL, `current_generation_key` BLOB, `status` INTEGER NOT NULL, FOREIGN KEY(`device_id`) REFERENCES `device`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "cryptContainerId", + "columnName": "crypt_container_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverVersion", + "columnName": "server_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentGeneration", + "columnName": "current_generation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentGenerationFirstTimestamp", + "columnName": "current_generation_first_timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "nextCounter", + "columnName": "next_counter", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentGenerationKey", + "columnName": "current_generation_key", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "crypt_container_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_crypt_container_metadata_device_id", + "unique": false, + "columnNames": [ + "device_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_crypt_container_metadata_device_id` ON `${TABLE_NAME}` (`device_id`)" + }, + { + "name": "index_crypt_container_metadata_category_id", + "unique": false, + "columnNames": [ + "category_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_crypt_container_metadata_category_id` ON `${TABLE_NAME}` (`category_id`)" + } + ], + "foreignKeys": [ + { + "table": "device", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "device_id" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "category", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "category_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "crypt_container_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`crypt_container_id` INTEGER NOT NULL, `encrypted_data` BLOB NOT NULL, PRIMARY KEY(`crypt_container_id`), FOREIGN KEY(`crypt_container_id`) REFERENCES `crypt_container_metadata`(`crypt_container_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "cryptContainerId", + "columnName": "crypt_container_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encryptedData", + "columnName": "encrypted_data", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "crypt_container_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "crypt_container_metadata", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "crypt_container_id" + ], + "referencedColumns": [ + "crypt_container_id" + ] + } + ] + }, + { + "tableName": "crypt_container_pending_key_request", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`crypt_container_id` INTEGER NOT NULL, `request_time_crypt_container_generation` INTEGER NOT NULL, `request_sequence_id` INTEGER NOT NULL, `request_key` BLOB NOT NULL, PRIMARY KEY(`crypt_container_id`), FOREIGN KEY(`crypt_container_id`) REFERENCES `crypt_container_metadata`(`crypt_container_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "cryptContainerId", + "columnName": "crypt_container_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requestTimeCryptContainerGeneration", + "columnName": "request_time_crypt_container_generation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requestSequenceId", + "columnName": "request_sequence_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requestKey", + "columnName": "request_key", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "crypt_container_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_crypt_container_pending_key_request_request_sequence_id", + "unique": true, + "columnNames": [ + "request_sequence_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_crypt_container_pending_key_request_request_sequence_id` ON `${TABLE_NAME}` (`request_sequence_id`)" + } + ], + "foreignKeys": [ + { + "table": "crypt_container_metadata", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "crypt_container_id" + ], + "referencedColumns": [ + "crypt_container_id" + ] + } + ] + }, + { + "tableName": "crypt_container_key_result", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`request_sequence_id` INTEGER NOT NULL, `device_id` TEXT NOT NULL, `status` INTEGER NOT NULL, PRIMARY KEY(`request_sequence_id`, `device_id`), FOREIGN KEY(`request_sequence_id`) REFERENCES `crypt_container_pending_key_request`(`request_sequence_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`device_id`) REFERENCES `device`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "requestSequenceId", + "columnName": "request_sequence_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "request_sequence_id", + "device_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_crypt_container_key_result_request_sequence_id", + "unique": false, + "columnNames": [ + "request_sequence_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_crypt_container_key_result_request_sequence_id` ON `${TABLE_NAME}` (`request_sequence_id`)" + }, + { + "name": "index_crypt_container_key_result_device_id", + "unique": false, + "columnNames": [ + "device_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_crypt_container_key_result_device_id` ON `${TABLE_NAME}` (`device_id`)" + } + ], + "foreignKeys": [ + { + "table": "crypt_container_pending_key_request", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "request_sequence_id" + ], + "referencedColumns": [ + "request_sequence_id" + ] + }, + { + "table": "device", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "device_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "device_public_key", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `public_key` BLOB NOT NULL, `next_sequence_number` INTEGER NOT NULL, PRIMARY KEY(`device_id`), FOREIGN KEY(`device_id`) REFERENCES `device`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "public_key", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "nextSequenceNumber", + "columnName": "next_sequence_number", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "device_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "device", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "device_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "user_u2f_key", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `added_at` INTEGER NOT NULL, `key_handle` BLOB NOT NULL, `public_key` BLOB NOT NULL, `next_counter` INTEGER NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "keyId", + "columnName": "key_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addedAt", + "columnName": "added_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keyHandle", + "columnName": "key_handle", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "publicKey", + "columnName": "public_key", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "nextCounter", + "columnName": "next_counter", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_user_u2f_key_key_handle_public_key", + "unique": true, + "columnNames": [ + "key_handle", + "public_key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_user_u2f_key_key_handle_public_key` ON `${TABLE_NAME}` (`key_handle`, `public_key`)" + }, + { + "name": "index_user_u2f_key_user_id", + "unique": false, + "columnNames": [ + "user_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_user_u2f_key_user_id` ON `${TABLE_NAME}` (`user_id`)" + } + ], + "foreignKeys": [ + { + "table": "user", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "user_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "widget_category", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`widget_id` INTEGER NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`widget_id`, `category_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "widgetId", + "columnName": "widget_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "widget_id", + "category_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_widget_category_category_id", + "unique": false, + "columnNames": [ + "category_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_widget_category_category_id` ON `${TABLE_NAME}` (`category_id`)" + } + ], + "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, 'cd8c9703a2467f95e13b2e79b1117bde')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5f31d7c..cae3804 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -112,6 +112,14 @@ android:taskAffinity=":update" android:launchMode="singleTop" /> + + + + + + diff --git a/app/src/main/java/io/timelimit/android/data/Database.kt b/app/src/main/java/io/timelimit/android/data/Database.kt index e7544c9..c786f51 100644 --- a/app/src/main/java/io/timelimit/android/data/Database.kt +++ b/app/src/main/java/io/timelimit/android/data/Database.kt @@ -46,6 +46,7 @@ interface Database { fun cryptContainerKeyResult(): CryptContainerKeyResultDao fun deviceKey(): DeviceKeyDao fun u2f(): U2FDao + fun widgetCategory(): WidgetCategoryDao fun runInTransaction(block: () -> T): T fun runInUnobservedTransaction(block: () -> T): T 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 695c618..4419806 100644 --- a/app/src/main/java/io/timelimit/android/data/Migrations.kt +++ b/app/src/main/java/io/timelimit/android/data/Migrations.kt @@ -326,6 +326,13 @@ object DatabaseMigrations { } } + val MIGRATE_TO_V45 = object: Migration(44, 45) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `widget_category` (`widget_id` INTEGER NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`widget_id`, `category_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )") + database.execSQL("CREATE INDEX IF NOT EXISTS `index_widget_category_category_id` ON `widget_category` (`category_id`)") + } + } + val ALL = arrayOf( MIGRATE_TO_V2, MIGRATE_TO_V3, @@ -369,6 +376,7 @@ object DatabaseMigrations { MIGRATE_TO_V41, MIGRATE_TO_V42, MIGRATE_TP_V43, - MIGRATE_TO_V44 + MIGRATE_TO_V44, + MIGRATE_TO_V45 ) } 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 99b2acc..0e24449 100644 --- a/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt +++ b/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt @@ -58,8 +58,9 @@ import java.util.concurrent.TimeUnit CryptContainerPendingKeyRequest::class, CryptContainerKeyResult::class, DevicePublicKey::class, - UserU2FKey::class -], version = 44) + UserU2FKey::class, + WidgetCategory::class +], version = 45) abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database { companion object { private val lock = Object() diff --git a/app/src/main/java/io/timelimit/android/data/dao/WidgetCategoryDao.kt b/app/src/main/java/io/timelimit/android/data/dao/WidgetCategoryDao.kt new file mode 100644 index 0000000..b54f0e3 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/data/dao/WidgetCategoryDao.kt @@ -0,0 +1,47 @@ +/* + * 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.data.dao + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import io.timelimit.android.data.model.WidgetCategory + +@Dao +interface WidgetCategoryDao { + @Query("DELETE FROM widget_category WHERE widget_id = :widgetId") + fun deleteByWidgetId(widgetId: Int) + + @Query("DELETE FROM widget_category WHERE widget_id IN (:widgetIds)") + fun deleteByWidgetIds(widgetIds: IntArray) + + @Query("DELETE FROM widget_category WHERE widget_id = :widgetId AND category_id IN (:categoryIds)") + fun deleteByWidgetIdAndCategoryIds(widgetId: Int, categoryIds: List) + + @Query("DELETE FROM widget_category") + fun deleteAll() + + @Query("SELECT * FROM widget_category") + fun queryLive(): LiveData> + + @Query("SELECT category_id FROM widget_category WHERE widget_id = :widgetId") + fun queryByWidgetIdSync(widgetId: Int): List + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insert(items: List) +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/data/model/WidgetCategory.kt b/app/src/main/java/io/timelimit/android/data/model/WidgetCategory.kt new file mode 100644 index 0000000..58bd3f5 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/data/model/WidgetCategory.kt @@ -0,0 +1,40 @@ +/* + * 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.data.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey + +@Entity( + tableName = "widget_category", + primaryKeys = ["widget_id", "category_id"], + foreignKeys = [ + ForeignKey( + entity = Category::class, + parentColumns = ["id"], + childColumns = ["category_id"], + onUpdate = ForeignKey.CASCADE, + onDelete = ForeignKey.CASCADE + ) + ] +) +data class WidgetCategory ( + @ColumnInfo(name = "widget_id") + val widgetId: Int, + @ColumnInfo(name = "category_id", index = true) + val categoryId: String +) \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/BackgroundActionService.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/BackgroundActionService.kt index d458f22..df56620 100644 --- a/app/src/main/java/io/timelimit/android/integration/platform/android/BackgroundActionService.kt +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/BackgroundActionService.kt @@ -1,5 +1,5 @@ /* - * TimeLimit Copyright 2019 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 @@ -44,8 +44,13 @@ class BackgroundActionService: Service() { fun prepareRevokeTemporarilyAllowed(context: Context) = Intent(context, BackgroundActionService::class.java) .putExtra(ACTION, ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS) - fun prepareSwitchToDefaultUser(context: Context) = Intent(context, BackgroundActionService::class.java) - .putExtra(ACTION, ACTION_SWITCH_TO_DEFAULT_USER) + fun getSwitchToDefaultUserIntent(context: Context) = PendingIntent.getService( + context, + PendingIntentIds.SWITCH_TO_DEFAULT_USER, + Intent(context, BackgroundActionService::class.java) + .putExtra(ACTION, ACTION_SWITCH_TO_DEFAULT_USER), + PendingIntentIds.PENDING_INTENT_FLAGS + ) fun prepareDismissNotification(context: Context, type: Int, id: String) = Intent(context, BackgroundActionService::class.java) .putExtra(ACTION, ACTION_DISMISS_NOTIFICATION) diff --git a/app/src/main/java/io/timelimit/android/integration/platform/android/BackgroundService.kt b/app/src/main/java/io/timelimit/android/integration/platform/android/BackgroundService.kt index 80cbd52..63b6d8d 100644 --- a/app/src/main/java/io/timelimit/android/integration/platform/android/BackgroundService.kt +++ b/app/src/main/java/io/timelimit/android/integration/platform/android/BackgroundService.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,6 @@ package io.timelimit.android.integration.platform.android import android.app.ActivityManager import android.app.NotificationManager -import android.app.PendingIntent import android.app.Service import android.content.Context import android.content.Intent @@ -89,12 +88,7 @@ class BackgroundService: Service() { NotificationCompat.Action.Builder( R.drawable.ic_account_circle_black_24dp, context.getString(R.string.manage_device_default_user_switch_btn), - PendingIntent.getService( - context, - PendingIntentIds.SWITCH_TO_DEFAULT_USER, - BackgroundActionService.prepareSwitchToDefaultUser(context), - PendingIntentIds.PENDING_INTENT_FLAGS - ) + BackgroundActionService.getSwitchToDefaultUserIntent(context) ).build() ) } 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 c4f76d4..e98c472 100644 --- a/app/src/main/java/io/timelimit/android/logic/AppLogic.kt +++ b/app/src/main/java/io/timelimit/android/logic/AppLogic.kt @@ -30,6 +30,7 @@ import io.timelimit.android.sync.SyncUtil import io.timelimit.android.sync.network.api.ServerApi import io.timelimit.android.sync.websocket.NetworkStatusInterface import io.timelimit.android.sync.websocket.WebsocketClientCreator +import io.timelimit.android.ui.widget.TimesWidgetProvider class AppLogic( val platformIntegration: PlatformIntegration, @@ -98,6 +99,7 @@ class AppLogic( init { WatchdogLogic(this) + TimesWidgetProvider.triggerUpdates(context) } val suspendAppsLogic = SuspendAppsLogic(this) diff --git a/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetConfig.kt b/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetConfig.kt new file mode 100644 index 0000000..02685e4 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetConfig.kt @@ -0,0 +1,24 @@ +/* + * 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.widget + +import io.timelimit.android.data.model.WidgetCategory + +data class TimesWidgetConfig (val categories: List) { + val widgetCategoriesByWidgetId by lazy { + categories.groupBy { it.widgetId }.mapValues { it.value.map { it.categoryId }.toSet() } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetContent.kt b/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetContent.kt new file mode 100644 index 0000000..3e64f3b --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetContent.kt @@ -0,0 +1,32 @@ +/* + * 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.widget + +sealed class TimesWidgetContent { + object UnconfiguredDevice: TimesWidgetContent() + data class NoChildUser(val canSwitchToDefaultUser: Boolean): TimesWidgetContent() + data class Categories( + val categories: List, + val canSwitchToDefaultUser: Boolean + ): TimesWidgetContent() { + data class Item( + val categoryId: String, + val categoryName: String, + val level: Int, + val remainingTimeToday: Long? + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetContentLoader.kt b/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetContentLoader.kt new file mode 100644 index 0000000..dd27153 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetContentLoader.kt @@ -0,0 +1,135 @@ +/* + * 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.widget + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData +import io.timelimit.android.async.Threads +import io.timelimit.android.data.extensions.sortedCategories +import io.timelimit.android.data.model.UserType +import io.timelimit.android.livedata.ignoreUnchanged +import io.timelimit.android.logic.AppLogic +import io.timelimit.android.logic.RealTime +import io.timelimit.android.logic.blockingreason.CategoryHandlingCache + +object TimesWidgetContentLoader { + fun with(logic: AppLogic): LiveData { + val database = logic.database + val realTimeLogic = logic.realTimeLogic + val realTime = RealTime.newInstance() + val categoryHandlingCache = CategoryHandlingCache() + val handler = Threads.mainThreadHandler + + val deviceAndUserRelatedDataLive = database.derivedDataDao().getUserAndDeviceRelatedDataLive() + var deviceAndUserRelatedDataLiveLoaded = false + + val batteryStatusLive = logic.platformIntegration.getBatteryStatusLive() + + lateinit var timeModificationListener: () -> Unit + lateinit var updateByClockRunnable: Runnable + var isActive = false + + val newResult = object: MediatorLiveData() { + override fun onActive() { + super.onActive() + + isActive = true + + realTimeLogic.registerTimeModificationListener(timeModificationListener) + + // ensure that the next update gets scheduled + updateByClockRunnable.run() + } + + override fun onInactive() { + super.onInactive() + + isActive = true + + realTimeLogic.unregisterTimeModificationListener(timeModificationListener) + handler.removeCallbacks(updateByClockRunnable) + } + } + + fun update() { + handler.removeCallbacks(updateByClockRunnable) + + if (!deviceAndUserRelatedDataLiveLoaded) { return } + + val deviceAndUserRelatedData = deviceAndUserRelatedDataLive.value + val userRelatedData = deviceAndUserRelatedData?.userRelatedData + val canSwitchToDefaultUser = deviceAndUserRelatedData?.deviceRelatedData?.canSwitchToDefaultUser ?: false + + if (deviceAndUserRelatedData == null) { + newResult.value = TimesWidgetContent.UnconfiguredDevice + + return + } else if (userRelatedData?.user?.type != UserType.Child) { + newResult.value = TimesWidgetContent.NoChildUser( + canSwitchToDefaultUser = canSwitchToDefaultUser + ) + + return + } + + realTimeLogic.getRealTime(realTime) + + categoryHandlingCache.reportStatus( + user = userRelatedData, + timeInMillis = realTime.timeInMillis, + batteryStatus = logic.platformIntegration.getBatteryStatus(), + shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily, + assumeCurrentDevice = true, + currentNetworkId = null, // not relevant here + hasPremiumOrLocalMode = false // not relevant here + ) + + var maxTime = Long.MAX_VALUE + + val categories = userRelatedData.sortedCategories().map { (level, category) -> + val handling = categoryHandlingCache.get(categoryId = category.category.id) + + maxTime = maxTime.coerceAtMost(handling.dependsOnMaxTime) + + TimesWidgetContent.Categories.Item( + categoryId = category.category.id, + categoryName = category.category.title, + level = level, + remainingTimeToday = handling.remainingTime?.includingExtraTime + ) + } + + newResult.value = TimesWidgetContent.Categories( + categories = categories, + canSwitchToDefaultUser = canSwitchToDefaultUser + ) + + if (isActive && maxTime != Long.MAX_VALUE) { + val delay = maxTime - realTime.timeInMillis + + handler.postDelayed(updateByClockRunnable, delay) + } + } + + timeModificationListener = { update() } + updateByClockRunnable = Runnable { update() } + + newResult.addSource(deviceAndUserRelatedDataLive) { deviceAndUserRelatedDataLiveLoaded = true; update() } + newResult.addSource(batteryStatusLive) { update() } + + return newResult.ignoreUnchanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetItem.kt b/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetItem.kt index 47ce81b..985c5a4 100644 --- a/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetItem.kt +++ b/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetItem.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 @@ -15,8 +15,8 @@ */ package io.timelimit.android.ui.widget -data class TimesWidgetItem( - val title: String, - val level: Int, - val remainingTimeToday: Long? -) \ No newline at end of file +sealed class TimesWidgetItem { + data class TextMessage(val textRessourceId: Int): TimesWidgetItem() + data class Category(val category: TimesWidgetContent.Categories.Item): TimesWidgetItem() + object DefaultUserButton: TimesWidgetItem() +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetItems.kt b/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetItems.kt index dc78ac8..4ff43bd 100644 --- a/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetItems.kt +++ b/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetItems.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 @@ -15,107 +15,31 @@ */ package io.timelimit.android.ui.widget -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import io.timelimit.android.async.Threads -import io.timelimit.android.data.extensions.sortedCategories -import io.timelimit.android.livedata.ignoreUnchanged -import io.timelimit.android.logic.AppLogic -import io.timelimit.android.logic.RealTime -import io.timelimit.android.logic.blockingreason.CategoryHandlingCache +import io.timelimit.android.R object TimesWidgetItems { - fun with(logic: AppLogic): LiveData> { - val database = logic.database - val realTimeLogic = logic.realTimeLogic - val categoryHandlingCache = CategoryHandlingCache() - val realTime = RealTime.newInstance() - val handler = Threads.mainThreadHandler + fun with(content: TimesWidgetContent, config: TimesWidgetConfig, appWidgetId: Int): List = when (content) { + is TimesWidgetContent.UnconfiguredDevice -> listOf(TimesWidgetItem.TextMessage(R.string.widget_msg_unconfigured)) + is TimesWidgetContent.NoChildUser -> { + val base = TimesWidgetItem.TextMessage(R.string.widget_msg_no_child) - val deviceAndUserRelatedDataLive = database.derivedDataDao().getUserAndDeviceRelatedDataLive() - var deviceAndUserRelatedDataLiveLoaded = false - - val batteryStatusLive = logic.platformIntegration.getBatteryStatusLive() - - lateinit var timeModificationListener: () -> Unit - lateinit var updateByClockRunnable: Runnable - var isActive = false - - val newResult = object: MediatorLiveData>() { - override fun onActive() { - super.onActive() - - isActive = true - - realTimeLogic.registerTimeModificationListener(timeModificationListener) - - // ensure that the next update gets scheduled - updateByClockRunnable.run() - } - - override fun onInactive() { - super.onInactive() - - isActive = true - - realTimeLogic.unregisterTimeModificationListener(timeModificationListener) - handler.removeCallbacks(updateByClockRunnable) - } + if (content.canSwitchToDefaultUser) listOf(base, TimesWidgetItem.DefaultUserButton) + else listOf(base) } + is TimesWidgetContent.Categories -> { + val categoryFilter = config.widgetCategoriesByWidgetId[appWidgetId] ?: emptySet() - fun update() { - handler.removeCallbacks(updateByClockRunnable) + val categoryItems = if (content.categories.isEmpty()) listOf(TimesWidgetItem.TextMessage(R.string.widget_msg_no_category)) + else if (categoryFilter.isEmpty()) content.categories.map { TimesWidgetItem.Category(it) } + else { + val filteredCategories = content.categories.filter { categoryFilter.contains(it.categoryId) } - if (!deviceAndUserRelatedDataLiveLoaded) { return } - - val deviceAndUserRelatedData = deviceAndUserRelatedDataLive.value - val userRelatedData = deviceAndUserRelatedData?.userRelatedData - - if (userRelatedData == null) { - newResult.value = emptyList(); return + if (filteredCategories.isEmpty()) listOf(TimesWidgetItem.TextMessage(R.string.widget_msg_no_filtered_category)) + else filteredCategories.map { TimesWidgetItem.Category(it) } } - realTimeLogic.getRealTime(realTime) - - categoryHandlingCache.reportStatus( - user = userRelatedData, - timeInMillis = realTime.timeInMillis, - batteryStatus = logic.platformIntegration.getBatteryStatus(), - shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily, - assumeCurrentDevice = true, - currentNetworkId = null, // not relevant here - hasPremiumOrLocalMode = false // not relevant here - ) - - var maxTime = Long.MAX_VALUE - - val list = userRelatedData.sortedCategories().map { (level, category) -> - val handling = categoryHandlingCache.get(categoryId = category.category.id) - - maxTime = maxTime.coerceAtMost(handling.dependsOnMaxTime) - - TimesWidgetItem( - title = category.category.title, - level = level, - remainingTimeToday = handling.remainingTime?.includingExtraTime - ) - } - - newResult.value = list - - if (isActive && maxTime != Long.MAX_VALUE) { - val delay = maxTime - realTime.timeInMillis - - handler.postDelayed(updateByClockRunnable, delay) - } + if (content.canSwitchToDefaultUser) categoryItems + TimesWidgetItem.DefaultUserButton + else categoryItems } - - timeModificationListener = { update() } - updateByClockRunnable = Runnable { update() } - - newResult.addSource(deviceAndUserRelatedDataLive) { deviceAndUserRelatedDataLiveLoaded = true; update() } - newResult.addSource(batteryStatusLive) { update() } - - return newResult.ignoreUnchanged() } } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetProvider.kt b/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetProvider.kt index 53f1811..a1656a6 100644 --- a/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetProvider.kt +++ b/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetProvider.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,13 +17,45 @@ package io.timelimit.android.ui.widget import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider +import android.content.ComponentName import android.content.Context import android.content.Intent +import android.util.Log import android.widget.RemoteViews +import androidx.core.content.getSystemService +import io.timelimit.android.BuildConfig import io.timelimit.android.R +import io.timelimit.android.async.Threads +import io.timelimit.android.integration.platform.android.BackgroundActionService import io.timelimit.android.logic.DefaultAppLogic class TimesWidgetProvider: AppWidgetProvider() { + companion object { + private const val LOG_TAG = "TimesWidgetProvider" + + private fun handleUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { + for (appWidgetId in appWidgetIds) { + val views = RemoteViews(context.packageName, R.layout.widget_times) + + views.setRemoteAdapter(android.R.id.list, TimesWidgetService.intent(context, appWidgetId)) + views.setPendingIntentTemplate(android.R.id.list, BackgroundActionService.getSwitchToDefaultUserIntent(context)) + views.setEmptyView(android.R.id.list, android.R.id.empty) + + appWidgetManager.updateAppWidget(appWidgetId, views) + } + + TimesWidgetService.notifyContentChanges(context) + } + + fun triggerUpdates(context: Context) { + context.getSystemService()?.also { appWidgetManager -> + val appWidgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, TimesWidgetProvider::class.java)) + + handleUpdate(context, appWidgetManager, appWidgetIds) + } + } + } + override fun onReceive(context: Context, intent: Intent?) { super.onReceive(context, intent) @@ -34,13 +66,38 @@ class TimesWidgetProvider: AppWidgetProvider() { override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { super.onUpdate(context, appWidgetManager, appWidgetIds) - for (appWidgetId in appWidgetIds) { - val views = RemoteViews(context.packageName, R.layout.widget_times) + handleUpdate(context, appWidgetManager, appWidgetIds) + } - views.setRemoteAdapter(android.R.id.list, Intent(context, TimesWidgetService::class.java)) - views.setEmptyView(android.R.id.list, android.R.id.empty) + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + super.onDeleted(context, appWidgetIds) - appWidgetManager.updateAppWidget(appWidgetId, views) + val database = DefaultAppLogic.with(context).database + + Threads.database.execute { + try { + database.widgetCategory().deleteByWidgetIds(appWidgetIds) + } catch (ex: Exception) { + if (BuildConfig.DEBUG) { + Log.d(LOG_TAG, "onDisabled", ex) + } + } + } + } + + override fun onDisabled(context: Context) { + super.onDisabled(context) + + val database = DefaultAppLogic.with(context).database + + Threads.database.execute { + try { + database.widgetCategory().deleteAll() + } catch (ex: Exception) { + if (BuildConfig.DEBUG) { + Log.d(LOG_TAG, "onDisabled", ex) + } + } } } } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetService.kt b/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetService.kt index 4b619f3..1d255b4 100644 --- a/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetService.kt +++ b/app/src/main/java/io/timelimit/android/ui/widget/TimesWidgetService.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 @@ -17,101 +17,150 @@ package io.timelimit.android.ui.widget import android.appwidget.AppWidgetManager import android.content.ComponentName +import android.content.Context import android.content.Intent +import android.net.Uri import android.view.View import android.widget.RemoteViews import android.widget.RemoteViewsService +import androidx.core.content.getSystemService import androidx.lifecycle.LiveData import androidx.lifecycle.Observer +import androidx.lifecycle.map import io.timelimit.android.R import io.timelimit.android.async.Threads +import io.timelimit.android.livedata.mergeLiveDataWaitForValues import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.ui.manage.child.category.CategoryItemLeftPadding -import io.timelimit.android.util.TimeTextUtil class TimesWidgetService: RemoteViewsService() { - private val appWidgetManager: AppWidgetManager by lazy { AppWidgetManager.getInstance(this) } + companion object { + private const val EXTRA_APP_WIDGET_ID = "appWidgetId" - private val categoriesLive: LiveData> by lazy { - TimesWidgetItems.with(DefaultAppLogic.with(this)) + fun intent(context: Context, appWidgetId: Int) = Intent(context, TimesWidgetService::class.java) + .setData(Uri.parse("widget:$appWidgetId")) + .putExtra(EXTRA_APP_WIDGET_ID, appWidgetId) + + fun notifyContentChanges(context: Context) { + context.getSystemService()?.also { appWidgetManager -> + val widgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, TimesWidgetProvider::class.java)) + + appWidgetManager.notifyAppWidgetViewDataChanged(widgetIds, android.R.id.list) + } + } } - private var categoriesInput: List = emptyList() - private var categoriesCurrent: List = categoriesInput + private val content: LiveData> by lazy { + val logic = DefaultAppLogic.with(this) - private val categoriesObserver = Observer> { - categoriesInput = it + val content = TimesWidgetContentLoader.with(logic) + val config = logic.database.widgetCategory().queryLive().map { TimesWidgetConfig(it) } - val widgetIds = appWidgetManager.getAppWidgetIds(ComponentName(this, TimesWidgetProvider::class.java)) - - appWidgetManager.notifyAppWidgetViewDataChanged(widgetIds, android.R.id.list) + mergeLiveDataWaitForValues(content, config) } - private val factory = object : RemoteViewsFactory { + private var observerCounter = 0 + private var contentInput: Pair? = null + + private val contentObserver = Observer> { + contentInput = it + + notifyContentChanges(this) + } + + private fun createFactory(appWidgetId: Int) = object : RemoteViewsFactory { + private var currentItems: List = emptyList() + + init { onDataSetChanged() } + override fun onCreate() { - Threads.mainThreadHandler.post { categoriesLive.observeForever(categoriesObserver) } + Threads.mainThreadHandler.post { + if (observerCounter < 0) throw IllegalStateException() + else if (observerCounter == 0) content.observeForever(contentObserver) + + observerCounter++ + } } override fun onDestroy() { - Threads.mainThreadHandler.post { categoriesLive.removeObserver(categoriesObserver) } + Threads.mainThreadHandler.post { + if (observerCounter <= 0) throw IllegalStateException() + else if (observerCounter == 1) content.removeObserver(contentObserver) + + observerCounter-- + } } override fun onDataSetChanged() { - categoriesCurrent = categoriesInput + currentItems = contentInput?.let { TimesWidgetItems.with(it.first, it.second, appWidgetId) } ?: emptyList() } - override fun getCount(): Int = categoriesCurrent.size + override fun getCount(): Int = currentItems.size override fun getViewAt(position: Int): RemoteViews { - if (position >= categoriesCurrent.size) { - return RemoteViews(packageName, R.layout.widget_times_item) + if (position >= currentItems.size) { + return RemoteViews(packageName, R.layout.widget_times_category_item) } - val category = categoriesCurrent[position] - val result = RemoteViews(packageName, R.layout.widget_times_item) + fun createCategoryItem(title: String?, subtitle: String, paddingLeft: Int) = RemoteViews(packageName, R.layout.widget_times_category_item).also { result -> + result.setTextViewText(R.id.title, title ?: "") + result.setTextViewText(R.id.subtitle, subtitle) - result.setTextViewText(R.id.title, category.title) - result.setTextViewText( - R.id.subtitle, - if (category.remainingTimeToday == null) - getString(R.string.manage_child_category_no_time_limits) - else - TimeTextUtil.remaining(category.remainingTimeToday.toInt(), this@TimesWidgetService) - ) + result.setViewPadding(R.id.widgetInnerContainer, paddingLeft, 0, 0, 0) + result.setViewVisibility(R.id.title, if (title != null) View.VISIBLE else View.GONE) + result.setViewVisibility(R.id.topPadding, if (position == 0) View.VISIBLE else View.GONE) + result.setViewVisibility(R.id.bottomPadding, if (position == count - 1) View.VISIBLE else View.GONE) + } - result.setViewPadding( - R.id.widgetInnerContainer, - // not much space here => / 2 - CategoryItemLeftPadding.calculate(category.level, this@TimesWidgetService) / 2, - 0, 0, 0 - ) + val item = currentItems[position] - result.setViewVisibility(R.id.topPadding, if (position == 0) View.VISIBLE else View.GONE) - result.setViewVisibility(R.id.bottomPadding, if (position == count - 1) View.VISIBLE else View.GONE) + return when (item) { + is TimesWidgetItem.Category -> item.category.let { category -> + createCategoryItem( + title = if (category.remainingTimeToday == null) + getString(R.string.manage_child_category_no_time_limits_short) + else { + val remainingTimeToday = category.remainingTimeToday.coerceAtLeast(0) / (1000 * 60) + val minutes = remainingTimeToday % 60 + val hours = remainingTimeToday / 60 - return result + if (hours == 0L) "$minutes m" + else "$hours h $minutes m" + }, + subtitle = category.categoryName, + // not much space here => / 2 + paddingLeft = CategoryItemLeftPadding.calculate(category.level, this@TimesWidgetService) / 2 + ) + } + is TimesWidgetItem.TextMessage -> createCategoryItem(null, getString(item.textRessourceId), 0) + is TimesWidgetItem.DefaultUserButton -> RemoteViews(packageName, R.layout.widget_times_button).also { result -> + result.setTextViewText(R.id.button, getString(R.string.manage_device_default_user_switch_btn)) + result.setOnClickFillInIntent(R.id.button, Intent()) + } + } } - override fun getLoadingView(): RemoteViews? { - return null - } + override fun getLoadingView(): RemoteViews? = null - override fun getViewTypeCount(): Int { - return 1 - } + override fun getViewTypeCount(): Int = 2 override fun getItemId(position: Int): Long { - if (position >= categoriesCurrent.size) { + if (position >= currentItems.size) { return -(position.toLong()) } - return categoriesCurrent[position].hashCode().toLong() + val item = currentItems[position] + + return when (item) { + is TimesWidgetItem.Category -> item.category.categoryId.hashCode() + else -> item.hashCode() + }.toLong() } - override fun hasStableIds(): Boolean { - return true - } + override fun hasStableIds(): Boolean = true } - override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = factory + override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = createFactory( + intent.getIntExtra(EXTRA_APP_WIDGET_ID, 0) + ) } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/widget/config/UnconfiguredDialogFragment.kt b/app/src/main/java/io/timelimit/android/ui/widget/config/UnconfiguredDialogFragment.kt new file mode 100644 index 0000000..c698680 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/widget/config/UnconfiguredDialogFragment.kt @@ -0,0 +1,51 @@ +/* + * 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.widget.config + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import io.timelimit.android.R + +class UnconfiguredDialogFragment: DialogFragment() { + companion object { + const val DIALOG_TAG = "UnconfiguredDialogFragment" + } + + private val model: WidgetConfigModel by activityViewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + model.state.observe(this) { + if (!(it is WidgetConfigModel.State.Unconfigured)) dismissAllowingStateLoss() + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = AlertDialog.Builder(requireContext(), theme) + .setMessage(R.string.widget_msg_unconfigured) + .setPositiveButton(R.string.generic_ok) { _, _ -> model.userCancel() } + .create() + + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + + model.userCancel() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/widget/config/WidgetConfigActivity.kt b/app/src/main/java/io/timelimit/android/ui/widget/config/WidgetConfigActivity.kt new file mode 100644 index 0000000..b1c09e3 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/widget/config/WidgetConfigActivity.kt @@ -0,0 +1,78 @@ +/* + * 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.widget.config + +import android.appwidget.AppWidgetManager +import android.content.Intent +import android.os.Bundle +import android.widget.Toast +import androidx.activity.viewModels +import androidx.fragment.app.FragmentActivity +import io.timelimit.android.R +import io.timelimit.android.extensions.showSafe + +class WidgetConfigActivity: FragmentActivity() { + private val model: WidgetConfigModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (model.state.value == WidgetConfigModel.State.WaitingForInit) { + model.init( + intent?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) + ?: AppWidgetManager.INVALID_APPWIDGET_ID + ) + } + + model.state.observe(this) { state -> + when (state) { + is WidgetConfigModel.State.WaitingForInit -> {/* ignore */} + is WidgetConfigModel.State.Working -> {/* ignore */} + is WidgetConfigModel.State.Unconfigured -> { + if (supportFragmentManager.findFragmentByTag(UnconfiguredDialogFragment.DIALOG_TAG) == null) { + UnconfiguredDialogFragment().showSafe(supportFragmentManager, UnconfiguredDialogFragment.DIALOG_TAG) + } + } + is WidgetConfigModel.State.ShowModeSelection -> { + if (supportFragmentManager.findFragmentByTag(WidgetConfigModeDialogFragment.DIALOG_TAG) == null) { + WidgetConfigModeDialogFragment().showSafe(supportFragmentManager, WidgetConfigModeDialogFragment.DIALOG_TAG) + } + } + is WidgetConfigModel.State.ShowCategorySelection -> { + if (supportFragmentManager.findFragmentByTag(WidgetConfigFilterDialogFragment.DIALOG_TAG) == null) { + WidgetConfigFilterDialogFragment().showSafe(supportFragmentManager, WidgetConfigFilterDialogFragment.DIALOG_TAG) + } + } + is WidgetConfigModel.State.Done -> { + setResult( + RESULT_OK, + Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, state.appWidgetId) + ) + + finish() + } + is WidgetConfigModel.State.ErrorCancel -> { + Toast.makeText(this, R.string.error_general, Toast.LENGTH_SHORT).show() + + finish() + } + is WidgetConfigModel.State.UserCancel -> finish() + } + } + + setResult(RESULT_CANCELED) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/widget/config/WidgetConfigFilterDialogFragment.kt b/app/src/main/java/io/timelimit/android/ui/widget/config/WidgetConfigFilterDialogFragment.kt new file mode 100644 index 0000000..5925efa --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/widget/config/WidgetConfigFilterDialogFragment.kt @@ -0,0 +1,88 @@ +/* + * 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.widget.config + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import io.timelimit.android.R + +class WidgetConfigFilterDialogFragment: DialogFragment() { + companion object { + private const val STATE_CATEGORY_IDS = "categoryIds" + + const val DIALOG_TAG = "WidgetConfigFilterDialogFragment" + } + + private val model: WidgetConfigModel by activityViewModels() + private val selectedCategoryIds = mutableSetOf() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + model.state.value?.also { state -> + if (state is WidgetConfigModel.State.ShowCategorySelection) { + selectedCategoryIds.clear() + selectedCategoryIds.addAll(state.selectedFilterCategories) + } + } + + savedInstanceState?.also { + selectedCategoryIds.clear() + selectedCategoryIds.addAll(it.getStringArray(STATE_CATEGORY_IDS) ?: emptyArray()) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + outState.putStringArray(STATE_CATEGORY_IDS, selectedCategoryIds.toTypedArray()) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val state = model.state.value + + if (!(state is WidgetConfigModel.State.ShowCategorySelection)) return super.onCreateDialog(savedInstanceState) + + return AlertDialog.Builder(requireContext(), theme) + .setMultiChoiceItems( + state.categories.map { it.title }.toTypedArray(), + state.categories.map { selectedCategoryIds.contains(it.id) }.toBooleanArray() + ) { _, index, checked -> + val categoryId = state.categories[index].id + + if (checked) selectedCategoryIds.add(categoryId) else selectedCategoryIds.remove(categoryId) + } + .setPositiveButton(R.string.wiazrd_next) { _, _ -> + if (selectedCategoryIds.isEmpty()) { + Toast.makeText(requireContext(), R.string.widget_config_error_filter_empty, Toast.LENGTH_SHORT).show() + + model.userCancel() + } else model.selectFilterItems(selectedCategoryIds) + } + .create() + } + + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + + model.userCancel() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/widget/config/WidgetConfigModeDialogFragment.kt b/app/src/main/java/io/timelimit/android/ui/widget/config/WidgetConfigModeDialogFragment.kt new file mode 100644 index 0000000..ffe97cd --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/widget/config/WidgetConfigModeDialogFragment.kt @@ -0,0 +1,77 @@ +/* + * 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.widget.config + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import io.timelimit.android.R + +class WidgetConfigModeDialogFragment: DialogFragment() { + companion object { + private const val STATE_SELECTION = "selection" + + const val DIALOG_TAG = "WidgetConfigModeDialogFragment" + } + + private val model: WidgetConfigModel by activityViewModels() + private var selection = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + model.state.value?.also { + if (it is WidgetConfigModel.State.ShowModeSelection) { + selection = if (it.selectedFilterCategories.isEmpty()) 0 else 1 + } + } + + savedInstanceState?.also { selection = it.getInt(STATE_SELECTION) } + + model.state.observe(this) { + if (!(it is WidgetConfigModel.State.ShowModeSelection)) dismissAllowingStateLoss() + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + outState.putInt(STATE_SELECTION, selection) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = AlertDialog.Builder(requireContext(), theme) + .setSingleChoiceItems( + arrayOf( + getString(R.string.widget_config_mode_all), + getString(R.string.widget_config_mode_filter) + ), + selection + ) { _, selectedItemIndex -> selection = selectedItemIndex } + .setPositiveButton(R.string.wiazrd_next) { _, _ -> + if (selection == 0) model.selectModeAll() + else model.selectModeFilter() + } + .create() + + override fun onCancel(dialog: DialogInterface) { + super.onCancel(dialog) + + model.userCancel() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/widget/config/WidgetConfigModel.kt b/app/src/main/java/io/timelimit/android/ui/widget/config/WidgetConfigModel.kt new file mode 100644 index 0000000..98d5858 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/widget/config/WidgetConfigModel.kt @@ -0,0 +1,165 @@ +/* + * 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.widget.config + +import android.app.Application +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import io.timelimit.android.BuildConfig +import io.timelimit.android.async.Threads +import io.timelimit.android.coroutines.executeAndWait +import io.timelimit.android.data.extensions.sortedCategories +import io.timelimit.android.data.model.Category +import io.timelimit.android.data.model.UserType +import io.timelimit.android.data.model.WidgetCategory +import io.timelimit.android.livedata.castDown +import io.timelimit.android.logic.DefaultAppLogic +import kotlinx.coroutines.launch + +class WidgetConfigModel(application: Application): AndroidViewModel(application) { + companion object { + private const val LOG_TAG = "WidgetConfigModel" + } + + private val stateInternal = MutableLiveData().apply { value = State.WaitingForInit } + private val database = DefaultAppLogic.with(application).database + + val state = stateInternal.castDown() + + fun init(appWidgetId: Int) { + if (state.value != State.WaitingForInit) return + stateInternal.value = State.Working + + viewModelScope.launch { + try { + val (deviceAndUserRelatedData, selectedFilterCategories) = Threads.database.executeAndWait { + database.runInTransaction { + val deviceAndUserRelatedData = database.derivedDataDao().getUserAndDeviceRelatedDataSync() + val selectedFilterCategories = database.widgetCategory().queryByWidgetIdSync(appWidgetId).toSet() + + Pair(deviceAndUserRelatedData, selectedFilterCategories) + } + } + + val isNoChildUser = deviceAndUserRelatedData?.userRelatedData?.user?.type != UserType.Child + val currentUserCategories = deviceAndUserRelatedData?.userRelatedData?.sortedCategories()?.map { it.second.category } + + if (currentUserCategories == null || isNoChildUser) { + stateInternal.value = State.Unconfigured + + return@launch + } + + stateInternal.value = State.ShowModeSelection(appWidgetId, selectedFilterCategories, currentUserCategories) + } catch (ex: Exception) { + if (BuildConfig.DEBUG) { + Log.d(LOG_TAG, "selectModeAll", ex) + } + + stateInternal.value = State.ErrorCancel + } + } + } + + fun selectModeAll() { + val oldState = state.value + if (!(oldState is State.ShowModeSelection)) return + stateInternal.value = State.Working + + viewModelScope.launch { + try { + Threads.database.executeAndWait { + database.widgetCategory().deleteByWidgetId(oldState.appWidgetId) + } + + stateInternal.value = State.Done(appWidgetId = oldState.appWidgetId) + } catch (ex: Exception) { + if (BuildConfig.DEBUG) { + Log.d(LOG_TAG, "selectModeAll", ex) + } + + stateInternal.value = State.ErrorCancel + } + } + } + + fun selectModeFilter() { + val oldState = state.value + if (!(oldState is State.ShowModeSelection)) return + stateInternal.value = State.Working + + stateInternal.value = State.ShowCategorySelection( + appWidgetId = oldState.appWidgetId, + selectedFilterCategories = oldState.selectedFilterCategories, + categories = oldState.categories + ) + } + + fun selectFilterItems(selectedCategoryIds: Set) { + val oldState = state.value + if (!(oldState is State.ShowCategorySelection)) return + stateInternal.value = State.Working + + viewModelScope.launch { + try { + Threads.database.executeAndWait { + val userAndDeviceRelatedData = database.derivedDataDao().getUserAndDeviceRelatedDataSync() + val currentCategoryIds = userAndDeviceRelatedData!!.userRelatedData!!.categoryById.keys + + val categoriesToRemove = currentCategoryIds - selectedCategoryIds + val categoriesToAdd = selectedCategoryIds.filter { currentCategoryIds.contains(it) } + + if (categoriesToRemove.isNotEmpty()) { + database.widgetCategory().deleteByWidgetIdAndCategoryIds( + oldState.appWidgetId, categoriesToRemove.toList() + ) + } + + if (categoriesToAdd.isNotEmpty()) { + database.widgetCategory().insert( + categoriesToAdd.toList().map { WidgetCategory(oldState.appWidgetId, it) } + ) + } + } + + stateInternal.value = State.Done(appWidgetId = oldState.appWidgetId) + } catch (ex: Exception) { + if (BuildConfig.DEBUG) { + Log.d(LOG_TAG, "selectModeAll", ex) + } + + stateInternal.value = State.ErrorCancel + } + } + } + + fun userCancel() { + stateInternal.value = State.UserCancel + } + + sealed class State { + object WaitingForInit: State() + object Working: State() + data class ShowModeSelection(val appWidgetId: Int, val selectedFilterCategories: Set, val categories: List): State() + data class ShowCategorySelection(val appWidgetId: Int, val selectedFilterCategories: Set, val categories: List): State() + data class Done(val appWidgetId: Int): State() + object Unconfigured: State() + object UserCancel: State() + object ErrorCancel: State() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-de/widget_preview.png b/app/src/main/res/drawable-de/widget_preview.png index b5d2c55..125126b 100644 Binary files a/app/src/main/res/drawable-de/widget_preview.png and b/app/src/main/res/drawable-de/widget_preview.png differ diff --git a/app/src/main/res/drawable/widget_preview.png b/app/src/main/res/drawable/widget_preview.png index 0cb60f2..fbba680 100644 Binary files a/app/src/main/res/drawable/widget_preview.png and b/app/src/main/res/drawable/widget_preview.png differ diff --git a/app/src/main/res/layout/widget_times.xml b/app/src/main/res/layout/widget_times.xml index 9dc6c37..17e6a4f 100644 --- a/app/src/main/res/layout/widget_times.xml +++ b/app/src/main/res/layout/widget_times.xml @@ -1,23 +1,23 @@ - + android:background="@color/widgetBackground"> - + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="?android:attr/progressBarStyleLarge" + android:layout_gravity="center" /> \ No newline at end of file diff --git a/app/src/main/res/layout/widget_times_button.xml b/app/src/main/res/layout/widget_times_button.xml new file mode 100644 index 0000000..1dcc003 --- /dev/null +++ b/app/src/main/res/layout/widget_times_button.xml @@ -0,0 +1,33 @@ + + + + +