Add barcode unlocking for the local mode

This commit is contained in:
Jonas Lochmann 2020-05-04 02:00:00 +02:00
parent 0cb4d4a649
commit 43dabe17ab
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
60 changed files with 2690 additions and 40 deletions

View file

@ -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'
}

View file

@ -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 {}

View 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')"
]
}
}

View file

@ -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"

View file

@ -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
}

View file

@ -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])
}
}
}
)
}

View file

@ -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
}

View 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)
}
}

View 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
}

View file

@ -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()

View file

@ -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`)")
}
}
}

View file

@ -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()
}

View file

@ -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")
}

View file

@ -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()
}
}

View file

@ -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) } }
}

View file

@ -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)
}

View file

@ -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()
}
}

View 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
}
}

View file

@ -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 {

View file

@ -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)
}

View file

@ -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(

View file

@ -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()

View file

@ -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!!))
}
}

View file

@ -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(

View file

@ -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)
}

View file

@ -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)
}
}
}

View file

@ -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?)
}

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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
}
}

View file

@ -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?
}
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}

View file

@ -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
}
}

View 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>

View 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>

View file

@ -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" />

View file

@ -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"

View 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 &amp;&amp; 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 &amp;&amp; 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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View file

@ -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.

View 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>

View file

@ -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.

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View file

@ -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.

View 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>

View file

@ -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.