From 5e694f0c1cfaee7adca96deaf974b01546888e5e Mon Sep 17 00:00:00 2001 From: Jonas L Date: Mon, 1 Jul 2019 00:00:00 +0000 Subject: [PATCH] Add new time picker mode --- .../19.json | 759 ++++++++++++++++++ .../io/timelimit/android/data/Migrations.kt | 8 + .../io/timelimit/android/data/RoomDatabase.kt | 5 +- .../timelimit/android/data/dao/ConfigDao.kt | 3 + .../android/data/model/ConfigurationItem.kt | 9 +- .../timelimit/android/ui/lock/LockFragment.kt | 19 + .../settings/CategorySettingsFragment.kt | 20 + .../edit/EditTimeLimitRuleDialogFragment.kt | 18 +- .../android/ui/view/SelectTimeSpanView.kt | 61 +- .../drawable/ic_unfold_more_black_24dp.xml | 9 + .../main/res/layout/view_select_time_span.xml | 168 +++- .../strings-select-time-span-view.xml | 20 + .../values/strings-select-time-span-view.xml | 20 + 13 files changed, 1074 insertions(+), 45 deletions(-) create mode 100644 app/schemas/io.timelimit.android.data.RoomDatabase/19.json create mode 100644 app/src/main/res/drawable/ic_unfold_more_black_24dp.xml create mode 100644 app/src/main/res/values-de/strings-select-time-span-view.xml create mode 100644 app/src/main/res/values/strings-select-time-span-view.xml diff --git a/app/schemas/io.timelimit.android.data.RoomDatabase/19.json b/app/schemas/io.timelimit.android.data.RoomDatabase/19.json new file mode 100644 index 0000000..d654257 --- /dev/null +++ b/app/schemas/io.timelimit.android.data.RoomDatabase/19.json @@ -0,0 +1,759 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "a7aedc16546b129da089b18988dce47f", + "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, 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 + } + ], + "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, `did_report_uninstall` INTEGER NOT NULL, `is_user_kept_signed_in` INTEGER NOT NULL, `show_device_connected` INTEGER NOT NULL, `default_user` TEXT NOT NULL, `default_user_timeout` INTEGER NOT NULL, `consider_reboot_manipulation` INTEGER NOT NULL, `current_overlay_permission` TEXT NOT NULL, `highest_overlay_permission` TEXT NOT NULL, `current_accessibility_service_permission` INTEGER NOT NULL, `was_accessibility_service_permission` INTEGER NOT NULL, `enable_activity_level_blocking` INTEGER NOT NULL, `q_or_later` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "addedAt", + "columnName": "added_at", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentUserId", + "columnName": "current_user_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installedAppsVersion", + "columnName": "apps_version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "networkTime", + "columnName": "network_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentProtectionLevel", + "columnName": "current_protection_level", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highestProtectionLevel", + "columnName": "highest_permission_level", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentUsageStatsPermission", + "columnName": "current_usage_stats_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highestUsageStatsPermission", + "columnName": "highest_usage_stats_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentNotificationAccessPermission", + "columnName": "current_notification_access_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highestNotificationAccessPermission", + "columnName": "highest_notification_access_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "currentAppVersion", + "columnName": "current_app_version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "highestAppVersion", + "columnName": "highest_app_version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "manipulationTriedDisablingDeviceAdmin", + "columnName": "tried_disabling_device_admin", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "manipulationDidReboot", + "columnName": "did_reboot", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hadManipulation", + "columnName": "had_manipulation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "didReportUninstall", + "columnName": "did_report_uninstall", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUserKeptSignedIn", + "columnName": "is_user_kept_signed_in", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "showDeviceConnected", + "columnName": "show_device_connected", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultUser", + "columnName": "default_user", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultUserTimeout", + "columnName": "default_user_timeout", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "considerRebootManipulation", + "columnName": "consider_reboot_manipulation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "currentOverlayPermission", + "columnName": "current_overlay_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "highestOverlayPermission", + "columnName": "highest_overlay_permission", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessibilityServiceEnabled", + "columnName": "current_accessibility_service_permission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wasAccessibilityServiceEnabled", + "columnName": "was_accessibility_service_permission", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableActivityLevelBlocking", + "columnName": "enable_activity_level_blocking", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "qOrLater", + "columnName": "q_or_later", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, `title` TEXT NOT NULL, `launchable` INTEGER NOT NULL, `recommendation` TEXT NOT NULL, PRIMARY KEY(`device_id`, `package_name`))", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "device_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isLaunchable", + "columnName": "launchable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "device_id", + "package_name" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_app_device_id", + "unique": false, + "columnNames": [ + "device_id" + ], + "createSql": "CREATE INDEX `index_app_device_id` ON `${TABLE_NAME}` (`device_id`)" + }, + { + "name": "index_app_package_name", + "unique": false, + "columnNames": [ + "package_name" + ], + "createSql": "CREATE INDEX `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": "packageName", + "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" + ], + "createSql": "CREATE INDEX `index_category_app_category_id` ON `${TABLE_NAME}` (`category_id`)" + }, + { + "name": "index_category_app_package_name", + "unique": false, + "columnNames": [ + "package_name" + ], + "createSql": "CREATE INDEX `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, `temporarily_blocked` INTEGER NOT NULL, `base_version` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `rules_version` TEXT NOT NULL, `usedtimes_version` TEXT NOT NULL, `parent_category_id` TEXT NOT NULL, `block_all_notifications` INTEGER NOT NULL, `time_warnings` INTEGER NOT NULL, 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": "temporarilyBlocked", + "columnName": "temporarily_blocked", + "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": "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 + } + ], + "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, PRIMARY KEY(`category_id`, `day_of_epoch`))", + "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 + } + ], + "primaryKey": { + "columnNames": [ + "category_id", + "day_of_epoch" + ], + "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, 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 + } + ], + "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" + ], + "createSql": "CREATE INDEX `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": [] + } + ], + "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, \"a7aedc16546b129da089b18988dce47f\")" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/data/Migrations.kt b/app/src/main/java/io/timelimit/android/data/Migrations.kt index 8ac346b..9f84f35 100644 --- a/app/src/main/java/io/timelimit/android/data/Migrations.kt +++ b/app/src/main/java/io/timelimit/android/data/Migrations.kt @@ -133,4 +133,12 @@ object DatabaseMigrations { database.execSQL("ALTER TABLE `category` ADD COLUMN `time_warnings` INTEGER NOT NULL DEFAULT 0") } } + + val MIGRATE_TO_V19 = object: Migration(18, 19) { + override fun migrate(database: SupportSQLiteDatabase) { + // this is empty + // + // a new possible enum value was added, the version upgrade enables the downgrade mechanism + } + } } 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 983f235..b52da7b 100644 --- a/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt +++ b/app/src/main/java/io/timelimit/android/data/RoomDatabase.kt @@ -34,7 +34,7 @@ import io.timelimit.android.data.model.* PendingSyncAction::class, AppActivity::class, Notification::class -], version = 18) +], version = 19) abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database { companion object { private val lock = Object() @@ -86,7 +86,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database DatabaseMigrations.MIGRATE_TO_V15, DatabaseMigrations.MIGRATE_TO_V16, DatabaseMigrations.MIGRATE_TO_V17, - DatabaseMigrations.MIGRATE_TO_V18 + DatabaseMigrations.MIGRATE_TO_V18, + DatabaseMigrations.MIGRATE_TO_V19 ) .build() } diff --git a/app/src/main/java/io/timelimit/android/data/dao/ConfigDao.kt b/app/src/main/java/io/timelimit/android/data/dao/ConfigDao.kt index 9fa64f6..5f4e11a 100644 --- a/app/src/main/java/io/timelimit/android/data/dao/ConfigDao.kt +++ b/app/src/main/java/io/timelimit/android/data/dao/ConfigDao.kt @@ -221,4 +221,7 @@ abstract class ConfigDao { fun getEnableBackgroundSyncAsync(): LiveData = getValueOfKeyAsync(ConfigurationItemType.EnableBackgroundSync).map { (it ?: "0") != "0" } fun setEnableBackgroundSync(enable: Boolean) = updateValueSync(ConfigurationItemType.EnableBackgroundSync, if (enable) "1" else "0") + + fun getEnableAlternativeDurationSelectionAsync() = getValueOfKeyAsync(ConfigurationItemType.EnableAlternativeDurationSelection).map { it == "1" } + fun setEnableAlternativeDurationSelectionSync(enable: Boolean) = updateValueSync(ConfigurationItemType.EnableAlternativeDurationSelection, if (enable) "1" else "0") } diff --git a/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt b/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt index a7c6273..3d350b4 100644 --- a/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt +++ b/app/src/main/java/io/timelimit/android/data/model/ConfigurationItem.kt @@ -90,7 +90,8 @@ enum class ConfigurationItemType { ServerMessage, CustomServerUrl, ForegroundAppQueryRange, - EnableBackgroundSync + EnableBackgroundSync, + EnableAlternativeDurationSelection } object ConfigurationItemTypeUtil { @@ -108,6 +109,7 @@ object ConfigurationItemTypeUtil { private const val CUSTOM_SERVER_URL = 13 private const val FOREGROUND_APP_QUERY_RANGE = 14 private const val ENABLE_BACKGROUND_SYNC = 15 + private const val ENABLE_ALTERNATIVE_DURATION_SELECTION = 16 val TYPES = listOf( ConfigurationItemType.OwnDeviceId, @@ -123,7 +125,8 @@ object ConfigurationItemTypeUtil { ConfigurationItemType.ServerMessage, ConfigurationItemType.CustomServerUrl, ConfigurationItemType.ForegroundAppQueryRange, - ConfigurationItemType.EnableBackgroundSync + ConfigurationItemType.EnableBackgroundSync, + ConfigurationItemType.EnableAlternativeDurationSelection ) fun serialize(value: ConfigurationItemType) = when(value) { @@ -141,6 +144,7 @@ object ConfigurationItemTypeUtil { ConfigurationItemType.CustomServerUrl -> CUSTOM_SERVER_URL ConfigurationItemType.ForegroundAppQueryRange -> FOREGROUND_APP_QUERY_RANGE ConfigurationItemType.EnableBackgroundSync -> ENABLE_BACKGROUND_SYNC + ConfigurationItemType.EnableAlternativeDurationSelection -> ENABLE_ALTERNATIVE_DURATION_SELECTION } fun parse(value: Int) = when(value) { @@ -158,6 +162,7 @@ object ConfigurationItemTypeUtil { CUSTOM_SERVER_URL -> ConfigurationItemType.CustomServerUrl FOREGROUND_APP_QUERY_RANGE -> ConfigurationItemType.ForegroundAppQueryRange ENABLE_BACKGROUND_SYNC -> ConfigurationItemType.EnableBackgroundSync + ENABLE_ALTERNATIVE_DURATION_SELECTION -> ConfigurationItemType.EnableAlternativeDurationSelection else -> throw IllegalArgumentException() } } diff --git a/app/src/main/java/io/timelimit/android/ui/lock/LockFragment.kt b/app/src/main/java/io/timelimit/android/ui/lock/LockFragment.kt index 2829a51..bfdf8c3 100644 --- a/app/src/main/java/io/timelimit/android/ui/lock/LockFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/lock/LockFragment.kt @@ -50,6 +50,7 @@ import io.timelimit.android.ui.manage.child.advanced.managedisabletimelimits.Man import io.timelimit.android.ui.manage.child.category.create.CreateCategoryDialogFragment import io.timelimit.android.ui.manage.child.primarydevice.UpdatePrimaryDeviceDialogFragment import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment +import io.timelimit.android.ui.view.SelectTimeSpanViewListener class LockFragment : Fragment() { companion object { @@ -224,6 +225,8 @@ class LockFragment : Fragment() { // bind adding extra time controls logic.fullVersion.shouldProvideFullVersionFunctions.observe(this, Observer { hasFullVersion -> binding.extraTimeBtnOk.setOnClickListener { + binding.extraTimeSelection.clearNumberPickerFocus() + if (hasFullVersion) { if (auth.isParentAuthenticated()) { runAsync { @@ -255,6 +258,22 @@ class LockFragment : Fragment() { } }) + logic.database.config().getEnableAlternativeDurationSelectionAsync().observe(this, Observer { + binding.extraTimeSelection.enablePickerMode(it) + }) + + binding.extraTimeSelection.listener = object: SelectTimeSpanViewListener { + override fun onTimeSpanChanged(newTimeInMillis: Long) { + // ignore + } + + override fun setEnablePickerMode(enable: Boolean) { + Threads.database.execute { + logic.database.config().setEnableAlternativeDurationSelectionSync(enable) + } + } + } + // bind disable time limits mergeLiveData(logic.deviceUserEntry, logic.fullVersion.shouldProvideFullVersionFunctions).observe(this, Observer { (child, hasFullVersion) -> diff --git a/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategorySettingsFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategorySettingsFragment.kt index 8f8ea00..4b2348a 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategorySettingsFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/category/settings/CategorySettingsFragment.kt @@ -23,6 +23,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import com.google.android.material.snackbar.Snackbar import io.timelimit.android.R +import io.timelimit.android.async.Threads import io.timelimit.android.databinding.FragmentCategorySettingsBinding import io.timelimit.android.logic.AppLogic import io.timelimit.android.logic.DefaultAppLogic @@ -31,6 +32,7 @@ import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.getActivityViewModel import io.timelimit.android.ui.manage.category.ManageCategoryFragmentArgs import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment +import io.timelimit.android.ui.view.SelectTimeSpanViewListener class CategorySettingsFragment : Fragment() { companion object { @@ -99,6 +101,8 @@ class CategorySettingsFragment : Fragment() { appLogic.fullVersion.shouldProvideFullVersionFunctions.observe(this, Observer { hasFullVersion -> binding.extraTimeBtnOk.setOnClickListener { + binding.extraTimeSelection.clearNumberPickerFocus() + if (hasFullVersion) { val newExtraTime = binding.extraTimeSelection.timeInMillis @@ -118,6 +122,22 @@ class CategorySettingsFragment : Fragment() { } }) + appLogic.database.config().getEnableAlternativeDurationSelectionAsync().observe(this, Observer { + binding.extraTimeSelection.enablePickerMode(it) + }) + + binding.extraTimeSelection.listener = object: SelectTimeSpanViewListener { + override fun onTimeSpanChanged(newTimeInMillis: Long) { + // ignore + } + + override fun setEnablePickerMode(enable: Boolean) { + Threads.database.execute { + appLogic.database.config().setEnableAlternativeDurationSelectionSync(enable) + } + } + } + return binding.root } diff --git a/app/src/main/java/io/timelimit/android/ui/manage/category/timelimit_rules/edit/EditTimeLimitRuleDialogFragment.kt b/app/src/main/java/io/timelimit/android/ui/manage/category/timelimit_rules/edit/EditTimeLimitRuleDialogFragment.kt index ab7693b..5e54872 100644 --- a/app/src/main/java/io/timelimit/android/ui/manage/category/timelimit_rules/edit/EditTimeLimitRuleDialogFragment.kt +++ b/app/src/main/java/io/timelimit/android/ui/manage/category/timelimit_rules/edit/EditTimeLimitRuleDialogFragment.kt @@ -26,6 +26,7 @@ import androidx.lifecycle.Observer import com.google.android.material.R import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import io.timelimit.android.async.Threads import io.timelimit.android.data.IdGenerator import io.timelimit.android.data.model.TimeLimitRule import io.timelimit.android.data.model.UserType @@ -92,6 +93,7 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() { val view = FragmentEditTimeLimitRuleDialogBinding.inflate(layoutInflater, container, false) val listener = targetFragment as EditTimeLimitRuleDialogFragmentListener var newRule: TimeLimitRule + val database = DefaultAppLogic.with(context!!).database auth.authenticatedUser.observe(this, Observer { if (it == null || it.second.type != UserType.Parent) { @@ -135,7 +137,7 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() { view.timeSpan.timeInMillis = newRule.maximumTimeInMillis.toLong() val affectedDays = Math.max(0, (0..6).map { (newRule.dayMask.toInt() shr it) and 1 }.sum()) - view.timeSpan.maxDays = affectedDays - 1 + view.timeSpan.maxDays = Math.max(0, affectedDays - 1) // max prevents crash view.affectsMultipleDays = affectedDays >= 2 } @@ -160,6 +162,8 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() { } override fun onSaveRule() { + view.timeSpan.clearNumberPickerFocus() + if (existingRule != null) { if (existingRule != newRule) { if (!auth.tryDispatchParentAction( @@ -213,10 +217,20 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() { bindRule() } } + + override fun setEnablePickerMode(enable: Boolean) { + Threads.database.execute { + database.config().setEnableAlternativeDurationSelectionSync(enable) + } + } } + database.config().getEnableAlternativeDurationSelectionAsync().observe(this, Observer { + view.timeSpan.enablePickerMode(it) + }) + if (existingRule != null) { - DefaultAppLogic.with(context!!).database.timeLimitRules() + database.timeLimitRules() .getTimeLimitRuleByIdLive(existingRule!!.id).observe(this, Observer { if (it == null) { // rule was deleted diff --git a/app/src/main/java/io/timelimit/android/ui/view/SelectTimeSpanView.kt b/app/src/main/java/io/timelimit/android/ui/view/SelectTimeSpanView.kt index d108d93..f8b7787 100644 --- a/app/src/main/java/io/timelimit/android/ui/view/SelectTimeSpanView.kt +++ b/app/src/main/java/io/timelimit/android/ui/view/SelectTimeSpanView.kt @@ -18,6 +18,7 @@ package io.timelimit.android.ui.view import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater +import android.view.View import android.widget.FrameLayout import android.widget.SeekBar import io.timelimit.android.R @@ -34,14 +35,16 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet): FrameLay var listener: SelectTimeSpanViewListener? = null - var timeInMillis: Long by Delegates.observable(0L) { - _, _, _ -> - bindTime() - listener?.onTimeSpanChanged(timeInMillis) + var timeInMillis: Long by Delegates.observable(0L) { _, _, _ -> + bindTime() + listener?.onTimeSpanChanged(timeInMillis) } - var maxDays: Int by Delegates.observable(0) { - _, _, _ -> binding.maxDays = maxDays + var maxDays: Int by Delegates.observable(0) { _, _, _ -> + binding.maxDays = maxDays + + binding.dayPicker.maxValue = maxDays + binding.dayPickerContainer.visibility = if (maxDays > 0) View.VISIBLE else View.GONE } init { @@ -69,6 +72,10 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet): FrameLay binding.daysText = TimeTextUtil.days(totalDays, context!!) binding.minutesText = TimeTextUtil.minutes(minutes, context!!) binding.hoursText = TimeTextUtil.hours(hours, context!!) + + binding.minutePicker.value = binding.minutes ?: 0 + binding.hourPicker.value = binding.hours ?: 0 + binding.dayPicker.value = binding.days ?: 0 } private fun readStatusFromBinding() { @@ -79,7 +86,43 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet): FrameLay timeInMillis = (((days * 24) + hours) * 60 + minutes) * 1000 * 60 } + fun clearNumberPickerFocus() { + binding.minutePicker.clearFocus() + binding.hourPicker.clearFocus() + binding.dayPicker.clearFocus() + } + + fun enablePickerMode(enable: Boolean) { + binding.seekbarContainer.visibility = if (enable) View.GONE else View.VISIBLE + binding.pickerContainer.visibility = if (enable) View.VISIBLE else View.GONE + } + init { + binding.minutePicker.minValue = 0 + binding.minutePicker.maxValue = 59 + + binding.hourPicker.minValue = 0 + binding.hourPicker.maxValue = 23 + + binding.dayPicker.minValue = 0 + binding.dayPicker.maxValue = 1 + binding.dayPickerContainer.visibility = View.GONE + + binding.minutePicker.setOnValueChangedListener { _, _, newValue -> + binding.minutes = newValue + readStatusFromBinding() + } + + binding.hourPicker.setOnValueChangedListener { _, _, newValue -> + binding.hours = newValue + readStatusFromBinding() + } + + binding.dayPicker.setOnValueChangedListener { _, _, newValue -> + binding.days = newValue + readStatusFromBinding() + } + binding.daysSeek.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { binding.days = progress @@ -124,9 +167,15 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet): FrameLay // ignore } }) + + binding.pickerContainer.visibility = GONE + + binding.switchToPickerButton.setOnClickListener { listener?.setEnablePickerMode(true) } + binding.switchToSeekbarButton.setOnClickListener { listener?.setEnablePickerMode(false) } } } interface SelectTimeSpanViewListener { fun onTimeSpanChanged(newTimeInMillis: Long) + fun setEnablePickerMode(enable: Boolean) } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_unfold_more_black_24dp.xml b/app/src/main/res/drawable/ic_unfold_more_black_24dp.xml new file mode 100644 index 0000000..e9ba754 --- /dev/null +++ b/app/src/main/res/drawable/ic_unfold_more_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/view_select_time_span.xml b/app/src/main/res/layout/view_select_time_span.xml index 4dfa900..1c9f725 100644 --- a/app/src/main/res/layout/view_select_time_span.xml +++ b/app/src/main/res/layout/view_select_time_span.xml @@ -53,46 +53,148 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - + android:layout_height="wrap_content"> - + - + - + - + - + + + + + + + + + + + + + android:layout_height="wrap_content"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-de/strings-select-time-span-view.xml b/app/src/main/res/values-de/strings-select-time-span-view.xml new file mode 100644 index 0000000..fed0437 --- /dev/null +++ b/app/src/main/res/values-de/strings-select-time-span-view.xml @@ -0,0 +1,20 @@ + + + + Minuten + Stunden + Tage + \ No newline at end of file diff --git a/app/src/main/res/values/strings-select-time-span-view.xml b/app/src/main/res/values/strings-select-time-span-view.xml new file mode 100644 index 0000000..66e1cf9 --- /dev/null +++ b/app/src/main/res/values/strings-select-time-span-view.xml @@ -0,0 +1,20 @@ + + + + Minutes + Hours + Days + \ No newline at end of file