mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-05 10:49:26 +02:00
Add contact whitelist
This commit is contained in:
parent
5e694f0c1c
commit
dc3c2a0023
29 changed files with 1707 additions and 7 deletions
791
app/schemas/io.timelimit.android.data.RoomDatabase/20.json
Normal file
791
app/schemas/io.timelimit.android.data.RoomDatabase/20.json
Normal file
|
@ -0,0 +1,791 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 20,
|
||||
"identityHash": "a59be3b5567854fe9546f64b96f2480e",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "user",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `password` TEXT NOT NULL, `second_password_salt` TEXT NOT NULL, `type` TEXT NOT NULL, `timezone` TEXT NOT NULL, `disable_limits_until` INTEGER NOT NULL, `mail` TEXT NOT NULL, `current_device` TEXT NOT NULL, `category_for_not_assigned_apps` TEXT NOT NULL, `relax_primary_device` INTEGER NOT NULL, `mail_notification_flags` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "password",
|
||||
"columnName": "password",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "secondPasswordSalt",
|
||||
"columnName": "second_password_salt",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeZone",
|
||||
"columnName": "timezone",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "disableLimitsUntil",
|
||||
"columnName": "disable_limits_until",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mail",
|
||||
"columnName": "mail",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "currentDevice",
|
||||
"columnName": "current_device",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryForNotAssignedApps",
|
||||
"columnName": "category_for_not_assigned_apps",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "relaxPrimaryDevice",
|
||||
"columnName": "relax_primary_device",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mailNotificationFlags",
|
||||
"columnName": "mail_notification_flags",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "device",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `model` TEXT NOT NULL, `added_at` INTEGER NOT NULL, `current_user_id` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `network_time` TEXT NOT NULL, `current_protection_level` TEXT NOT NULL, `highest_permission_level` TEXT NOT NULL, `current_usage_stats_permission` TEXT NOT NULL, `highest_usage_stats_permission` TEXT NOT NULL, `current_notification_access_permission` TEXT NOT NULL, `highest_notification_access_permission` TEXT NOT NULL, `current_app_version` INTEGER NOT NULL, `highest_app_version` INTEGER NOT NULL, `tried_disabling_device_admin` INTEGER NOT NULL, `did_reboot` INTEGER NOT NULL, `had_manipulation` INTEGER NOT NULL, `did_report_uninstall` INTEGER NOT NULL, `is_user_kept_signed_in` INTEGER NOT NULL, `show_device_connected` INTEGER NOT NULL, `default_user` TEXT NOT NULL, `default_user_timeout` INTEGER NOT NULL, `consider_reboot_manipulation` INTEGER NOT NULL, `current_overlay_permission` TEXT NOT NULL, `highest_overlay_permission` TEXT NOT NULL, `current_accessibility_service_permission` INTEGER NOT NULL, `was_accessibility_service_permission` INTEGER NOT NULL, `enable_activity_level_blocking` INTEGER NOT NULL, `q_or_later` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "model",
|
||||
"columnName": "model",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "addedAt",
|
||||
"columnName": "added_at",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "currentUserId",
|
||||
"columnName": "current_user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "installedAppsVersion",
|
||||
"columnName": "apps_version",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "networkTime",
|
||||
"columnName": "network_time",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "currentProtectionLevel",
|
||||
"columnName": "current_protection_level",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "highestProtectionLevel",
|
||||
"columnName": "highest_permission_level",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "currentUsageStatsPermission",
|
||||
"columnName": "current_usage_stats_permission",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "highestUsageStatsPermission",
|
||||
"columnName": "highest_usage_stats_permission",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "currentNotificationAccessPermission",
|
||||
"columnName": "current_notification_access_permission",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "highestNotificationAccessPermission",
|
||||
"columnName": "highest_notification_access_permission",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "currentAppVersion",
|
||||
"columnName": "current_app_version",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "highestAppVersion",
|
||||
"columnName": "highest_app_version",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "manipulationTriedDisablingDeviceAdmin",
|
||||
"columnName": "tried_disabling_device_admin",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "manipulationDidReboot",
|
||||
"columnName": "did_reboot",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "hadManipulation",
|
||||
"columnName": "had_manipulation",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "didReportUninstall",
|
||||
"columnName": "did_report_uninstall",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isUserKeptSignedIn",
|
||||
"columnName": "is_user_kept_signed_in",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "showDeviceConnected",
|
||||
"columnName": "show_device_connected",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultUser",
|
||||
"columnName": "default_user",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultUserTimeout",
|
||||
"columnName": "default_user_timeout",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "considerRebootManipulation",
|
||||
"columnName": "consider_reboot_manipulation",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "currentOverlayPermission",
|
||||
"columnName": "current_overlay_permission",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "highestOverlayPermission",
|
||||
"columnName": "highest_overlay_permission",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessibilityServiceEnabled",
|
||||
"columnName": "current_accessibility_service_permission",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wasAccessibilityServiceEnabled",
|
||||
"columnName": "was_accessibility_service_permission",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enableActivityLevelBlocking",
|
||||
"columnName": "enable_activity_level_blocking",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "qOrLater",
|
||||
"columnName": "q_or_later",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "app",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, `title` TEXT NOT NULL, `launchable` INTEGER NOT NULL, `recommendation` TEXT NOT NULL, PRIMARY KEY(`device_id`, `package_name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "deviceId",
|
||||
"columnName": "device_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "package_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLaunchable",
|
||||
"columnName": "launchable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "recommendation",
|
||||
"columnName": "recommendation",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"device_id",
|
||||
"package_name"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_app_device_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"device_id"
|
||||
],
|
||||
"createSql": "CREATE INDEX `index_app_device_id` ON `${TABLE_NAME}` (`device_id`)"
|
||||
},
|
||||
{
|
||||
"name": "index_app_package_name",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"package_name"
|
||||
],
|
||||
"createSql": "CREATE INDEX `index_app_package_name` ON `${TABLE_NAME}` (`package_name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "category_app",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`category_id`, `package_name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "categoryId",
|
||||
"columnName": "category_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "package_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"category_id",
|
||||
"package_name"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_category_app_category_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"category_id"
|
||||
],
|
||||
"createSql": "CREATE INDEX `index_category_app_category_id` ON `${TABLE_NAME}` (`category_id`)"
|
||||
},
|
||||
{
|
||||
"name": "index_category_app_package_name",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"package_name"
|
||||
],
|
||||
"createSql": "CREATE INDEX `index_category_app_package_name` ON `${TABLE_NAME}` (`package_name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "category",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `child_id` TEXT NOT NULL, `title` TEXT NOT NULL, `blocked_times` TEXT NOT NULL, `extra_time` INTEGER NOT NULL, `temporarily_blocked` INTEGER NOT NULL, `base_version` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `rules_version` TEXT NOT NULL, `usedtimes_version` TEXT NOT NULL, `parent_category_id` TEXT NOT NULL, `block_all_notifications` INTEGER NOT NULL, `time_warnings` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "childId",
|
||||
"columnName": "child_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "blockedMinutesInWeek",
|
||||
"columnName": "blocked_times",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "extraTimeInMillis",
|
||||
"columnName": "extra_time",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "temporarilyBlocked",
|
||||
"columnName": "temporarily_blocked",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "baseVersion",
|
||||
"columnName": "base_version",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "assignedAppsVersion",
|
||||
"columnName": "apps_version",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeLimitRulesVersion",
|
||||
"columnName": "rules_version",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "usedTimesVersion",
|
||||
"columnName": "usedtimes_version",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "parentCategoryId",
|
||||
"columnName": "parent_category_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "blockAllNotifications",
|
||||
"columnName": "block_all_notifications",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeWarnings",
|
||||
"columnName": "time_warnings",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "used_time",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`day_of_epoch` INTEGER NOT NULL, `used_time` INTEGER NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `day_of_epoch`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "dayOfEpoch",
|
||||
"columnName": "day_of_epoch",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "usedMillis",
|
||||
"columnName": "used_time",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryId",
|
||||
"columnName": "category_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"category_id",
|
||||
"day_of_epoch"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "time_limit_rule",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `category_id` TEXT NOT NULL, `apply_to_extra_time_usage` INTEGER NOT NULL, `day_mask` INTEGER NOT NULL, `max_time` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryId",
|
||||
"columnName": "category_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "applyToExtraTimeUsage",
|
||||
"columnName": "apply_to_extra_time_usage",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dayMask",
|
||||
"columnName": "day_mask",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "maximumTimeInMillis",
|
||||
"columnName": "max_time",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "config",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "temporarily_allowed_app",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`device_id`, `package_name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "deviceId",
|
||||
"columnName": "device_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "package_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"device_id",
|
||||
"package_name"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "pending_sync_action",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sequence_number` INTEGER NOT NULL, `action` TEXT NOT NULL, `integrity` TEXT NOT NULL, `scheduled_for_upload` INTEGER NOT NULL, `type` TEXT NOT NULL, `user_id` TEXT NOT NULL, PRIMARY KEY(`sequence_number`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "sequenceNumber",
|
||||
"columnName": "sequence_number",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "encodedAction",
|
||||
"columnName": "action",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "integrity",
|
||||
"columnName": "integrity",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "scheduledForUpload",
|
||||
"columnName": "scheduled_for_upload",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "userId",
|
||||
"columnName": "user_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"sequence_number"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_pending_sync_action_scheduled_for_upload",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"scheduled_for_upload"
|
||||
],
|
||||
"createSql": "CREATE INDEX `index_pending_sync_action_scheduled_for_upload` ON `${TABLE_NAME}` (`scheduled_for_upload`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "app_activity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `app_package_name` TEXT NOT NULL, `activity_class_name` TEXT NOT NULL, `activity_title` TEXT NOT NULL, PRIMARY KEY(`device_id`, `app_package_name`, `activity_class_name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "deviceId",
|
||||
"columnName": "device_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appPackageName",
|
||||
"columnName": "app_package_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "activityClassName",
|
||||
"columnName": "activity_class_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "activity_title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"device_id",
|
||||
"app_package_name",
|
||||
"activity_class_name"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "notification",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL, `id` TEXT NOT NULL, `first_notify_time` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, PRIMARY KEY(`type`, `id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "firstNotifyTime",
|
||||
"columnName": "first_notify_time",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDismissed",
|
||||
"columnName": "dismissed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"type",
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
],
|
||||
"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, \"a59be3b5567854fe9546f64b96f2480e\")"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -31,6 +31,8 @@
|
|||
tools:ignore="ProtectedPermissions" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
<uses-feature android:name="android.hardware.telephony" android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".Application"
|
||||
|
|
|
@ -31,6 +31,7 @@ interface Database {
|
|||
fun pendingSyncAction(): PendingSyncActionDao
|
||||
fun appActivity(): AppActivityDao
|
||||
fun notification(): NotificationDao
|
||||
fun allowedContact(): AllowedContactDao
|
||||
|
||||
fun beginTransaction()
|
||||
fun setTransactionSuccessful()
|
||||
|
|
|
@ -141,4 +141,10 @@ object DatabaseMigrations {
|
|||
// a new possible enum value was added, the version upgrade enables the downgrade mechanism
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V20 = object: Migration(19, 20) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `allowed_contact` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `phone` TEXT NOT NULL)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,8 +33,9 @@ import io.timelimit.android.data.model.*
|
|||
TemporarilyAllowedApp::class,
|
||||
PendingSyncAction::class,
|
||||
AppActivity::class,
|
||||
Notification::class
|
||||
], version = 19)
|
||||
Notification::class,
|
||||
AllowedContact::class
|
||||
], version = 20)
|
||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||
companion object {
|
||||
private val lock = Object()
|
||||
|
@ -87,7 +88,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
|
|||
DatabaseMigrations.MIGRATE_TO_V16,
|
||||
DatabaseMigrations.MIGRATE_TO_V17,
|
||||
DatabaseMigrations.MIGRATE_TO_V18,
|
||||
DatabaseMigrations.MIGRATE_TO_V19
|
||||
DatabaseMigrations.MIGRATE_TO_V19,
|
||||
DatabaseMigrations.MIGRATE_TO_V20
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ object DatabaseBackupLowlevel {
|
|||
private const val USER = "user"
|
||||
private const val APP_ACTIVITY = "appActivity"
|
||||
private const val NOTIFICATION = "notification"
|
||||
private const val ALLOWED_CONTACT = "allowedContact"
|
||||
|
||||
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
||||
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
|
||||
|
@ -83,6 +84,7 @@ object DatabaseBackupLowlevel {
|
|||
handleCollection(USER) { offset, pageSize -> database.user().getUserPageSync(offset, pageSize) }
|
||||
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) }
|
||||
|
||||
writer.endObject().flush()
|
||||
}
|
||||
|
@ -201,6 +203,18 @@ object DatabaseBackupLowlevel {
|
|||
|
||||
reader.endArray()
|
||||
}
|
||||
ALLOWED_CONTACT -> {
|
||||
reader.beginArray()
|
||||
|
||||
while (reader.hasNext()) {
|
||||
database.allowedContact().addContactSync(
|
||||
// this will use an unused id
|
||||
AllowedContact.parse(reader).copy(id = 0)
|
||||
)
|
||||
}
|
||||
|
||||
reader.endArray()
|
||||
}
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 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.Query
|
||||
import io.timelimit.android.data.model.AllowedContact
|
||||
|
||||
@Dao
|
||||
interface AllowedContactDao {
|
||||
@Query("SELECT * FROM allowed_contact LIMIT :pageSize OFFSET :offset")
|
||||
fun getAllowedContactPageSync(offset: Int, pageSize: Int): List<AllowedContact>
|
||||
|
||||
@Query("SELECT * FROM allowed_contact")
|
||||
fun getAllowedContactsLive(): LiveData<List<AllowedContact>>
|
||||
|
||||
@Insert
|
||||
fun addContactSync(item: AllowedContact)
|
||||
|
||||
@Query("DELETE FROM allowed_contact WHERE id = :id")
|
||||
fun removeContactSync(id: Int)
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 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.JsonReader
|
||||
import android.util.JsonWriter
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
|
||||
@Entity(tableName = "allowed_contact")
|
||||
data class AllowedContact(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val phone: String
|
||||
): JsonSerializable {
|
||||
companion object {
|
||||
private const val ID = "id"
|
||||
private const val TITLE = "title"
|
||||
private const val PHONE = "phone"
|
||||
|
||||
fun parse(reader: JsonReader): AllowedContact {
|
||||
var id: Int? = null
|
||||
var title: String? = null
|
||||
var phone: String? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
ID -> id = reader.nextInt()
|
||||
TITLE -> title = reader.nextString()
|
||||
PHONE -> phone = reader.nextString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
return AllowedContact(
|
||||
id = id!!,
|
||||
title = title!!,
|
||||
phone = phone!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(ID).value(id)
|
||||
writer.name(TITLE).value(title)
|
||||
writer.name(PHONE).value(phone)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
|
@ -180,4 +180,5 @@ object HintsToShow {
|
|||
const val DEVICE_SCREEN_INTRODUCTION = 2L
|
||||
const val CATEGORIES_INTRODUCTION = 4L
|
||||
const val TIME_LIMIT_RULE_INTRODUCTION = 8L
|
||||
const val CONTACTS_INTRO = 16L
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ package io.timelimit.android.logic
|
|||
import android.util.Log
|
||||
import android.util.SparseArray
|
||||
import android.util.SparseLongArray
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.LiveData
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
|
@ -47,6 +46,8 @@ import kotlinx.coroutines.sync.withLock
|
|||
import java.util.*
|
||||
|
||||
class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||
var pauseBackgroundLoop = false
|
||||
|
||||
companion object {
|
||||
private const val CHECK_PERMISSION_INTERVAL = 10 * 1000L // all 10 seconds
|
||||
private const val BACKGROUND_SERVICE_INTERVAL = 100L // all 100 ms
|
||||
|
@ -280,7 +281,14 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
|
||||
// the following is not executed if the permission is missing
|
||||
|
||||
if (
|
||||
if (pauseBackgroundLoop) {
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
title = appLogic.context.getString(R.string.background_logic_paused_title),
|
||||
text = appLogic.context.getString(R.string.background_logic_paused_text)
|
||||
))
|
||||
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||
} else if (
|
||||
(foregroundAppPackageName == BuildConfig.APPLICATION_ID) ||
|
||||
(foregroundAppPackageName != null && AndroidIntegrationApps.ignoredApps[foregroundAppPackageName].let {
|
||||
when (it) {
|
||||
|
|
|
@ -63,6 +63,7 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
|||
setActivityCheckout(checkout)
|
||||
}
|
||||
}
|
||||
override var ignoreStop: Boolean = false
|
||||
|
||||
val googleSignInUtil = GoogleSignInUtil(this)
|
||||
|
||||
|
@ -134,7 +135,7 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
|||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
if (!isChangingConfigurations) {
|
||||
if ((!isChangingConfigurations) && (!ignoreStop)) {
|
||||
getActivityViewModel().logOut()
|
||||
}
|
||||
|
||||
|
@ -150,6 +151,10 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
|||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
if ((intent?.flags ?: 0) and Intent.FLAG_ACTIVITY_REORDER_TO_FRONT == Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) {
|
||||
return
|
||||
}
|
||||
|
||||
getNavController().popBackStack(R.id.overviewFragment, true)
|
||||
getNavController().handleDeepLink(
|
||||
getNavController().createDeepLink()
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 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.contacts
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.databinding.AddItemViewBinding
|
||||
import io.timelimit.android.databinding.ContactsItemBinding
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class ContactsAdapter: RecyclerView.Adapter<ContactsViewHolder>() {
|
||||
companion object {
|
||||
private const val TYPE_INTRO = 1
|
||||
private const val TYPE_ITEM = 2
|
||||
private const val TYPE_ADD = 3
|
||||
}
|
||||
|
||||
var items: List<ContactsItem>? by Delegates.observable(null as List<ContactsItem>?) { _, _, _ -> notifyDataSetChanged() }
|
||||
var handlers: ContactsHandlers? = null
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items?.size ?: 0
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
val item = items!![position]
|
||||
|
||||
return when (item) {
|
||||
is IntroContactsItem -> Long.MAX_VALUE
|
||||
is AddContactsItem -> Long.MAX_VALUE - 1
|
||||
is ContactContactsItem -> item.item.id.toLong()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = when (items!![position]) {
|
||||
is IntroContactsItem -> TYPE_INTRO
|
||||
is ContactContactsItem -> TYPE_ITEM
|
||||
is AddContactsItem -> TYPE_ADD
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactsViewHolder = when (viewType) {
|
||||
TYPE_INTRO -> ContactsStaticHolder(
|
||||
LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.contacts_intro, parent, false)
|
||||
)
|
||||
TYPE_ITEM -> ContactsItemHolder(
|
||||
ContactsItemBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
)
|
||||
TYPE_ADD -> ContactsStaticHolder(
|
||||
AddItemViewBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
).let {
|
||||
it.label = parent.context.getString(R.string.contacts_add)
|
||||
|
||||
it.root.setOnClickListener {
|
||||
handlers?.onAddContactClicked()
|
||||
}
|
||||
|
||||
it.root
|
||||
}
|
||||
)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ContactsViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is ContactsStaticHolder -> {/* nothing to do */}
|
||||
is ContactsItemHolder -> {
|
||||
val item = items!![position]
|
||||
|
||||
item as ContactContactsItem
|
||||
|
||||
holder.view.title = item.item.title
|
||||
holder.view.phone = item.item.phone
|
||||
|
||||
holder.view.card.setOnClickListener { handlers?.onContactClicked(item) }
|
||||
holder.view.card.setOnLongClickListener { handlers?.onContactLongClicked(item) ?: false }
|
||||
|
||||
holder.view.executePendingBindings()
|
||||
|
||||
null
|
||||
}
|
||||
}.let {/* require handling all cases */}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ContactsViewHolder(root: View): RecyclerView.ViewHolder(root)
|
||||
class ContactsStaticHolder(root: View): ContactsViewHolder(root)
|
||||
class ContactsItemHolder(val view: ContactsItemBinding): ContactsViewHolder(view.root)
|
||||
|
||||
interface ContactsHandlers {
|
||||
fun onAddContactClicked()
|
||||
fun onContactLongClicked(item: ContactContactsItem): Boolean
|
||||
fun onContactClicked(item: ContactContactsItem)
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 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.contacts
|
||||
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import io.timelimit.android.R
|
||||
import android.provider.ContactsContract
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.data.model.AllowedContact
|
||||
import io.timelimit.android.databinding.ContactsFragmentBinding
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.ui.MainActivity
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||
import io.timelimit.android.util.PhoneNumberUtils
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
||||
class ContactsFragment : Fragment() {
|
||||
companion object {
|
||||
private const val LOG_TAG = "ContactsFragment"
|
||||
private const val REQ_SELECT_CONTACT = 1
|
||||
private const val REQ_CALL_PERMISSION = 2
|
||||
}
|
||||
|
||||
private val model: ContactsModel by lazy {
|
||||
ViewModelProviders.of(this).get(ContactsModel::class.java)
|
||||
}
|
||||
|
||||
private val activityModelHolder: ActivityViewModelHolder by lazy { activity as ActivityViewModelHolder }
|
||||
private val auth: ActivityViewModel by lazy { activityModelHolder.getActivityViewModel() }
|
||||
private var numberToCallWithPermission: String? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = ContactsFragmentBinding.inflate(inflater, container, false)
|
||||
val adapter = ContactsAdapter()
|
||||
|
||||
model.listItems.observe(this, Observer { adapter.items = it })
|
||||
|
||||
binding.recycler.layoutManager = LinearLayoutManager(context)
|
||||
binding.recycler.adapter = adapter
|
||||
|
||||
adapter.handlers = object: ContactsHandlers {
|
||||
override fun onAddContactClicked() {
|
||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||
activityModelHolder.ignoreStop = true
|
||||
|
||||
showContactSelection()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContactLongClicked(item: ContactContactsItem): Boolean {
|
||||
removeItem(item.item)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onContactClicked(item: ContactContactsItem) {
|
||||
startCall(item.item.phone)
|
||||
}
|
||||
}
|
||||
|
||||
ItemTouchHelper(object: ItemTouchHelper.Callback() {
|
||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||
val item = adapter.items!![viewHolder.adapterPosition]
|
||||
|
||||
if (item is ContactContactsItem && auth.isParentAuthenticated()) {
|
||||
return makeMovementFlags(0, ItemTouchHelper.START or ItemTouchHelper.END)
|
||||
} else if (item is IntroContactsItem) {
|
||||
return makeMovementFlags(0, ItemTouchHelper.START or ItemTouchHelper.END)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
||||
// ignore
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
val item = adapter.items!![viewHolder.adapterPosition]
|
||||
|
||||
if (item is ContactContactsItem) {
|
||||
removeItem(item.item)
|
||||
} else if (item is IntroContactsItem) {
|
||||
model.hideIntro()
|
||||
}
|
||||
}
|
||||
}).attachToRecyclerView(binding.recycler)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun showContactSelection() {
|
||||
startActivityForResult(
|
||||
Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
|
||||
.setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE),
|
||||
REQ_SELECT_CONTACT
|
||||
)
|
||||
}
|
||||
|
||||
private fun removeItem(item: AllowedContact) {
|
||||
if (auth.isParentAuthenticated()) {
|
||||
model.removeContact(item.id)
|
||||
|
||||
Snackbar.make(view!!, getString(R.string.contacts_snackbar_removed, item.title), Snackbar.LENGTH_SHORT)
|
||||
.setAction(R.string.generic_undo) {
|
||||
model.addContact(item)
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
Snackbar.make(view!!, R.string.contacts_snackbar_remove_auth, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startCall(number: String) {
|
||||
if (ContextCompat.checkSelfPermission(context!!, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
|
||||
val logic = DefaultAppLogic.with(context!!)
|
||||
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_CALL, Uri.parse("tel:" + PhoneNumberUtils.normalizeNumber(number)))
|
||||
|
||||
logic.backgroundTaskLogic.pauseBackgroundLoop = true
|
||||
|
||||
startActivity(intent)
|
||||
|
||||
runAsync {
|
||||
delay(500)
|
||||
|
||||
startActivity(
|
||||
Intent(context!!, MainActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
)
|
||||
|
||||
delay(200)
|
||||
|
||||
logic.backgroundTaskLogic.pauseBackgroundLoop = false
|
||||
|
||||
delay(500)
|
||||
|
||||
Snackbar.make(view!!, R.string.contacts_snackbar_call_started, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "could not start call", ex)
|
||||
}
|
||||
|
||||
logic.backgroundTaskLogic.pauseBackgroundLoop = false
|
||||
|
||||
Snackbar.make(view!!, R.string.contacts_snackbar_call_failed, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
numberToCallWithPermission = number
|
||||
requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), REQ_CALL_PERMISSION)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (requestCode == REQ_SELECT_CONTACT) {
|
||||
activityModelHolder.ignoreStop = false
|
||||
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
data?.data?.let { contactData ->
|
||||
val cursor = context!!.contentResolver.query(contactData, null, null, null, null)
|
||||
|
||||
cursor?.use {
|
||||
if (cursor.moveToFirst()) {
|
||||
val title = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME))
|
||||
val phoneNumber = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
|
||||
|
||||
model.addContact(title = title, phoneNumber = phoneNumber)
|
||||
|
||||
Snackbar.make(view!!, R.string.contacts_snackbar_added, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
if (requestCode == REQ_CALL_PERMISSION) {
|
||||
if (grantResults.size == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
numberToCallWithPermission?.let { number -> startCall(number) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 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.contacts
|
||||
|
||||
import io.timelimit.android.data.model.AllowedContact
|
||||
|
||||
sealed class ContactsItem
|
||||
object IntroContactsItem: ContactsItem()
|
||||
object AddContactsItem: ContactsItem()
|
||||
data class ContactContactsItem(val item: AllowedContact): ContactsItem()
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 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.contacts
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.data.model.AllowedContact
|
||||
import io.timelimit.android.data.model.HintsToShow
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.switchMap
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
|
||||
class ContactsModel(application: Application): AndroidViewModel(application) {
|
||||
private val appLogic = DefaultAppLogic.with(application)
|
||||
private val allowedContacts = appLogic.database.allowedContact().getAllowedContactsLive()
|
||||
private val didHideIntro = appLogic.database.config().wereHintsShown(HintsToShow.CONTACTS_INTRO)
|
||||
|
||||
private val convertedContactItems = allowedContacts.map { items -> items.map { ContactContactsItem(it) } }
|
||||
private val baseListItems = convertedContactItems.map { list -> list + listOf(AddContactsItem) }
|
||||
|
||||
val listItems = didHideIntro.switchMap { hideIntro ->
|
||||
baseListItems.map { baseItems ->
|
||||
if (hideIntro) {
|
||||
baseItems
|
||||
} else {
|
||||
listOf(IntroContactsItem) + baseItems
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addContact(title: String, phoneNumber: String) {
|
||||
Threads.database.submit {
|
||||
appLogic.database.allowedContact().addContactSync(AllowedContact(
|
||||
id = 0,
|
||||
phone = phoneNumber,
|
||||
title = title
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fun addContact(item: AllowedContact) {
|
||||
Threads.database.submit { appLogic.database.allowedContact().addContactSync(item) }
|
||||
}
|
||||
|
||||
fun removeContact(id: Int) {
|
||||
Threads.database.submit { appLogic.database.allowedContact().removeContactSync(id) }
|
||||
}
|
||||
|
||||
fun hideIntro() {
|
||||
Threads.database.submit { appLogic.database.config().setHintsShownSync(HintsToShow.CONTACTS_INTRO) }
|
||||
}
|
||||
}
|
|
@ -54,6 +54,8 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
|
|||
}
|
||||
}
|
||||
|
||||
override var ignoreStop: Boolean = false
|
||||
|
||||
private val blockedPackageName: String by lazy {
|
||||
intent.getStringExtra(EXTRA_PACKAGE_NAME)
|
||||
}
|
||||
|
@ -115,7 +117,7 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
|
|||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
if (!isChangingConfigurations) {
|
||||
if ((!isChangingConfigurations) && (!ignoreStop)) {
|
||||
getActivityViewModel().logOut()
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.app.Activity
|
|||
interface ActivityViewModelHolder {
|
||||
fun getActivityViewModel(): ActivityViewModel
|
||||
fun showAuthenticationScreen()
|
||||
var ignoreStop: Boolean
|
||||
}
|
||||
|
||||
fun getActivityViewModel(activity: Activity): ActivityViewModel {
|
||||
|
|
|
@ -36,6 +36,8 @@ class UnlockAfterManipulationActivity : AppCompatActivity(), ActivityViewModelHo
|
|||
ViewModelProviders.of(this).get(ActivityViewModel::class.java)
|
||||
}
|
||||
|
||||
override var ignoreStop: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_unlock_after_manipulation)
|
||||
|
|
|
@ -36,6 +36,7 @@ import io.timelimit.android.livedata.switchMap
|
|||
import io.timelimit.android.livedata.waitForNullableValue
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.ui.contacts.ContactsFragment
|
||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||
import io.timelimit.android.ui.main.AuthenticationFab
|
||||
import io.timelimit.android.ui.manage.device.add.AddDeviceFragment
|
||||
|
@ -113,6 +114,7 @@ class MainFragment : Fragment(), OverviewFragmentParentHandlers, AboutFragmentPa
|
|||
fun updateShowFab(selectedItemId: Int) {
|
||||
showAuthButtonLive.value = when (selectedItemId) {
|
||||
R.id.main_tab_overview -> true
|
||||
R.id.main_tab_contacts -> true
|
||||
R.id.main_tab_uninstall -> !BuildConfig.storeCompilant
|
||||
R.id.main_tab_about -> false
|
||||
else -> throw IllegalStateException()
|
||||
|
@ -125,6 +127,7 @@ class MainFragment : Fragment(), OverviewFragmentParentHandlers, AboutFragmentPa
|
|||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.replace(R.id.container, when(menuItem.itemId) {
|
||||
R.id.main_tab_overview -> OverviewFragment()
|
||||
R.id.main_tab_contacts -> ContactsFragment()
|
||||
R.id.main_tab_uninstall -> UninstallFragment()
|
||||
R.id.main_tab_about -> AboutFragment()
|
||||
else -> throw IllegalStateException()
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
// this is a reduced version of https://raw.githubusercontent.com/aosp-mirror/platform_frameworks_base/master/telephony/java/android/telephony/PhoneNumberUtils.java
|
||||
|
||||
/*
|
||||
* Copyright (C) 2006 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.timelimit.android.util;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.SparseIntArray;
|
||||
|
||||
public class PhoneNumberUtils {
|
||||
private PhoneNumberUtils() {}
|
||||
|
||||
|
||||
/**
|
||||
* Normalize a phone number by removing the characters other than digits. If
|
||||
* the given number has keypad letters, the letters will be converted to
|
||||
* digits first.
|
||||
*
|
||||
* @param phoneNumber the number to be normalized.
|
||||
* @return the normalized number.
|
||||
*/
|
||||
public static String normalizeNumber(String phoneNumber) {
|
||||
if (TextUtils.isEmpty(phoneNumber)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int len = phoneNumber.length();
|
||||
for (int i = 0; i < len; i++) {
|
||||
char c = phoneNumber.charAt(i);
|
||||
// Character.digit() supports ASCII and Unicode digits (fullwidth, Arabic-Indic, etc.)
|
||||
int digit = Character.digit(c, 10);
|
||||
if (digit != -1) {
|
||||
sb.append(digit);
|
||||
} else if (sb.length() == 0 && c == '+') {
|
||||
sb.append(c);
|
||||
} else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
|
||||
return normalizeNumber(PhoneNumberUtils.convertKeypadLettersToDigits(phoneNumber));
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates any alphabetic letters (i.e. [A-Za-z]) in the
|
||||
* specified phone number into the equivalent numeric digits,
|
||||
* according to the phone keypad letter mapping described in
|
||||
* ITU E.161 and ISO/IEC 9995-8.
|
||||
*
|
||||
* @return the input string, with alpha letters converted to numeric
|
||||
* digits using the phone keypad letter mapping. For example,
|
||||
* an input of "1-800-GOOG-411" will return "1-800-4664-411".
|
||||
*/
|
||||
public static String convertKeypadLettersToDigits(String input) {
|
||||
if (input == null) {
|
||||
return input;
|
||||
}
|
||||
int len = input.length();
|
||||
if (len == 0) {
|
||||
return input;
|
||||
}
|
||||
|
||||
char[] out = input.toCharArray();
|
||||
|
||||
for (int i = 0; i < len; i++) {
|
||||
char c = out[i];
|
||||
// If this char isn't in KEYPAD_MAP at all, just leave it alone.
|
||||
out[i] = (char) KEYPAD_MAP.get(c, c);
|
||||
}
|
||||
|
||||
return new String(out);
|
||||
}
|
||||
|
||||
/**
|
||||
* The phone keypad letter mapping (see ITU E.161 or ISO/IEC 9995-8.)
|
||||
*/
|
||||
private static final SparseIntArray KEYPAD_MAP = new SparseIntArray();
|
||||
static {
|
||||
KEYPAD_MAP.put('a', '2'); KEYPAD_MAP.put('b', '2'); KEYPAD_MAP.put('c', '2');
|
||||
KEYPAD_MAP.put('A', '2'); KEYPAD_MAP.put('B', '2'); KEYPAD_MAP.put('C', '2');
|
||||
|
||||
KEYPAD_MAP.put('d', '3'); KEYPAD_MAP.put('e', '3'); KEYPAD_MAP.put('f', '3');
|
||||
KEYPAD_MAP.put('D', '3'); KEYPAD_MAP.put('E', '3'); KEYPAD_MAP.put('F', '3');
|
||||
|
||||
KEYPAD_MAP.put('g', '4'); KEYPAD_MAP.put('h', '4'); KEYPAD_MAP.put('i', '4');
|
||||
KEYPAD_MAP.put('G', '4'); KEYPAD_MAP.put('H', '4'); KEYPAD_MAP.put('I', '4');
|
||||
|
||||
KEYPAD_MAP.put('j', '5'); KEYPAD_MAP.put('k', '5'); KEYPAD_MAP.put('l', '5');
|
||||
KEYPAD_MAP.put('J', '5'); KEYPAD_MAP.put('K', '5'); KEYPAD_MAP.put('L', '5');
|
||||
|
||||
KEYPAD_MAP.put('m', '6'); KEYPAD_MAP.put('n', '6'); KEYPAD_MAP.put('o', '6');
|
||||
KEYPAD_MAP.put('M', '6'); KEYPAD_MAP.put('N', '6'); KEYPAD_MAP.put('O', '6');
|
||||
|
||||
KEYPAD_MAP.put('p', '7'); KEYPAD_MAP.put('q', '7'); KEYPAD_MAP.put('r', '7'); KEYPAD_MAP.put('s', '7');
|
||||
KEYPAD_MAP.put('P', '7'); KEYPAD_MAP.put('Q', '7'); KEYPAD_MAP.put('R', '7'); KEYPAD_MAP.put('S', '7');
|
||||
|
||||
KEYPAD_MAP.put('t', '8'); KEYPAD_MAP.put('u', '8'); KEYPAD_MAP.put('v', '8');
|
||||
KEYPAD_MAP.put('T', '8'); KEYPAD_MAP.put('U', '8'); KEYPAD_MAP.put('V', '8');
|
||||
|
||||
KEYPAD_MAP.put('w', '9'); KEYPAD_MAP.put('x', '9'); KEYPAD_MAP.put('y', '9'); KEYPAD_MAP.put('z', '9');
|
||||
KEYPAD_MAP.put('W', '9'); KEYPAD_MAP.put('X', '9'); KEYPAD_MAP.put('Y', '9'); KEYPAD_MAP.put('Z', '9');
|
||||
}}
|
|
@ -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="M19,3h-1L18,1h-2v2L8,3L8,1L6,1v2L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,6c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM18,18L6,18v-1c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1v1z"/>
|
||||
</vector>
|
24
app/src/main/res/layout/contacts_fragment.xml
Normal file
24
app/src/main/res/layout/contacts_fragment.xml
Normal file
|
@ -0,0 +1,24 @@
|
|||
<!--
|
||||
TimeLimit Copyright <C> 2019 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"
|
||||
tools:context="io.timelimit.android.ui.contacts.ContactsFragment">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</layout>
|
46
app/src/main/res/layout/contacts_intro.xml
Normal file
46
app/src/main/res/layout/contacts_intro.xml
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 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/>.
|
||||
-->
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
app:cardUseCompatPadding="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<LinearLayout
|
||||
android:padding="8dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:text="@string/contacts_title_long"
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@string/contacts_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
android:text="@string/contacts_intro_swipe"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
57
app/src/main/res/layout/contacts_item.xml
Normal file
57
app/src/main/res/layout/contacts_item.xml
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 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="title"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="phone"
|
||||
type="String" />
|
||||
</data>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/card"
|
||||
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
|
||||
tools:text="Max Mustermann"
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
android:text="@{title}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
tools:text="+49 1234 567890123"
|
||||
android:text="@{phone}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</layout>
|
|
@ -19,6 +19,11 @@
|
|||
android:title="@string/main_tab_overview"
|
||||
android:id="@+id/main_tab_overview" />
|
||||
|
||||
<item
|
||||
android:icon="@drawable/ic_perm_contact_calendar_black_24dp"
|
||||
android:title="@string/contacts_title"
|
||||
android:id="@+id/main_tab_contacts" />
|
||||
|
||||
<item
|
||||
android:icon="@drawable/ic_delete_black_24dp"
|
||||
android:title="@string/main_tab_uninstall"
|
||||
|
|
|
@ -31,4 +31,7 @@
|
|||
|
||||
<string name="background_logic_timeout_title">automatische Abmeldung aktiviert</string>
|
||||
<string name="background_logic_timeout_text">TimeLimit läuft noch, um das zu ermöglichen</string>
|
||||
|
||||
<string name="background_logic_paused_title">Vorübergehend deaktiviert</string>
|
||||
<string name="background_logic_paused_text">Keine Einschränkungen</string>
|
||||
</resources>
|
||||
|
|
32
app/src/main/res/values-de/strings-contacts.xml
Normal file
32
app/src/main/res/values-de/strings-contacts.xml
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 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="contacts_title">Kontakte</string>
|
||||
<string name="contacts_snackbar_added">Kontakt wurde hinzugefügt</string>
|
||||
<string name="contacts_title_long">Erlaubte Kontakte</string>
|
||||
<string name="contacts_description">Das ist eine Liste von Telefonnummern.
|
||||
Eltern können Einträge von der Systemkontaktliste hinzufügen.
|
||||
Jeder Nutzer diese Gerätes kann über diese Ansicht einen Anruf zu diesen Nummern starten,
|
||||
auch wenn die Telefon-App gesperrt ist.
|
||||
Die Telefonnummern werden nur auf diesem Gerät gespeichert.
|
||||
</string>
|
||||
<string name="contacts_intro_swipe">Se können diesen Hinweis entfernen, indem Sie ihn zur Seite wischen</string>
|
||||
<string name="contacts_add">Kontakt hinzufügen</string>
|
||||
<string name="contacts_snackbar_remove_auth">Sie müssen sich anmelden um Kontakte zu entfernen</string>
|
||||
<string name="contacts_snackbar_removed">%s wurde entfernt</string>
|
||||
<string name="contacts_snackbar_call_failed">Anruf fehlgeschlagen</string>
|
||||
<string name="contacts_snackbar_call_started">Anruf gestartet; Kann per Benachrichtigungsbereich beendet werden</string>
|
||||
</resources>
|
|
@ -31,4 +31,7 @@
|
|||
|
||||
<string name="background_logic_timeout_title">auto logout enabled</string>
|
||||
<string name="background_logic_timeout_text">TimeLimit still runs to do that</string>
|
||||
|
||||
<string name="background_logic_paused_title">Temporarily disabled</string>
|
||||
<string name="background_logic_paused_text">No limitations apply</string>
|
||||
</resources>
|
||||
|
|
32
app/src/main/res/values/strings-contacts.xml
Normal file
32
app/src/main/res/values/strings-contacts.xml
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 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="contacts_title">Contacts</string>
|
||||
<string name="contacts_snackbar_added">Contact was added</string>
|
||||
<string name="contacts_title_long">Allowed contacts</string>
|
||||
<string name="contacts_description">This is a list of phone numbers.
|
||||
Parents can add items from the contact list of the device.
|
||||
Every user of this device can start a call to one of these numbers using this screen.
|
||||
This works if the phone app is blocked.
|
||||
The phone numbers are only saved at this device and are never transmitted.
|
||||
</string>
|
||||
<string name="contacts_intro_swipe">Swipe to the side to remove this message</string>
|
||||
<string name="contacts_add">Add contact</string>
|
||||
<string name="contacts_snackbar_remove_auth">You must sign in to remove contacts</string>
|
||||
<string name="contacts_snackbar_removed">%s was removed</string>
|
||||
<string name="contacts_snackbar_call_failed">Could not start call</string>
|
||||
<string name="contacts_snackbar_call_started">Call started; You can hang up using the notification area</string>
|
||||
</resources>
|
Loading…
Add table
Add a link
Reference in a new issue