mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 01:39:22 +02:00
Add barcode unlocking for the local mode
This commit is contained in:
parent
0cb4d4a649
commit
43dabe17ab
60 changed files with 2690 additions and 40 deletions
|
@ -197,4 +197,8 @@ dependencies {
|
|||
}
|
||||
|
||||
implementation 'org.apache.commons:commons-text:1.6'
|
||||
|
||||
implementation 'org.whispersystems:curve25519-java:0.5.0'
|
||||
|
||||
implementation 'com.google.zxing:core:3.2.1'
|
||||
}
|
12
app/proguard-rules.pro
vendored
12
app/proguard-rules.pro
vendored
|
@ -1,4 +1,4 @@
|
|||
# TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
# TimeLimit Copyright <C> 2019 - 2020 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
|
||||
|
@ -48,3 +48,13 @@
|
|||
|
||||
# readable stack traces
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# Curve25519 support
|
||||
-keepnames class org.whispersystems.curve25519.NativeCurve25519Provider {}
|
||||
-keepnames class org.whispersystems.curve25519.JavaCurve25519Provider {}
|
||||
-keepnames class org.whispersystems.curve25519.J2meCurve25519Provider {}
|
||||
-keepnames class org.whispersystems.curve25519.OpportunisticCurve25519Provider {}
|
||||
-keep class org.whispersystems.curve25519.NativeCurve25519Provider {}
|
||||
-keep class org.whispersystems.curve25519.JavaCurve25519Provider {}
|
||||
-keep class org.whispersystems.curve25519.J2meCurve25519Provider {}
|
||||
-keep class org.whispersystems.curve25519.OpportunisticCurve25519Provider {}
|
887
app/schemas/io.timelimit.android.data.RoomDatabase/28.json
Normal file
887
app/schemas/io.timelimit.android.data.RoomDatabase/28.json
Normal file
|
@ -0,0 +1,887 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 28,
|
||||
"identityHash": "afecf774158d4b4648f40b5aaa068a6b",
|
||||
"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, 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": "blockedTimes",
|
||||
"columnName": "blocked_times",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "device",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `model` TEXT NOT NULL, `added_at` INTEGER NOT NULL, `current_user_id` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `network_time` TEXT NOT NULL, `current_protection_level` TEXT NOT NULL, `highest_permission_level` TEXT NOT NULL, `current_usage_stats_permission` TEXT NOT NULL, `highest_usage_stats_permission` TEXT NOT NULL, `current_notification_access_permission` TEXT NOT NULL, `highest_notification_access_permission` TEXT NOT NULL, `current_app_version` INTEGER NOT NULL, `highest_app_version` INTEGER NOT NULL, `tried_disabling_device_admin` INTEGER NOT NULL, `did_reboot` INTEGER NOT NULL, `had_manipulation` INTEGER NOT NULL, `had_manipulation_flags` INTEGER NOT NULL, `did_report_uninstall` INTEGER NOT NULL, `is_user_kept_signed_in` INTEGER NOT NULL, `show_device_connected` INTEGER NOT NULL, `default_user` TEXT NOT NULL, `default_user_timeout` INTEGER NOT NULL, `consider_reboot_manipulation` INTEGER NOT NULL, `current_overlay_permission` TEXT NOT NULL, `highest_overlay_permission` TEXT NOT NULL, `current_accessibility_service_permission` INTEGER NOT NULL, `was_accessibility_service_permission` INTEGER NOT NULL, `enable_activity_level_blocking` INTEGER NOT NULL, `q_or_later` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "model",
|
||||
"columnName": "model",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "addedAt",
|
||||
"columnName": "added_at",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "currentUserId",
|
||||
"columnName": "current_user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "installedAppsVersion",
|
||||
"columnName": "apps_version",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "networkTime",
|
||||
"columnName": "network_time",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "currentProtectionLevel",
|
||||
"columnName": "current_protection_level",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "highestProtectionLevel",
|
||||
"columnName": "highest_permission_level",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "currentUsageStatsPermission",
|
||||
"columnName": "current_usage_stats_permission",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "highestUsageStatsPermission",
|
||||
"columnName": "highest_usage_stats_permission",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "currentNotificationAccessPermission",
|
||||
"columnName": "current_notification_access_permission",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "highestNotificationAccessPermission",
|
||||
"columnName": "highest_notification_access_permission",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "currentAppVersion",
|
||||
"columnName": "current_app_version",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "highestAppVersion",
|
||||
"columnName": "highest_app_version",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "manipulationTriedDisablingDeviceAdmin",
|
||||
"columnName": "tried_disabling_device_admin",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "manipulationDidReboot",
|
||||
"columnName": "did_reboot",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "hadManipulation",
|
||||
"columnName": "had_manipulation",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "hadManipulationFlags",
|
||||
"columnName": "had_manipulation_flags",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "didReportUninstall",
|
||||
"columnName": "did_report_uninstall",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isUserKeptSignedIn",
|
||||
"columnName": "is_user_kept_signed_in",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "showDeviceConnected",
|
||||
"columnName": "show_device_connected",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultUser",
|
||||
"columnName": "default_user",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultUserTimeout",
|
||||
"columnName": "default_user_timeout",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "considerRebootManipulation",
|
||||
"columnName": "consider_reboot_manipulation",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "currentOverlayPermission",
|
||||
"columnName": "current_overlay_permission",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "highestOverlayPermission",
|
||||
"columnName": "highest_overlay_permission",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessibilityServiceEnabled",
|
||||
"columnName": "current_accessibility_service_permission",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wasAccessibilityServiceEnabled",
|
||||
"columnName": "was_accessibility_service_permission",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enableActivityLevelBlocking",
|
||||
"columnName": "enable_activity_level_blocking",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "qOrLater",
|
||||
"columnName": "q_or_later",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "app",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, `title` TEXT NOT NULL, `launchable` INTEGER NOT NULL, `recommendation` TEXT NOT NULL, PRIMARY KEY(`device_id`, `package_name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "deviceId",
|
||||
"columnName": "device_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "package_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLaunchable",
|
||||
"columnName": "launchable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "recommendation",
|
||||
"columnName": "recommendation",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"device_id",
|
||||
"package_name"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_device_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"device_id"
|
||||
],
|
||||
"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"
|
||||
],
|
||||
"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": "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 IF NOT EXISTS `index_category_app_category_id` ON `${TABLE_NAME}` (`category_id`)"
|
||||
},
|
||||
{
|
||||
"name": "index_category_app_package_name",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"package_name"
|
||||
],
|
||||
"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, `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, 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": "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
|
||||
}
|
||||
],
|
||||
"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 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"
|
||||
],
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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, 'afecf774158d4b4648f40b5aaa068a6b')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -64,7 +64,6 @@
|
|||
android:autoRemoveFromRecents="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:noHistory="true"
|
||||
android:resizeableActivity="false"
|
||||
android:supportsPictureInPicture="false"
|
||||
android:taskAffinity=":lock"
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.barcode
|
||||
|
||||
object BarcodeConstants {
|
||||
const val LOCAL_MODE_MAGIC: Int = 473967493
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.barcode
|
||||
|
||||
import com.google.zxing.common.BitMatrix
|
||||
import java.util.*
|
||||
|
||||
data class BarcodeMask(val width: Int, val height: Int, val mask: BitSet) {
|
||||
companion object {
|
||||
fun fromBitMatrix(bitMatrix: BitMatrix) = BarcodeMask(
|
||||
width = bitMatrix.width,
|
||||
height = bitMatrix.height,
|
||||
mask = BitSet(bitMatrix.width * bitMatrix.height).apply {
|
||||
(0 until bitMatrix.width).forEach { x ->
|
||||
(0 until bitMatrix.height).forEach { y ->
|
||||
set(y * bitMatrix.width + x, bitMatrix[x, y])
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun withPadding(size: Int) = BarcodeMask(
|
||||
width = width + size * 2,
|
||||
height = height + size * 2,
|
||||
mask = BitSet((width + size * 2) * (height + size * 2)).apply {
|
||||
(0 until width).forEach { x ->
|
||||
(0 until height).forEach { y ->
|
||||
set((y + size) * (width + size * 2) + (x + size), mask[y * width + x])
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.barcode
|
||||
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.Drawable
|
||||
|
||||
class BarcodeMaskDrawable(val mask: BarcodeMask): Drawable() {
|
||||
private val blackPaint = Paint(Color.BLACK)
|
||||
|
||||
override fun draw(target: Canvas) {
|
||||
target.drawColor(Color.WHITE)
|
||||
|
||||
val bLeft = bounds.left
|
||||
val bTop = bounds.top
|
||||
val bWidth = bounds.width()
|
||||
val bHeight = bounds.height()
|
||||
|
||||
(0 until mask.height).forEach { y ->
|
||||
(0 until mask.width).forEach { x ->
|
||||
if (mask.mask[y * mask.width + x]) {
|
||||
val left = bLeft + x.toFloat() / mask.width * bWidth
|
||||
val top = bTop + y.toFloat() / mask.height * bHeight
|
||||
val right = bLeft + (x + 1).toFloat() / mask.width * bWidth
|
||||
val bottom = bTop + (y + 1).toFloat() / mask.height * bHeight
|
||||
|
||||
target.drawRect(
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
blackPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setAlpha(p0: Int) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
override fun setColorFilter(p0: ColorFilter?) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
override fun getOpacity(): Int = PixelFormat.OPAQUE
|
||||
|
||||
override fun getIntrinsicWidth(): Int = mask.width * 2
|
||||
|
||||
override fun getIntrinsicHeight(): Int = mask.height * 2
|
||||
}
|
25
app/src/main/java/io/timelimit/android/barcode/DataMatrix.kt
Normal file
25
app/src/main/java/io/timelimit/android/barcode/DataMatrix.kt
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.barcode
|
||||
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.datamatrix.DataMatrixWriter
|
||||
|
||||
object DataMatrix {
|
||||
fun generate(data: String): BarcodeMask {
|
||||
return BarcodeMask.fromBitMatrix(DataMatrixWriter().encode(data, BarcodeFormat.DATA_MATRIX, 0, 0)).withPadding(3)
|
||||
}
|
||||
}
|
67
app/src/main/java/io/timelimit/android/crypto/Curve25519.kt
Normal file
67
app/src/main/java/io/timelimit/android/crypto/Curve25519.kt
Normal file
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.crypto
|
||||
|
||||
import android.util.Base64
|
||||
import io.timelimit.android.crypto.Curve25519.PRIVATE_KEY_SIZE
|
||||
import io.timelimit.android.crypto.Curve25519.PUBLIC_KEY_SIZE
|
||||
import org.whispersystems.curve25519.Curve25519
|
||||
import org.whispersystems.curve25519.Curve25519KeyPair
|
||||
|
||||
object Curve25519 {
|
||||
const val PRIVATE_KEY_SIZE = 32
|
||||
const val PUBLIC_KEY_SIZE = 32
|
||||
const val SIGNATURE_SIZE = 64
|
||||
|
||||
private val instance: Curve25519 by lazy { org.whispersystems.curve25519.Curve25519.getInstance(org.whispersystems.curve25519.Curve25519.JAVA) }
|
||||
|
||||
fun generateKeyPair() = instance.generateKeyPair().serialize()
|
||||
|
||||
fun getPublicKey(data: ByteArray): ByteArray {
|
||||
if (data.size != PRIVATE_KEY_SIZE + PUBLIC_KEY_SIZE) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
return data.copyOfRange(0, PUBLIC_KEY_SIZE)
|
||||
}
|
||||
|
||||
fun getPublicKeyId(publicKey: ByteArray): String {
|
||||
return Base64.encodeToString(publicKey.copyOfRange(0, 6), Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
fun getPrivateKey(data: ByteArray): ByteArray {
|
||||
if (data.size != PRIVATE_KEY_SIZE + PUBLIC_KEY_SIZE) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
return data.copyOfRange(PUBLIC_KEY_SIZE, PUBLIC_KEY_SIZE + PRIVATE_KEY_SIZE)
|
||||
}
|
||||
|
||||
fun sign(privateKey: ByteArray, message: ByteArray): ByteArray = instance.calculateSignature(privateKey, message)
|
||||
|
||||
fun validateSignature(publicKey: ByteArray, message: ByteArray, signature: ByteArray) = instance.verifySignature(publicKey, message, signature)
|
||||
}
|
||||
|
||||
fun Curve25519KeyPair.serialize(): ByteArray {
|
||||
val private = this.privateKey
|
||||
val public = this.publicKey
|
||||
|
||||
if (public.size != PUBLIC_KEY_SIZE || privateKey.size != PRIVATE_KEY_SIZE) {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
|
||||
return public + private
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2020 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
|
||||
|
@ -32,6 +32,7 @@ interface Database {
|
|||
fun appActivity(): AppActivityDao
|
||||
fun notification(): NotificationDao
|
||||
fun allowedContact(): AllowedContactDao
|
||||
fun userKey(): UserKeyDao
|
||||
|
||||
fun beginTransaction()
|
||||
fun setTransactionSuccessful()
|
||||
|
|
|
@ -192,4 +192,11 @@ object DatabaseMigrations {
|
|||
database.execSQL("ALTER TABLE `category` ADD COLUMN `extra_time_day` INTEGER NOT NULL DEFAULT -1")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V28 = object: Migration(27, 28) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `user_key` (`user_id` TEXT NOT NULL, `key` BLOB NOT NULL, `last_use` INTEGER NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
|
||||
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_user_key_key` ON `user_key` (`key`)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,8 +34,9 @@ import io.timelimit.android.data.model.*
|
|||
PendingSyncAction::class,
|
||||
AppActivity::class,
|
||||
Notification::class,
|
||||
AllowedContact::class
|
||||
], version = 27)
|
||||
AllowedContact::class,
|
||||
UserKey::class
|
||||
], version = 28)
|
||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||
companion object {
|
||||
private val lock = Object()
|
||||
|
@ -96,7 +97,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
|
|||
DatabaseMigrations.MIGRATE_TO_V24,
|
||||
DatabaseMigrations.MIGRATE_TO_V25,
|
||||
DatabaseMigrations.MIGRATE_TO_V26,
|
||||
DatabaseMigrations.MIGRATE_TO_V27
|
||||
DatabaseMigrations.MIGRATE_TO_V27,
|
||||
DatabaseMigrations.MIGRATE_TO_V28
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -67,7 +67,11 @@ class DatabaseBackup(private val context: Context) {
|
|||
|
||||
val database = RoomDatabase.with(context)
|
||||
|
||||
if (database.config().getOwnDeviceIdSync().orEmpty().isNotEmpty()) {
|
||||
if (
|
||||
database.config().getOwnDeviceIdSync().orEmpty().isNotEmpty() ||
|
||||
database.config().getParentModeKeySync() != null ||
|
||||
database.config().getCustomServerUrlSync().isNotEmpty()
|
||||
) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "database is not empty -> don't restore backup")
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2020 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
|
||||
|
@ -41,6 +41,7 @@ object DatabaseBackupLowlevel {
|
|||
private const val APP_ACTIVITY = "appActivity"
|
||||
private const val NOTIFICATION = "notification"
|
||||
private const val ALLOWED_CONTACT = "allowedContact"
|
||||
private const val USER_KEY = "userKey"
|
||||
|
||||
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
||||
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
|
||||
|
@ -85,6 +86,7 @@ object DatabaseBackupLowlevel {
|
|||
handleCollection(APP_ACTIVITY) { offset, pageSize -> database.appActivity().getAppActivityPageSync(offset, pageSize) }
|
||||
handleCollection(NOTIFICATION) { offset, pageSize -> database.notification().getNotificationPageSync(offset, pageSize) }
|
||||
handleCollection(ALLOWED_CONTACT) { offset, pageSize -> database.allowedContact().getAllowedContactPageSync(offset, pageSize) }
|
||||
handleCollection(USER_KEY) { offset, pageSize -> database.userKey().getUserKeyPageSync(offset, pageSize) }
|
||||
|
||||
writer.endObject().flush()
|
||||
}
|
||||
|
@ -215,6 +217,15 @@ object DatabaseBackupLowlevel {
|
|||
|
||||
reader.endArray()
|
||||
}
|
||||
USER_KEY -> {
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
database.userKey().addUserKeySync(UserKey.parse(reader))
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package io.timelimit.android.data.dao
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.util.Base64
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.room.*
|
||||
|
@ -271,4 +272,8 @@ abstract class ConfigDao {
|
|||
|
||||
fun getHomescreenDelaySync(): Int = getValueOfKeySync(ConfigurationItemType.HomescreenDelay)?.toIntOrNull(radix = 10) ?: 5
|
||||
fun setHomescreenDelaySync(delay: Int) = updateValueSync(ConfigurationItemType.HomescreenDelay, delay.toString(radix = 10))
|
||||
|
||||
fun setParentModeKeySync(key: ByteArray) = updateValueSync(ConfigurationItemType.ParentModeKey, Base64.encodeToString(key, 0))
|
||||
fun getParentModeKeySync() = getValueOfKeySync(ConfigurationItemType.ParentModeKey)?.let { value -> Base64.decode(value, 0) }
|
||||
fun getParentModeKeyLive() = getValueOfKeyAsync(ConfigurationItemType.ParentModeKey).map { value -> value?.let { Base64.decode(it, 0) } }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.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.UserKey
|
||||
|
||||
@Dao
|
||||
interface UserKeyDao {
|
||||
@Query("SELECT * FROM user_key WHERE `key` = :publicKey")
|
||||
fun findUserKeyByPublicKeySync(publicKey: ByteArray): UserKey?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
fun addUserKeySync(key: UserKey)
|
||||
|
||||
@Query("SELECT * FROM user_key LIMIT :pageSize OFFSET :offset")
|
||||
fun getUserKeyPageSync(offset: Int, pageSize: Int): List<UserKey>
|
||||
|
||||
@Query("SELECT * FROM user_key WHERE user_id = :userId")
|
||||
fun getUserKeyByUserIdLive(userId: String): LiveData<UserKey?>
|
||||
|
||||
@Query("DELETE FROM user_key WHERE user_id = :userId")
|
||||
fun deleteUserKeySync(userId: String)
|
||||
|
||||
@Query("UPDATE user_key SET last_use = :timestamp WHERE `key` = :key")
|
||||
fun updateKeyTimestamp(key: ByteArray, timestamp: Long)
|
||||
}
|
|
@ -94,7 +94,8 @@ enum class ConfigurationItemType {
|
|||
EnableAlternativeDurationSelection,
|
||||
ExperimentalFlags,
|
||||
DefaultHomescreen,
|
||||
HomescreenDelay
|
||||
HomescreenDelay,
|
||||
ParentModeKey
|
||||
}
|
||||
|
||||
object ConfigurationItemTypeUtil {
|
||||
|
@ -116,6 +117,7 @@ object ConfigurationItemTypeUtil {
|
|||
private const val EXPERIMENTAL_FLAGS = 17
|
||||
private const val DEFAULT_HOMESCREEN = 18
|
||||
private const val HOMESCREEN_DELAY = 19
|
||||
private const val PARENT_MODE_KEY = 20
|
||||
|
||||
val TYPES = listOf(
|
||||
ConfigurationItemType.OwnDeviceId,
|
||||
|
@ -135,7 +137,8 @@ object ConfigurationItemTypeUtil {
|
|||
ConfigurationItemType.EnableAlternativeDurationSelection,
|
||||
ConfigurationItemType.ExperimentalFlags,
|
||||
ConfigurationItemType.DefaultHomescreen,
|
||||
ConfigurationItemType.HomescreenDelay
|
||||
ConfigurationItemType.HomescreenDelay,
|
||||
ConfigurationItemType.ParentModeKey
|
||||
)
|
||||
|
||||
fun serialize(value: ConfigurationItemType) = when(value) {
|
||||
|
@ -157,6 +160,7 @@ object ConfigurationItemTypeUtil {
|
|||
ConfigurationItemType.ExperimentalFlags -> EXPERIMENTAL_FLAGS
|
||||
ConfigurationItemType.DefaultHomescreen -> DEFAULT_HOMESCREEN
|
||||
ConfigurationItemType.HomescreenDelay -> HOMESCREEN_DELAY
|
||||
ConfigurationItemType.ParentModeKey -> PARENT_MODE_KEY
|
||||
}
|
||||
|
||||
fun parse(value: Int) = when(value) {
|
||||
|
@ -178,6 +182,7 @@ object ConfigurationItemTypeUtil {
|
|||
EXPERIMENTAL_FLAGS -> ConfigurationItemType.ExperimentalFlags
|
||||
DEFAULT_HOMESCREEN -> ConfigurationItemType.DefaultHomescreen
|
||||
HOMESCREEN_DELAY -> ConfigurationItemType.HomescreenDelay
|
||||
PARENT_MODE_KEY -> ConfigurationItemType.ParentModeKey
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
|
119
app/src/main/java/io/timelimit/android/data/model/UserKey.kt
Normal file
119
app/src/main/java/io/timelimit/android/data/model/UserKey.kt
Normal file
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.model
|
||||
|
||||
import android.util.Base64
|
||||
import android.util.JsonReader
|
||||
import android.util.JsonWriter
|
||||
import androidx.room.*
|
||||
import io.timelimit.android.crypto.Curve25519
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
|
||||
@Entity(
|
||||
tableName = "user_key",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = User::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["user_id"],
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
],
|
||||
indices = [
|
||||
Index(
|
||||
value = ["key"],
|
||||
unique = true
|
||||
)
|
||||
]
|
||||
)
|
||||
data class UserKey(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "user_id")
|
||||
val userId: String,
|
||||
@ColumnInfo(name = "key")
|
||||
val publicKey: ByteArray,
|
||||
@ColumnInfo(name = "last_use")
|
||||
val lastUse: Long
|
||||
): JsonSerializable {
|
||||
companion object {
|
||||
private const val USER_ID = "userId"
|
||||
private const val PUBLIC_KEY = "publicKey"
|
||||
private const val LAST_USE = "lastUse"
|
||||
|
||||
fun parse(reader: JsonReader): UserKey {
|
||||
var userId: String? = null
|
||||
var publicKey: ByteArray? = null
|
||||
var lastUse: Long? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
USER_ID -> userId = reader.nextString()
|
||||
PUBLIC_KEY -> publicKey = Base64.decode(reader.nextString(), 0)
|
||||
LAST_USE -> lastUse = reader.nextLong()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
return UserKey(
|
||||
userId = userId!!,
|
||||
publicKey = publicKey!!,
|
||||
lastUse = lastUse!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
if (publicKey.size != Curve25519.PUBLIC_KEY_SIZE) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
IdGenerator.assertIdValid(userId)
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(USER_ID).value(userId)
|
||||
writer.name(PUBLIC_KEY).value(Base64.encodeToString(publicKey, Base64.NO_WRAP))
|
||||
writer.name(LAST_USE).value(lastUse)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as UserKey
|
||||
|
||||
if (userId != other.userId) return false
|
||||
if (!publicKey.contentEquals(other.publicKey)) return false
|
||||
if (lastUse != other.lastUse) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = userId.hashCode()
|
||||
result = 31 * result + publicKey.contentHashCode()
|
||||
result = 31 * result + lastUse.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -44,6 +44,7 @@ import io.timelimit.android.ui.manage.parent.link.LinkParentMailFragment
|
|||
import io.timelimit.android.ui.manage.parent.password.restore.RestoreParentPasswordFragment
|
||||
import io.timelimit.android.ui.migrate_to_connected.MigrateToConnectedModeFragment
|
||||
import io.timelimit.android.ui.overview.main.MainFragment
|
||||
import io.timelimit.android.ui.parentmode.ParentModeFragment
|
||||
import io.timelimit.android.ui.payment.ActivityPurchaseModel
|
||||
import io.timelimit.android.ui.setup.SetupTermsFragment
|
||||
import io.timelimit.android.ui.setup.parent.SetupParentModeFragment
|
||||
|
@ -104,7 +105,7 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
|||
|
||||
// up button
|
||||
val shouldShowBackButtonForNavigatorFragment = currentNavigatorFragment.map { fragment ->
|
||||
(!(fragment is MainFragment)) && (!(fragment is SetupTermsFragment))
|
||||
(!(fragment is MainFragment)) && (!(fragment is SetupTermsFragment)) && (!(fragment is ParentModeFragment))
|
||||
}
|
||||
|
||||
val shouldShowUpButton = shouldShowBackButtonForNavigatorFragment
|
||||
|
@ -224,7 +225,7 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
|||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (currentNavigatorFragment.value is SetupTermsFragment) {
|
||||
if (currentNavigatorFragment.value is SetupTermsFragment || currentNavigatorFragment.value is ParentModeFragment) {
|
||||
// hack to prevent the user from going to the launch screen of the App if it is not set up
|
||||
finish()
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.login
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import io.timelimit.android.ui.manage.parent.key.ScanKeyDialogFragment
|
||||
import io.timelimit.android.ui.manage.parent.key.ScannedKey
|
||||
|
||||
class CodeLoginDialogFragment: ScanKeyDialogFragment() {
|
||||
companion object {
|
||||
private const val DIALOG_TAG = "CodeLoginDialogFragment"
|
||||
}
|
||||
|
||||
override fun handleResult(key: ScannedKey?) {
|
||||
if (key == null) {
|
||||
Toast.makeText(context!!, R.string.manage_user_key_invalid, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
(targetFragment as NewLoginFragment).tryCodeLogin(key)
|
||||
}
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||
}
|
|
@ -27,6 +27,7 @@ import io.timelimit.android.coroutines.runAsync
|
|||
import io.timelimit.android.crypto.PasswordHashing
|
||||
import io.timelimit.android.data.model.User
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.data.transaction
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.logic.BlockingReasonUtil
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
|
@ -37,6 +38,7 @@ import io.timelimit.android.sync.actions.apply.ApplyActionChildAuthentication
|
|||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.main.AuthenticatedUser
|
||||
import io.timelimit.android.ui.manage.parent.key.ScannedKey
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.*
|
||||
|
@ -131,8 +133,13 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
}
|
||||
}
|
||||
null -> {
|
||||
logic.fullVersion.isLocalMode.switchMap { isLocalMode ->
|
||||
users.map { users ->
|
||||
UserListLoginDialogStatus(users) as LoginDialogStatus
|
||||
UserListLoginDialogStatus(
|
||||
usersToShow = users,
|
||||
isLocalMode = isLocalMode
|
||||
) as LoginDialogStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -186,6 +193,78 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
}
|
||||
}
|
||||
|
||||
fun tryCodeLogin(code: ScannedKey, model: ActivityViewModel) {
|
||||
runAsync {
|
||||
loginLock.withLock {
|
||||
if (!logic.fullVersion.isLocalMode.waitForNonNullValue()) {
|
||||
Toast.makeText(getApplication(), R.string.error_general, Toast.LENGTH_SHORT).show()
|
||||
|
||||
return@runAsync
|
||||
}
|
||||
|
||||
val user: User? = Threads.database.executeAndWait {
|
||||
logic.database.transaction().use {
|
||||
val keyEntry = logic.database.userKey().findUserKeyByPublicKeySync(code.publicKey)
|
||||
|
||||
if (keyEntry == null) {
|
||||
Threads.mainThreadHandler.post {
|
||||
Toast.makeText(getApplication(), R.string.login_scan_code_err_not_linked, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
return@executeAndWait null
|
||||
}
|
||||
|
||||
if (keyEntry.lastUse >= code.timestamp) {
|
||||
Threads.mainThreadHandler.post {
|
||||
Toast.makeText(getApplication(), R.string.login_scan_code_err_expired, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
return@executeAndWait null
|
||||
}
|
||||
|
||||
logic.database.userKey().updateKeyTimestamp(code.publicKey, code.timestamp)
|
||||
it.setSuccess()
|
||||
|
||||
logic.database.user().getUserByIdSync(keyEntry.userId)
|
||||
}
|
||||
}
|
||||
|
||||
if (user != null && user.type == UserType.Parent) {
|
||||
val hasBlockedTimes = !user.blockedTimes.dataNotToModify.isEmpty
|
||||
|
||||
val shouldSignIn = if (hasBlockedTimes) {
|
||||
val hasPremium = logic.fullVersion.shouldProvideFullVersionFunctions.waitForNonNullValue()
|
||||
|
||||
if (hasPremium) {
|
||||
val isGoodTime = blockingReasonUtil.getTrustedMinuteOfWeekLive(TimeZone.getTimeZone(user.timeZone)).map { minuteOfWeek ->
|
||||
minuteOfWeek != null && user.blockedTimes.dataNotToModify[minuteOfWeek] == false
|
||||
}.waitForNonNullValue()
|
||||
|
||||
isGoodTime
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
if (shouldSignIn) {
|
||||
// this feature is limited to the local mode
|
||||
model.setAuthenticatedUser(AuthenticatedUser(
|
||||
userId = user.id,
|
||||
firstPasswordHash = user.password,
|
||||
secondPasswordHash = "device"
|
||||
))
|
||||
|
||||
isLoginDone.value = true
|
||||
} else {
|
||||
Toast.makeText(getApplication(), R.string.login_blocked_time, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun tryParentLogin(
|
||||
password: String,
|
||||
keepSignedIn: Boolean,
|
||||
|
@ -329,7 +408,7 @@ class LoginDialogFragmentModel(application: Application): AndroidViewModel(appli
|
|||
}
|
||||
|
||||
sealed class LoginDialogStatus
|
||||
data class UserListLoginDialogStatus(val usersToShow: List<User>): LoginDialogStatus()
|
||||
data class UserListLoginDialogStatus(val usersToShow: List<User>, val isLocalMode: Boolean): LoginDialogStatus()
|
||||
object ParentUserLoginMissingTrustedTime: LoginDialogStatus()
|
||||
object ParentUserLoginBlockedTime: LoginDialogStatus()
|
||||
data class ParentUserLogin(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2020 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,19 +17,20 @@ package io.timelimit.android.ui.login
|
|||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.model.User
|
||||
import io.timelimit.android.ui.list.TextViewHolder
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class LoginUserAdapter : RecyclerView.Adapter<TextViewHolder>() {
|
||||
var data: List<User>? by Delegates.observable(null as List<User>?) { _, _, _ -> notifyDataSetChanged() }
|
||||
var data: List<LoginUserAdapterItem>? by Delegates.observable(null as List<LoginUserAdapterItem>?) { _, _, _ -> notifyDataSetChanged() }
|
||||
var listener: LoginUserAdapterListener? by Delegates.observable(null as LoginUserAdapterListener?) { _, _, _ -> notifyDataSetChanged() }
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
fun getItem(position: Int): User {
|
||||
fun getItem(position: Int): LoginUserAdapterItem {
|
||||
return data!![position]
|
||||
}
|
||||
|
||||
|
@ -44,7 +45,12 @@ class LoginUserAdapter : RecyclerView.Adapter<TextViewHolder>() {
|
|||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return getItem(position).id.hashCode().toLong()
|
||||
val item = getItem(position)
|
||||
|
||||
return when (item) {
|
||||
is LoginUserAdapterUser -> item.item.id.hashCode().toLong()
|
||||
LoginUserAdapterScan -> 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextViewHolder {
|
||||
|
@ -55,10 +61,18 @@ class LoginUserAdapter : RecyclerView.Adapter<TextViewHolder>() {
|
|||
val item = getItem(position)
|
||||
val listener = this.listener
|
||||
|
||||
holder.textView.text = item.name
|
||||
holder.textView.text = when (item) {
|
||||
is LoginUserAdapterUser -> item.item.name
|
||||
LoginUserAdapterScan -> holder.textView.context.getString(R.string.login_scan_code)
|
||||
}
|
||||
|
||||
if (listener !=null) {
|
||||
holder.textView.setOnClickListener { listener.onUserClicked(item) }
|
||||
holder.textView.setOnClickListener {
|
||||
when (item) {
|
||||
is LoginUserAdapterUser -> listener.onUserClicked(item.item)
|
||||
LoginUserAdapterScan -> listener.onScanCodeRequested()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
holder.textView.setOnClickListener(null)
|
||||
}
|
||||
|
@ -67,4 +81,9 @@ class LoginUserAdapter : RecyclerView.Adapter<TextViewHolder>() {
|
|||
|
||||
interface LoginUserAdapterListener {
|
||||
fun onUserClicked(user: User)
|
||||
fun onScanCodeRequested()
|
||||
}
|
||||
|
||||
sealed class LoginUserAdapterItem
|
||||
data class LoginUserAdapterUser(val item: User): LoginUserAdapterItem()
|
||||
object LoginUserAdapterScan: LoginUserAdapterItem()
|
|
@ -35,6 +35,7 @@ import io.timelimit.android.data.model.User
|
|||
import io.timelimit.android.databinding.NewLoginFragmentBinding
|
||||
import io.timelimit.android.extensions.setOnEnterListenr
|
||||
import io.timelimit.android.ui.main.getActivityViewModel
|
||||
import io.timelimit.android.ui.manage.parent.key.ScannedKey
|
||||
import io.timelimit.android.ui.view.KeyboardViewListener
|
||||
|
||||
class NewLoginFragment: DialogFragment() {
|
||||
|
@ -115,6 +116,12 @@ class NewLoginFragment: DialogFragment() {
|
|||
// go to the next step
|
||||
model.startSignIn(user)
|
||||
}
|
||||
|
||||
override fun onScanCodeRequested() {
|
||||
CodeLoginDialogFragment().apply {
|
||||
setTargetFragment(this@NewLoginFragment, 0)
|
||||
}.show(parentFragmentManager)
|
||||
}
|
||||
}
|
||||
|
||||
binding.userList.recycler.adapter = adapter
|
||||
|
@ -181,7 +188,12 @@ class NewLoginFragment: DialogFragment() {
|
|||
binding.switcher.displayedChild = USER_LIST
|
||||
}
|
||||
|
||||
adapter.data = status.usersToShow
|
||||
val users = status.usersToShow.map { LoginUserAdapterUser(it) }
|
||||
|
||||
adapter.data = if (status.isLocalMode)
|
||||
users + LoginUserAdapterScan
|
||||
else
|
||||
users
|
||||
|
||||
Threads.mainThreadHandler.post { binding.userList.recycler.requestFocus() }
|
||||
|
||||
|
@ -302,4 +314,8 @@ class NewLoginFragment: DialogFragment() {
|
|||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
fun tryCodeLogin(code: ScannedKey) {
|
||||
model.tryCodeLogin(code, getActivityViewModel(activity!!))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ import io.timelimit.android.ui.main.AuthenticationFab
|
|||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||
import io.timelimit.android.ui.manage.child.advanced.timezone.UserTimezoneView
|
||||
import io.timelimit.android.ui.manage.parent.delete.DeleteParentView
|
||||
import io.timelimit.android.ui.manage.parent.key.ManageUserKeyView
|
||||
|
||||
class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
|
||||
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||
|
@ -115,6 +116,14 @@ class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
|
|||
userEntry = parentUser
|
||||
)
|
||||
|
||||
ManageUserKeyView.bind(
|
||||
view = binding.userKey,
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
userId = params.parentId,
|
||||
auth = activity.getActivityViewModel(),
|
||||
fragmentManager = parentFragmentManager
|
||||
)
|
||||
|
||||
binding.handlers = object: ManageParentFragmentHandlers {
|
||||
override fun onChangePasswordClicked() {
|
||||
navigation.safeNavigate(
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.parent.key
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.data.model.UserKey
|
||||
import io.timelimit.android.data.transaction
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
|
||||
class AddUserKeyDialogFragment: ScanKeyDialogFragment() {
|
||||
companion object {
|
||||
private const val DIALOG_TAG = "AddUserKeyDialogFragment"
|
||||
private const val USER_ID = "userId"
|
||||
|
||||
fun newInstance(userId: String) = AddUserKeyDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(USER_ID, userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleResult(key: ScannedKey?) {
|
||||
if (key == null) {
|
||||
Toast.makeText(context!!, R.string.manage_user_key_invalid, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
val context = context!!.applicationContext
|
||||
val database = DefaultAppLogic.with(context!!).database
|
||||
val userId = arguments!!.getString(USER_ID)!!
|
||||
|
||||
Threads.database.execute {
|
||||
database.transaction().use {
|
||||
val old = database.userKey().findUserKeyByPublicKeySync(key.publicKey)
|
||||
|
||||
if (old == null) {
|
||||
database.userKey().addUserKeySync(
|
||||
UserKey(
|
||||
userId = userId,
|
||||
publicKey = key.publicKey,
|
||||
lastUse = key.timestamp
|
||||
)
|
||||
)
|
||||
|
||||
Threads.mainThreadHandler.post {
|
||||
Toast.makeText(context, R.string.manage_user_key_added, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
it.setSuccess()
|
||||
} else {
|
||||
Threads.mainThreadHandler.post {
|
||||
Toast.makeText(context, R.string.manage_user_key_other_user, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.parent.key
|
||||
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.crypto.Curve25519
|
||||
import io.timelimit.android.databinding.ManageUserKeyViewBinding
|
||||
import io.timelimit.android.ui.help.HelpDialogFragment
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
|
||||
object ManageUserKeyView {
|
||||
fun bind(
|
||||
view: ManageUserKeyViewBinding,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
userId: String,
|
||||
auth: ActivityViewModel,
|
||||
fragmentManager: FragmentManager
|
||||
) {
|
||||
val userKey = auth.logic.database.userKey().getUserKeyByUserIdLive(userId)
|
||||
|
||||
userKey.observe(lifecycleOwner, Observer {
|
||||
view.isLoaded = true
|
||||
view.keyId = it?.publicKey?.let { Curve25519.getPublicKeyId(it) }
|
||||
})
|
||||
|
||||
auth.logic.fullVersion.isLocalMode.observe(lifecycleOwner, Observer {
|
||||
view.isLocalMode = it
|
||||
})
|
||||
|
||||
view.addKeyButton.setOnClickListener {
|
||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||
AddUserKeyDialogFragment.newInstance(userId).show(fragmentManager)
|
||||
}
|
||||
}
|
||||
|
||||
view.removeKeyButton.setOnClickListener {
|
||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||
val database = auth.database
|
||||
|
||||
Threads.database.execute {
|
||||
database.userKey().deleteUserKeySync(userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view.titleView.setOnClickListener {
|
||||
HelpDialogFragment.newInstance(
|
||||
title = R.string.manage_user_key_title,
|
||||
text = R.string.manage_user_key_info
|
||||
).show(fragmentManager)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.parent.key
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import io.timelimit.android.R
|
||||
|
||||
abstract class ScanKeyDialogFragment: DialogFragment() {
|
||||
companion object {
|
||||
private const val CAN_NOT_SCAN = "canNotScan"
|
||||
private const val REQ_SCAN = 1
|
||||
}
|
||||
|
||||
private var canNotScan = false
|
||||
|
||||
final override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
try {
|
||||
startActivityForResult(
|
||||
Intent()
|
||||
.setPackage("de.markusfisch.android.binaryeye")
|
||||
.setAction("com.google.zxing.client.android.SCAN"),
|
||||
REQ_SCAN
|
||||
)
|
||||
} catch (ex: ActivityNotFoundException) {
|
||||
canNotScan = true
|
||||
}
|
||||
} else {
|
||||
canNotScan = savedInstanceState.getBoolean(CAN_NOT_SCAN)
|
||||
}
|
||||
}
|
||||
|
||||
final override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
if (canNotScan) {
|
||||
return AlertDialog.Builder(context!!, theme)
|
||||
.setTitle(R.string.scan_key_missing_title)
|
||||
.setMessage(R.string.scan_key_missing_text)
|
||||
.setNegativeButton(R.string.generic_cancel, null)
|
||||
.setPositiveButton(R.string.scan_key_missing_install) { _, _ ->
|
||||
try {
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("market://details?id=de.markusfisch.android.binaryeye")
|
||||
|
||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
} catch (ex: ActivityNotFoundException) {
|
||||
Toast.makeText(context!!, R.string.error_general, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
.create()
|
||||
} else {
|
||||
return super.onCreateDialog(savedInstanceState)
|
||||
}
|
||||
}
|
||||
|
||||
final override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putBoolean(CAN_NOT_SCAN, canNotScan)
|
||||
}
|
||||
|
||||
final override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (requestCode == REQ_SCAN && (!canNotScan)) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val key = ScannedKey.tryDecode(data?.getStringExtra("SCAN_RESULT") ?: "")
|
||||
|
||||
handleResult(key)
|
||||
}
|
||||
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun handleResult(key: ScannedKey?)
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.parent.key
|
||||
|
||||
import android.util.Base64
|
||||
import io.timelimit.android.barcode.BarcodeConstants
|
||||
import io.timelimit.android.crypto.Curve25519
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
class ScannedKey(val publicKey: ByteArray, val timestamp: Long) {
|
||||
companion object {
|
||||
private const val expectedSize = 12 + Curve25519.SIGNATURE_SIZE + Curve25519.PUBLIC_KEY_SIZE
|
||||
|
||||
fun tryDecode(dataString: String): ScannedKey? {
|
||||
val data = try {
|
||||
Base64.decode(dataString, 0)
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (data.size != expectedSize) {
|
||||
return null
|
||||
}
|
||||
|
||||
val buffer = ByteBuffer.wrap(data).apply { order(ByteOrder.BIG_ENDIAN) }
|
||||
|
||||
if (buffer.getInt(0) != BarcodeConstants.LOCAL_MODE_MAGIC) {
|
||||
return null
|
||||
}
|
||||
|
||||
val timestamp = buffer.getLong(4)
|
||||
val dataToSign = data.copyOfRange(0, 12)
|
||||
val signature = data.copyOfRange(12, 12 + Curve25519.SIGNATURE_SIZE)
|
||||
val publicKey = data.copyOfRange(12 + Curve25519.SIGNATURE_SIZE, 12 + Curve25519.SIGNATURE_SIZE + Curve25519.PUBLIC_KEY_SIZE)
|
||||
|
||||
if (!Curve25519.validateSignature(publicKey, dataToSign, signature)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ScannedKey(publicKey, timestamp)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2020 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
|
||||
|
@ -30,11 +30,22 @@ import io.timelimit.android.logic.AppLogic
|
|||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
|
||||
class AboutFragment : Fragment() {
|
||||
companion object {
|
||||
private const val EXTRA_SHOWN_OUTSIDE_OF_OVERVIEW = "shownOutsideOfOverview"
|
||||
|
||||
fun newInstance(shownOutsideOfOverview: Boolean = false) = AboutFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putBoolean(EXTRA_SHOWN_OUTSIDE_OF_OVERVIEW, shownOutsideOfOverview)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
||||
private val listener: AboutFragmentParentHandlers by lazy { parentFragment as AboutFragmentParentHandlers }
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = FragmentAboutBinding.inflate(inflater, container, false)
|
||||
val shownOutsideOfOverview = arguments?.getBoolean(EXTRA_SHOWN_OUTSIDE_OF_OVERVIEW, false) ?: false
|
||||
|
||||
mergeLiveData(
|
||||
logic.database.config().getDeviceAuthTokenAsync(), logic.database.config().getFullVersionUntilAsync()
|
||||
|
@ -78,6 +89,10 @@ class AboutFragment : Fragment() {
|
|||
binding.containedSoftwareText.movementMethod = LinkMovementMethod.getInstance()
|
||||
binding.sourceCodeUrl.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
if (shownOutsideOfOverview) {
|
||||
binding.resetShownHintsView.root.visibility = View.GONE
|
||||
binding.errorDiagnoseCard.visibility = View.GONE
|
||||
} else {
|
||||
ResetShownHints.bind(
|
||||
binding = binding.resetShownHintsView,
|
||||
lifecycleOwner = this,
|
||||
|
@ -87,6 +102,7 @@ class AboutFragment : Fragment() {
|
|||
binding.errorDiagnoseCard.setOnClickListener {
|
||||
listener.onShowDiagnoseScreen()
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@ import androidx.navigation.NavController
|
|||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.extensions.safeNavigate
|
||||
|
@ -94,12 +96,23 @@ class MainFragment : Fragment(), OverviewFragmentParentHandlers, AboutFragmentPa
|
|||
}
|
||||
}.observe(viewLifecycleOwner, Observer { shouldShowSetup ->
|
||||
if (shouldShowSetup == true) {
|
||||
fab.post {
|
||||
runAsync {
|
||||
val hasParentKey = Threads.database.executeAndWait { logic.database.config().getParentModeKeySync() != null }
|
||||
|
||||
if (parentFragmentManager.isStateSaved == false) {
|
||||
if (hasParentKey) {
|
||||
navigation.safeNavigate(
|
||||
MainFragmentDirections.actionOverviewFragmentToParentModeFragment(),
|
||||
R.id.overviewFragment
|
||||
)
|
||||
} else {
|
||||
navigation.safeNavigate(
|
||||
MainFragmentDirections.actionOverviewFragmentToSetupTermsFragment(),
|
||||
R.id.overviewFragment
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (savedInstanceState == null && !didRedirectToUserScreen) {
|
||||
didRedirectToUserScreen = true
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.parentmode
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.timelimit.android.barcode.BarcodeMaskDrawable
|
||||
import io.timelimit.android.databinding.ParentModeCodeFragmentBinding
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
|
||||
class ParentModeCodeFragment: Fragment() {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = ParentModeCodeFragmentBinding.inflate(inflater, container, false)
|
||||
val model = ViewModelProvider(this).get(ParentModeCodeModel::class.java)
|
||||
|
||||
model.init(DefaultAppLogic.with(context!!).database)
|
||||
|
||||
model.barcodeContent.observe(viewLifecycleOwner, Observer {
|
||||
binding.image.setImageDrawable(if (it != null) BarcodeMaskDrawable(it) else null)
|
||||
})
|
||||
|
||||
model.keyId.observe(viewLifecycleOwner, Observer {
|
||||
binding.keyId = it
|
||||
})
|
||||
|
||||
return binding.root
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.parentmode
|
||||
|
||||
import android.util.Base64
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.timelimit.android.barcode.BarcodeConstants
|
||||
import io.timelimit.android.barcode.BarcodeMask
|
||||
import io.timelimit.android.barcode.DataMatrix
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.livedata.liveDataFromFunction
|
||||
import io.timelimit.android.livedata.liveDataFromValue
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.switchMap
|
||||
import io.timelimit.android.crypto.Curve25519
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
class ParentModeCodeModel: ViewModel() {
|
||||
private var didInit = false
|
||||
lateinit var barcodeContent: LiveData<BarcodeMask?>
|
||||
lateinit var keyId: LiveData<String>
|
||||
|
||||
fun init(database: Database) {
|
||||
if (didInit) {
|
||||
return
|
||||
}
|
||||
|
||||
didInit = true
|
||||
|
||||
val parentKey = database.config().getParentModeKeyLive()
|
||||
val timestamp = liveDataFromFunction (5 * 1000L) { System.currentTimeMillis() }
|
||||
|
||||
keyId = parentKey.map { key ->
|
||||
if (key != null) {
|
||||
Curve25519.getPublicKeyId(Curve25519.getPublicKey(key))
|
||||
} else {
|
||||
"???"
|
||||
}
|
||||
}
|
||||
|
||||
barcodeContent = parentKey.switchMap { parentKey ->
|
||||
if (parentKey == null) {
|
||||
liveDataFromValue(null as BarcodeMask?)
|
||||
} else {
|
||||
val privateKey = Curve25519.getPrivateKey(parentKey)
|
||||
val publicKey = Curve25519.getPublicKey(parentKey)
|
||||
|
||||
timestamp.map { timestamp ->
|
||||
val dataToSign = ByteArray(12)
|
||||
|
||||
ByteBuffer.allocate(12).apply {
|
||||
order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
putInt(0, BarcodeConstants.LOCAL_MODE_MAGIC)
|
||||
putLong(4, timestamp)
|
||||
|
||||
get(dataToSign)
|
||||
}
|
||||
|
||||
val signature = Curve25519.sign(
|
||||
privateKey,
|
||||
dataToSign
|
||||
)
|
||||
|
||||
val content = dataToSign + signature + publicKey
|
||||
|
||||
DataMatrix.generate(Base64.encodeToString(content, Base64.NO_WRAP)) as BarcodeMask?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.parentmode
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.ui.overview.about.AboutFragment
|
||||
import kotlinx.android.synthetic.main.fragment_main.*
|
||||
|
||||
class ParentModeFragment : Fragment() {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.parent_mode_fragment, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, ParentModeCodeFragment())
|
||||
.commitNow()
|
||||
}
|
||||
|
||||
bottom_navigation_view.setOnNavigationItemSelectedListener { menuItem ->
|
||||
if (childFragmentManager.isStateSaved) {
|
||||
false
|
||||
} else {
|
||||
childFragmentManager.beginTransaction()
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.replace(R.id.container, when (menuItem.itemId) {
|
||||
R.id.parent_mode_tab_code -> ParentModeCodeFragment()
|
||||
R.id.parent_mode_tab_help -> ParentModeHelpFragment()
|
||||
R.id.parent_mode_tab_about -> AboutFragment.newInstance(shownOutsideOfOverview = true)
|
||||
else -> throw IllegalStateException()
|
||||
})
|
||||
.commit()
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.parentmode
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.timelimit.android.R
|
||||
|
||||
class ParentModeHelpFragment: Fragment() {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.parent_mode_help_fragment, container, false)
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ import io.timelimit.android.R
|
|||
import io.timelimit.android.databinding.FragmentSetupSelectModeBinding
|
||||
import io.timelimit.android.extensions.safeNavigate
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.ui.setup.parentmode.SetupParentmodeDialogFragment
|
||||
import io.timelimit.android.ui.setup.privacy.PrivacyInfoDialogFragment
|
||||
import kotlinx.android.synthetic.main.fragment_setup_select_mode.*
|
||||
|
||||
|
@ -39,6 +40,7 @@ class SetupSelectModeFragment : Fragment() {
|
|||
companion object {
|
||||
private const val REQ_SETUP_CONNECTED_PARENT = 1
|
||||
private const val REQ_SETUP_CONNECTED_CHILD = 2
|
||||
private const val REQUEST_SETUP_PARENT_MODE = 3
|
||||
}
|
||||
|
||||
private lateinit var navigation: NavController
|
||||
|
@ -73,6 +75,12 @@ class SetupSelectModeFragment : Fragment() {
|
|||
}.show(fragmentManager!!)
|
||||
}
|
||||
|
||||
btn_parent_key_mode.setOnClickListener {
|
||||
SetupParentmodeDialogFragment().apply {
|
||||
setTargetFragment(this@SetupSelectModeFragment, REQUEST_SETUP_PARENT_MODE)
|
||||
}.show(parentFragmentManager)
|
||||
}
|
||||
|
||||
btn_uninstall.setOnClickListener {
|
||||
DefaultAppLogic.with(context!!).platformIntegration.disableDeviceAdmin()
|
||||
|
||||
|
@ -104,6 +112,8 @@ class SetupSelectModeFragment : Fragment() {
|
|||
SetupSelectModeFragmentDirections.actionSetupSelectModeFragmentToSetupParentModeFragment(),
|
||||
R.id.setupSelectModeFragment
|
||||
)
|
||||
} else if (requestCode == REQUEST_SETUP_PARENT_MODE && resultCode == Activity.RESULT_OK) {
|
||||
navigation.popBackStack(R.id.overviewFragment, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.setup.parentmode
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.app.ProgressDialog
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
|
||||
class SetupParentmodeDialogFragment: DialogFragment() {
|
||||
companion object {
|
||||
private const val DIALOG_TAG = "SetupParentmodeDialogFragment"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
ViewModelProvider(this).get(SetupParentmodeDialogModel::class.java)
|
||||
.init(DefaultAppLogic.with(context!!).database)
|
||||
.observe(this, Observer { ok ->
|
||||
dismissAllowingStateLoss()
|
||||
|
||||
if (ok) {
|
||||
targetFragment?.onActivityResult(targetRequestCode, Activity.RESULT_OK, null)
|
||||
}
|
||||
})
|
||||
|
||||
isCancelable = false
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val result = ProgressDialog(context, theme)
|
||||
|
||||
result.setCanceledOnTouchOutside(false)
|
||||
result.setMessage(getString(R.string.setup_select_mode_parent_key_progress))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.timelimit.android.ui.setup.parentmode
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.transaction
|
||||
import io.timelimit.android.crypto.Curve25519
|
||||
|
||||
class SetupParentmodeDialogModel: ViewModel() {
|
||||
private var didInit = false
|
||||
private val hadSuccess = MutableLiveData<Boolean>()
|
||||
|
||||
fun init(database: Database): LiveData<Boolean> {
|
||||
if (!didInit) {
|
||||
didInit = true
|
||||
|
||||
runAsync {
|
||||
val keys = Threads.crypto.executeAndWait { Curve25519.generateKeyPair() }
|
||||
val ok = Threads.database.executeAndWait {
|
||||
database.transaction().use {
|
||||
if (database.config().getOwnDeviceIdSync() != null) {
|
||||
false
|
||||
} else if (database.config().getParentModeKeySync() != null) {
|
||||
true
|
||||
} else {
|
||||
database.config().setParentModeKeySync(keys)
|
||||
|
||||
it.setSuccess()
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hadSuccess.value = ok
|
||||
}
|
||||
}
|
||||
|
||||
return hadSuccess
|
||||
}
|
||||
}
|
9
app/src/main/res/drawable/ic_help_outline_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_help_outline_black_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_vpn_key_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_vpn_key_black_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12.65,10C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H17v4h4v-4h2v-4H12.65zM7,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z"/>
|
||||
</vector>
|
|
@ -157,6 +157,9 @@
|
|||
<include android:id="@+id/manage_notifications"
|
||||
layout="@layout/manage_parent_notifications" />
|
||||
|
||||
<include android:id="@+id/user_key"
|
||||
layout="@layout/manage_user_key_view" />
|
||||
|
||||
<include android:id="@+id/timezone"
|
||||
layout="@layout/user_timezone_view" />
|
||||
|
||||
|
|
|
@ -127,6 +127,34 @@
|
|||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:focusable="true"
|
||||
android:id="@+id/btn_parent_key_mode"
|
||||
android:foreground="?selectableItemBackground"
|
||||
app:cardUseCompatPadding="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
android:text="@string/setup_select_mode_parent_key_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@string/setup_select_mode_parent_key_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:focusable="true"
|
||||
android:id="@+id/btn_uninstall"
|
||||
|
|
94
app/src/main/res/layout/manage_user_key_view.xml
Normal file
94
app/src/main/res/layout/manage_user_key_view.xml
Normal file
|
@ -0,0 +1,94 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="keyId"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="isLoaded"
|
||||
type="boolean" />
|
||||
|
||||
<variable
|
||||
name="isLocalMode"
|
||||
type="boolean" />
|
||||
|
||||
<import type="android.view.View" />
|
||||
<import type="android.text.TextUtils" />
|
||||
</data>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardUseCompatPadding="true">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
tools:ignore="UnusedAttribute"
|
||||
android:drawableTint="?colorOnSurface"
|
||||
android:id="@+id/title_view"
|
||||
android:drawableEnd="@drawable/ic_info_outline_black_24dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:text="@string/manage_user_key_title"
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
tools:text="@string/manage_user_key_not_enrolled"
|
||||
android:text="@{TextUtils.isEmpty(keyId) ? @string/manage_user_key_not_enrolled : @string/manage_user_key_is_enrolled(keyId)}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:visibility="@{TextUtils.isEmpty(keyId) ? View.VISIBLE : View.GONE}"
|
||||
android:enabled="@{isLoaded && isLocalMode}"
|
||||
android:id="@+id/add_key_button"
|
||||
android:layout_gravity="end"
|
||||
style="?materialButtonOutlinedStyle"
|
||||
android:text="@string/manage_user_key_add"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:visibility="@{TextUtils.isEmpty(keyId) ? View.GONE : View.VISIBLE}"
|
||||
android:enabled="@{isLoaded && isLocalMode}"
|
||||
android:id="@+id/remove_key_button"
|
||||
android:layout_gravity="end"
|
||||
style="?materialButtonOutlinedStyle"
|
||||
android:text="@string/manage_user_key_remove"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
android:text="@string/purchase_required_info_only_local_mode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</layout>
|
48
app/src/main/res/layout/parent_mode_code_fragment.xml
Normal file
48
app/src/main/res/layout/parent_mode_code_fragment.xml
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="keyId"
|
||||
type="String" />
|
||||
</data>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:scaleType="fitCenter"
|
||||
android:id="@+id/image"
|
||||
android:layout_weight="1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp" />
|
||||
|
||||
<TextView
|
||||
android:padding="8dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
android:text="@{@string/parent_mode_key_id(keyId)}"
|
||||
tools:text="@string/parent_mode_key_id"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
37
app/src/main/res/layout/parent_mode_fragment.xml
Normal file
37
app/src/main/res/layout/parent_mode_fragment.xml
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:context="io.timelimit.android.ui.parentmode.ParentModeFragment">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_height="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottom_navigation_view"
|
||||
app:menu="@menu/fragment_parent_menu_bottom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
28
app/src/main/res/layout/parent_mode_help_fragment.xml
Normal file
28
app/src/main/res/layout/parent_mode_help_fragment.xml
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
android:padding="16dp"
|
||||
android:text="@string/parent_mode_help_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
32
app/src/main/res/menu/fragment_parent_menu_bottom.xml
Normal file
32
app/src/main/res/menu/fragment_parent_menu_bottom.xml
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:icon="@drawable/ic_vpn_key_black_24dp"
|
||||
android:title="@string/parent_mode_key_title"
|
||||
android:id="@+id/parent_mode_tab_code" />
|
||||
|
||||
<item
|
||||
android:icon="@drawable/ic_help_outline_black_24dp"
|
||||
android:title="@string/generic_help"
|
||||
android:id="@+id/parent_mode_tab_help" />
|
||||
|
||||
<item
|
||||
android:icon="@drawable/ic_info_outline_black_24dp"
|
||||
android:title="@string/main_tab_about"
|
||||
android:id="@+id/parent_mode_tab_about" />
|
||||
</menu>
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
TimeLimit Copyright <C> 2019 - 2020 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.
|
||||
|
@ -94,6 +94,9 @@
|
|||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_overviewFragment_to_parentModeFragment"
|
||||
app:destination="@id/parentModeFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/manageChildFragment"
|
||||
|
@ -439,4 +442,9 @@
|
|||
android:name="io.timelimit.android.ui.diagnose.DiagnoseBatteryFragment"
|
||||
android:label="diagnose_battery_fragment"
|
||||
tools:layout="@layout/diagnose_battery_fragment" />
|
||||
<fragment
|
||||
android:id="@+id/parentModeFragment"
|
||||
android:name="io.timelimit.android.ui.parentmode.ParentModeFragment"
|
||||
android:label="parent_mode_fragment"
|
||||
tools:layout="@layout/parent_mode_fragment" />
|
||||
</navigation>
|
||||
|
|
|
@ -29,4 +29,7 @@
|
|||
<string name="login_child_done_toast">Der Benutzer dieses Gerätes wurde geändert</string>
|
||||
<string name="login_missing_trusted_time">Zum Anmelden mit diesem Benutzer wird die Uhrzeit benötigt</string>
|
||||
<string name="login_blocked_time">Das Anmelden mit diesem Benutzer zu dieser Zeit wurde gesperrt</string>
|
||||
<string name="login_scan_code">Code scannen</string>
|
||||
<string name="login_scan_code_err_expired">Dieser Code ist alt</string>
|
||||
<string name="login_scan_code_err_not_linked">Dieser Schlüssel ist nicht verknüpft</string>
|
||||
</resources>
|
31
app/src/main/res/values-de/strings-manage-user-key.xml
Normal file
31
app/src/main/res/values-de/strings-manage-user-key.xml
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<resources>
|
||||
<string name="manage_user_key_title">Benutzerschlüssel</string>
|
||||
<string name="manage_user_key_info">Hiermit kann sich ein Benutzer durch
|
||||
das Scannen eines Barcodes anmelden. Der gescannte Code muss von TimeLimit
|
||||
generiert worden sein.
|
||||
</string>
|
||||
<string name="manage_user_key_not_enrolled">Es gibt keinen Schlüssel für diesen Benutzer</string>
|
||||
<string name="manage_user_key_is_enrolled">Dieser Benutzer hat den Schlüssel %s</string>
|
||||
<string name="manage_user_key_add">Schlüssel hinzufügen</string>
|
||||
<string name="manage_user_key_remove">Schlüssel entfernen</string>
|
||||
<string name="manage_user_key_added">Der Schlüssel wurde hinzugefügt</string>
|
||||
<string name="manage_user_key_removed">Der Schlüssel wurde entfernt</string>
|
||||
<string name="manage_user_key_invalid">Dies ist kein gültiger TimeLimit-Schlüssel</string>
|
||||
<string name="manage_user_key_other_user">Dieser Schlüssel ist schon bei einem anderen Benutzer hinterlegt</string>
|
||||
</resources>
|
26
app/src/main/res/values-de/strings-parent-mode.xml
Normal file
26
app/src/main/res/values-de/strings-parent-mode.xml
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<resources>
|
||||
<string name="parent_mode_key_title">Schlüssel</string>
|
||||
<string name="parent_mode_key_id">Schlüssel-ID: %s</string>
|
||||
<string name="parent_mode_help_text">
|
||||
Sie haben den Eltern-Schlüssel-Modus gewählt.
|
||||
Dies ermöglicht es, dieses Gerät als Schlüssel zu benutzen.
|
||||
Wenn das nicht das ist, was Sie wollen,
|
||||
dann installieren Sie TimeLimit erneut.
|
||||
</string>
|
||||
</resources>
|
|
@ -53,6 +53,7 @@
|
|||
|
||||
<string name="purchase_required_info_only_networking">Diese Funktion ist nur in der Vollversion verfügbar.</string>
|
||||
<string name="purchase_required_info_local_mode_free">Diese Funktion ist nur in der Vollversion und im lokalen Modus verfügbar.</string>
|
||||
<string name="purchase_required_info_only_local_mode">Diese Funktion ist nur im lokalen Modus verfügbar.</string>
|
||||
|
||||
<string name="purchase_demo_temporarily_notice">
|
||||
Sie können die Funktionen der Vollversion vorübergehend nutzen.
|
||||
|
|
20
app/src/main/res/values-de/strings-scan-key-dialog.xml
Normal file
20
app/src/main/res/values-de/strings-scan-key-dialog.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<resources>
|
||||
<string name="scan_key_missing_title">Binary Eye wird benötigt</string>
|
||||
<string name="scan_key_missing_text">Zum Scannen der Barcodes wird die App Binary Eye benötigt</string>
|
||||
<string name="scan_key_missing_install">Binary Eye installieren</string>
|
||||
</resources>
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
TimeLimit Copyright <C> 2019 - 2020 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.
|
||||
|
@ -33,6 +33,13 @@
|
|||
Es erfolgt eine Anmeldung mittels Code, der auf einem anderen Gerät, auf dem TimeLimit bereits installiert ist, angezeigt wird.
|
||||
</string>
|
||||
|
||||
<string name="setup_select_mode_parent_key_title">Schlüsselgerät eines Elternteils</string>
|
||||
<string name="setup_select_mode_parent_key_text">
|
||||
TimeLimit schränkt dieses Gerät nicht ein. Es wird als zusätzlicher Schlüssel bei einem anderen Gerät im lokalen Mdous verwendet,
|
||||
um dort die Einstellungen von TimeLimit zu entsperren. Am anderen Gerät kann auch jederzeit ein Passwort verwendet werden.
|
||||
</string>
|
||||
<string name="setup_select_mode_parent_key_progress">Erstelle Schlüssel</string>
|
||||
|
||||
<string name="setup_select_mode_uninstall_title">TimeLimit entfernen</string>
|
||||
<string name="setup_select_mode_uninstall_text">
|
||||
Nicht wirklich eine Art der Einrichtung.
|
||||
|
|
|
@ -75,6 +75,10 @@
|
|||
(<a href="https://github.com/socketio/socket.io-client-java/blob/master/LICENSE">MIT License</a>)
|
||||
\n<a href="https://github.com/apache/commons-text">Apache Commons Text</a>
|
||||
(<a href="https://github.com/apache/commons-text/blob/master/LICENSE.txt">Apache License, Version 2.0</a>)
|
||||
\n<a href="https://github.com/signalapp/curve25519-java">curve25519-java</a>
|
||||
(<a href="https://github.com/signalapp/curve25519-java/blob/master/LICENSE">GNU General Public License v3.0</a>)
|
||||
\n<a href="https://github.com/zxing/zxing">ZXing</a>
|
||||
(<a href="https://github.com/zxing/zxing/blob/master/LICENSE">Apache License 2.0</a>)
|
||||
</string>
|
||||
|
||||
<string name="about_diagnose_title">Error diagnose</string>
|
||||
|
|
|
@ -29,4 +29,7 @@
|
|||
<string name="login_child_done_toast">The user of this device was changed</string>
|
||||
<string name="login_missing_trusted_time">The current time is required to sign in with this user</string>
|
||||
<string name="login_blocked_time">Signing in as this user at this time was blocked</string>
|
||||
<string name="login_scan_code">Scan code</string>
|
||||
<string name="login_scan_code_err_expired">This code is old</string>
|
||||
<string name="login_scan_code_err_not_linked">This key is not linked</string>
|
||||
</resources>
|
||||
|
|
30
app/src/main/res/values/strings-manage-user-key.xml
Normal file
30
app/src/main/res/values/strings-manage-user-key.xml
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<resources>
|
||||
<string name="manage_user_key_title">User key</string>
|
||||
<string name="manage_user_key_info">A key is a barcode which allows the user
|
||||
to sign in by scanning it. This code must be generated by TimeLimit.
|
||||
</string>
|
||||
<string name="manage_user_key_not_enrolled">There is no key for this user</string>
|
||||
<string name="manage_user_key_is_enrolled">This user has got the key %s</string>
|
||||
<string name="manage_user_key_add">add key</string>
|
||||
<string name="manage_user_key_remove">remove key</string>
|
||||
<string name="manage_user_key_added">The key was added</string>
|
||||
<string name="manage_user_key_removed">The key was removed</string>
|
||||
<string name="manage_user_key_invalid">This was no valid TimeLimit key</string>
|
||||
<string name="manage_user_key_other_user">This key is already linked to an other user</string>
|
||||
</resources>
|
26
app/src/main/res/values/strings-parent-mode.xml
Normal file
26
app/src/main/res/values/strings-parent-mode.xml
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<resources>
|
||||
<string name="parent_mode_key_title">Key</string>
|
||||
<string name="parent_mode_key_id">Key ID: %s</string>
|
||||
<string name="parent_mode_help_text">
|
||||
You have chosen the parent key mode.
|
||||
This allows you to use your device as parent key.
|
||||
In case this is not what you want,
|
||||
reset/ reinstall TimeLimit.
|
||||
</string>
|
||||
</resources>
|
|
@ -53,6 +53,7 @@
|
|||
|
||||
<string name="purchase_required_info_only_networking">This feature is only available in the premium version.</string>
|
||||
<string name="purchase_required_info_local_mode_free">This feature is only available in the premium version and the local mode.</string>
|
||||
<string name="purchase_required_info_only_local_mode">This feature is only available in the local mode.</string>
|
||||
|
||||
<string name="purchase_demo_temporarily_notice">
|
||||
You can use the premium version temporarily.
|
||||
|
|
20
app/src/main/res/values/strings-scan-key-dialog.xml
Normal file
20
app/src/main/res/values/strings-scan-key-dialog.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<resources>
|
||||
<string name="scan_key_missing_title">Missing Binary Eye</string>
|
||||
<string name="scan_key_missing_text">The App Binary Eye is required to scan a barcode</string>
|
||||
<string name="scan_key_missing_install">Install Binary Eye</string>
|
||||
</resources>
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
TimeLimit Copyright <C> 2019 - 2020 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.
|
||||
|
@ -34,6 +34,14 @@
|
|||
You sign in by a code which was generated on another device on which TimeLimit is installed.
|
||||
</string>
|
||||
|
||||
<string name="setup_select_mode_parent_key_title">key device of a parent</string>
|
||||
<string name="setup_select_mode_parent_key_text">
|
||||
TimeLimit will not limit this device. This device will be one possible key to
|
||||
unlock the settings of TimeLimit at an other device which uses the local mode (there is always
|
||||
a password to unlock the settings).
|
||||
</string>
|
||||
<string name="setup_select_mode_parent_key_progress">Creating key</string>
|
||||
|
||||
<string name="setup_select_mode_uninstall_title">uninstall TimeLimit</string>
|
||||
<string name="setup_select_mode_uninstall_text">
|
||||
Not a setup option.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue