mirror of
https://codeberg.org/timelimit/opentimelimit-android.git
synced 2025-10-03 01:39:21 +02:00
Import changes from TimeLimit
This commit is contained in:
parent
799a890989
commit
a9b1c824f8
194 changed files with 7804 additions and 895 deletions
|
@ -1,9 +1,8 @@
|
||||||
## bug reports and feature requests
|
## bug reports and feature requests
|
||||||
|
|
||||||
- open a ticket here at GitLab
|
- open a ticket here at GitLab
|
||||||
- alternativly, send a message to support@timelimit.io
|
- alternatively, send a message to support@timelimit.io
|
||||||
|
|
||||||
## merge requests
|
## merge requests
|
||||||
|
|
||||||
This App and the proprietary TimeLimit App are developed by the same developer who prefers to keep them similar to make the maintance easier.
|
Are possible but only after talking with the developer before developing anything.
|
||||||
Due to that, merge requests are not wanted to avoid licensing issues when adding something from a merge request to the proprietary version.
|
|
|
@ -25,18 +25,21 @@ androidExtensions {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 28
|
compileSdkVersion 29
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "io.timelimit.android.open"
|
applicationId "io.timelimit.android.open"
|
||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
targetSdkVersion 28
|
targetSdkVersion 29
|
||||||
versionCode 5
|
versionCode 50
|
||||||
versionName "0.2.3"
|
versionName "1.5.1"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
kapt {
|
kapt {
|
||||||
arguments {
|
arguments {
|
||||||
arg("room.schemaLocation", "$projectDir/schemas".toString())
|
arg("room.schemaLocation", "$projectDir/schemas".toString())
|
||||||
}
|
}
|
||||||
|
javacOptions {
|
||||||
|
option("-Xmaxerrs", 500)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +66,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
def nav_version = "1.0.0-beta02"
|
def nav_version = "1.0.0"
|
||||||
def room_version = "2.0.0"
|
def room_version = "2.0.0"
|
||||||
def paging_version = "2.1.0"
|
def paging_version = "2.1.0"
|
||||||
|
|
||||||
|
|
587
app/schemas/io.timelimit.android.data.RoomDatabase/5.json
Normal file
587
app/schemas/io.timelimit.android.data.RoomDatabase/5.json
Normal file
|
@ -0,0 +1,587 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 5,
|
||||||
|
"identityHash": "56a9f03550c893f49f3487dad7c271b4",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "user",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `password` TEXT NOT NULL, `type` TEXT NOT NULL, `timezone` TEXT NOT NULL, `disable_limits_until` INTEGER NOT NULL, `category_for_not_assigned_apps` 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": "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": "categoryForNotAssignedApps",
|
||||||
|
"columnName": "category_for_not_assigned_apps",
|
||||||
|
"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, `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, `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": "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": "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}` (`package_name` TEXT NOT NULL, `title` TEXT NOT NULL, `launchable` INTEGER NOT NULL, `recommendation` TEXT NOT NULL, PRIMARY KEY(`package_name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"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": [
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"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, `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": "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}` (`package_name` TEXT NOT NULL, PRIMARY KEY(`package_name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"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": "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, \"56a9f03550c893f49f3487dad7c271b4\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,7 @@
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
<!-- suppress DeprecatedClassUsageInspection -->
|
<!-- suppress DeprecatedClassUsageInspection -->
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.GET_TASKS"
|
android:name="android.permission.GET_TASKS"
|
||||||
|
@ -29,11 +30,14 @@
|
||||||
tools:ignore="ProtectedPermissions" />
|
tools:ignore="ProtectedPermissions" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<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
|
<application
|
||||||
android:name=".Application"
|
android:name=".Application"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
|
@ -111,6 +115,19 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service android:name=".integration.platform.android.AccessibilityService"
|
||||||
|
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data android:name="android.accessibilityservice"
|
||||||
|
android:resource="@xml/accesibility" />
|
||||||
|
|
||||||
|
</service>
|
||||||
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -28,6 +28,8 @@ interface Database {
|
||||||
fun usedTimes(): UsedTimeDao
|
fun usedTimes(): UsedTimeDao
|
||||||
fun user(): UserDao
|
fun user(): UserDao
|
||||||
fun temporarilyAllowedApp(): TemporarilyAllowedAppDao
|
fun temporarilyAllowedApp(): TemporarilyAllowedAppDao
|
||||||
|
fun appActivity(): AppActivityDao
|
||||||
|
fun allowedContact(): AllowedContactDao
|
||||||
|
|
||||||
fun beginTransaction()
|
fun beginTransaction()
|
||||||
fun setTransactionSuccessful()
|
fun setTransactionSuccessful()
|
||||||
|
|
|
@ -22,4 +22,28 @@ object DatabaseMigrations {
|
||||||
database.execSQL("ALTER TABLE `device` ADD COLUMN `consider_reboot_manipulation` INTEGER NOT NULL DEFAULT 0")
|
database.execSQL("ALTER TABLE `device` ADD COLUMN `consider_reboot_manipulation` INTEGER NOT NULL DEFAULT 0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val MIGRATE_TO_V5 = object: Migration(4, 5) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
// device table
|
||||||
|
database.execSQL("ALTER TABLE `device` ADD COLUMN `current_overlay_permission` TEXT NOT NULL DEFAULT \"not granted\"")
|
||||||
|
database.execSQL("ALTER TABLE `device` ADD COLUMN `highest_overlay_permission` TEXT NOT NULL DEFAULT \"not granted\"")
|
||||||
|
database.execSQL("ALTER TABLE `device` ADD COLUMN `current_accessibility_service_permission` INTEGER NOT NULL DEFAULT 0")
|
||||||
|
database.execSQL("ALTER TABLE `device` ADD COLUMN `was_accessibility_service_permission` INTEGER NOT NULL DEFAULT 0")
|
||||||
|
database.execSQL("ALTER TABLE `device` ADD COLUMN `enable_activity_level_blocking` INTEGER NOT NULL DEFAULT 0")
|
||||||
|
database.execSQL("ALTER TABLE `device` ADD COLUMN `q_or_later` INTEGER NOT NULL DEFAULT 0")
|
||||||
|
database.execSQL("ALTER TABLE `device` ADD COLUMN `default_user` TEXT NOT NULL DEFAULT \"\"")
|
||||||
|
database.execSQL("ALTER TABLE `device` ADD COLUMN `default_user_timeout` INTEGER NOT NULL DEFAULT 0")
|
||||||
|
|
||||||
|
// category table
|
||||||
|
database.execSQL("ALTER TABLE `category` ADD COLUMN `block_all_notifications` INTEGER NOT NULL DEFAULT 0")
|
||||||
|
database.execSQL("ALTER TABLE `category` ADD COLUMN `time_warnings` INTEGER NOT NULL DEFAULT 0")
|
||||||
|
|
||||||
|
// app_activity table
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `app_activity` (`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`))")
|
||||||
|
|
||||||
|
// allowed_contact table
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `allowed_contact` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `phone` TEXT NOT NULL)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -30,8 +30,10 @@ import io.timelimit.android.data.model.*
|
||||||
UsedTimeItem::class,
|
UsedTimeItem::class,
|
||||||
TimeLimitRule::class,
|
TimeLimitRule::class,
|
||||||
ConfigurationItem::class,
|
ConfigurationItem::class,
|
||||||
TemporarilyAllowedApp::class
|
TemporarilyAllowedApp::class,
|
||||||
], version = 4)
|
AppActivity::class,
|
||||||
|
AllowedContact::class
|
||||||
|
], version = 5)
|
||||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||||
companion object {
|
companion object {
|
||||||
private val lock = Object()
|
private val lock = Object()
|
||||||
|
@ -69,7 +71,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
|
||||||
.addMigrations(
|
.addMigrations(
|
||||||
DatabaseMigrations.MIGRATE_TO_V2,
|
DatabaseMigrations.MIGRATE_TO_V2,
|
||||||
DatabaseMigrations.MIGRATE_TO_V3,
|
DatabaseMigrations.MIGRATE_TO_V3,
|
||||||
DatabaseMigrations.MIGRATE_TO_V4
|
DatabaseMigrations.MIGRATE_TO_V4,
|
||||||
|
DatabaseMigrations.MIGRATE_TO_V5
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,8 @@ object DatabaseBackupLowlevel {
|
||||||
private const val TIME_LIMIT_RULE = "timelimitRule"
|
private const val TIME_LIMIT_RULE = "timelimitRule"
|
||||||
private const val USED_TIME_ITEM = "usedTime"
|
private const val USED_TIME_ITEM = "usedTime"
|
||||||
private const val USER = "user"
|
private const val USER = "user"
|
||||||
|
private const val APP_ACTIVITY = "appActivity"
|
||||||
|
private const val ALLOWED_CONTACT = "allowedContact"
|
||||||
|
|
||||||
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
||||||
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
|
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
|
||||||
|
@ -77,6 +79,9 @@ object DatabaseBackupLowlevel {
|
||||||
handleCollection(TIME_LIMIT_RULE) { offset, pageSize -> database.timeLimitRules().getRulePageSync(offset, pageSize) }
|
handleCollection(TIME_LIMIT_RULE) { offset, pageSize -> database.timeLimitRules().getRulePageSync(offset, pageSize) }
|
||||||
handleCollection(USED_TIME_ITEM) { offset, pageSize -> database.usedTimes().getUsedTimePageSync(offset, pageSize) }
|
handleCollection(USED_TIME_ITEM) { offset, pageSize -> database.usedTimes().getUsedTimePageSync(offset, pageSize) }
|
||||||
handleCollection(USER) { offset, pageSize -> database.user().getUserPageSync(offset, pageSize) }
|
handleCollection(USER) { offset, pageSize -> database.user().getUserPageSync(offset, pageSize) }
|
||||||
|
handleCollection(APP_ACTIVITY) { offset, pageSize -> database.appActivity().getAppActivityPageSync(offset, pageSize) }
|
||||||
|
handleCollection(ALLOWED_CONTACT) { offset, pageSize -> database.allowedContact().getAllowedContactPageSync(offset, pageSize) }
|
||||||
|
|
||||||
|
|
||||||
writer.endObject().flush()
|
writer.endObject().flush()
|
||||||
}
|
}
|
||||||
|
@ -168,6 +173,27 @@ object DatabaseBackupLowlevel {
|
||||||
|
|
||||||
reader.endArray()
|
reader.endArray()
|
||||||
}
|
}
|
||||||
|
APP_ACTIVITY -> {
|
||||||
|
reader.beginArray()
|
||||||
|
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
database.appActivity().addAppActivitySync(AppActivity.parse(reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
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,48 @@
|
||||||
|
/*
|
||||||
|
* 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.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import io.timelimit.android.data.model.AppActivity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface AppActivityDao {
|
||||||
|
@Query("SELECT * FROM app_activity LIMIT :pageSize OFFSET :offset")
|
||||||
|
fun getAppActivityPageSync(offset: Int, pageSize: Int): List<AppActivity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM app_activity WHERE device_id IN (:deviceIds)")
|
||||||
|
fun getAppActivitiesByDeviceIds(deviceIds: List<String>): LiveData<List<AppActivity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM app_activity WHERE app_package_name = :packageName")
|
||||||
|
fun getAppActivitiesByPackageName(packageName: String): LiveData<List<AppActivity>>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun addAppActivitySync(item: AppActivity)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun addAppActivitiesSync(items: List<AppActivity>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM app_activity WHERE device_id = :deviceId AND app_package_name = :packageName AND activity_class_name IN (:activities)")
|
||||||
|
fun deleteAppActivitiesSync(deviceId: String, packageName: String, activities: List<String>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM app_activity WHERE device_id IN (:deviceIds)")
|
||||||
|
fun deleteAppActivitiesByDeviceIds(deviceIds: List<String>)
|
||||||
|
}
|
|
@ -68,6 +68,9 @@ abstract class CategoryDao {
|
||||||
|
|
||||||
@Query("UPDATE category SET parent_category_id = :parentCategoryId WHERE id = :categoryId")
|
@Query("UPDATE category SET parent_category_id = :parentCategoryId WHERE id = :categoryId")
|
||||||
abstract fun updateParentCategory(categoryId: String, parentCategoryId: String)
|
abstract fun updateParentCategory(categoryId: String, parentCategoryId: String)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
abstract fun updateCategorySync(category: Category)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CategoryShortInfo(
|
data class CategoryShortInfo(
|
||||||
|
|
|
@ -112,4 +112,19 @@ abstract class ConfigDao {
|
||||||
|
|
||||||
fun wasDeviceLockedSync() = getValueOfKeySync(ConfigurationItemType.WasDeviceLocked) == "true"
|
fun wasDeviceLockedSync() = getValueOfKeySync(ConfigurationItemType.WasDeviceLocked) == "true"
|
||||||
fun setWasDeviceLockedSync(value: Boolean) = updateValueSync(ConfigurationItemType.WasDeviceLocked, if (value) "true" else "false")
|
fun setWasDeviceLockedSync(value: Boolean) = updateValueSync(ConfigurationItemType.WasDeviceLocked, if (value) "true" else "false")
|
||||||
|
|
||||||
|
fun getForegroundAppQueryIntervalAsync(): LiveData<Long> = getValueOfKeyAsync(ConfigurationItemType.ForegroundAppQueryRange).map { (it ?: "0").toLong() }
|
||||||
|
fun setForegroundAppQueryIntervalSync(interval: Long) {
|
||||||
|
if (interval < 0) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateValueSync(ConfigurationItemType.ForegroundAppQueryRange, interval.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEnableAlternativeDurationSelectionAsync() = getValueOfKeyAsync(ConfigurationItemType.EnableAlternativeDurationSelection).map { it == "1" }
|
||||||
|
fun setEnableAlternativeDurationSelectionSync(enable: Boolean) = updateValueSync(ConfigurationItemType.EnableAlternativeDurationSelection, if (enable) "1" else "0")
|
||||||
|
|
||||||
|
fun setLastScreenOnTime(time: Long) = updateValueSync(ConfigurationItemType.LastScreenOnTime, time.toString())
|
||||||
|
fun getLastScreenOnTime() = getValueOfKeySync(ConfigurationItemType.LastScreenOnTime)?.toLong() ?: 0L
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,9 @@ abstract class DeviceDao {
|
||||||
@Query("UPDATE device SET current_user_id = :userId WHERE id = :deviceId")
|
@Query("UPDATE device SET current_user_id = :userId WHERE id = :deviceId")
|
||||||
abstract fun updateDeviceUser(deviceId: String, userId: String)
|
abstract fun updateDeviceUser(deviceId: String, userId: String)
|
||||||
|
|
||||||
|
@Query("UPDATE device SET default_user = :defaultUserId WHERE id = :deviceId")
|
||||||
|
abstract fun updateDeviceDefaultUser(deviceId: String, defaultUserId: String)
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
abstract fun updateDeviceEntry(device: Device)
|
abstract fun updateDeviceEntry(device: Device)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* 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.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import io.timelimit.android.data.IdGenerator
|
||||||
|
import io.timelimit.android.data.JsonSerializable
|
||||||
|
|
||||||
|
@Entity(primaryKeys = ["device_id", "app_package_name", "activity_class_name"], tableName = "app_activity")
|
||||||
|
data class AppActivity(
|
||||||
|
@ColumnInfo(name = "device_id")
|
||||||
|
val deviceId: String,
|
||||||
|
@ColumnInfo(name = "app_package_name")
|
||||||
|
val appPackageName: String,
|
||||||
|
@ColumnInfo(name = "activity_class_name")
|
||||||
|
val activityClassName: String,
|
||||||
|
@ColumnInfo(name = "activity_title")
|
||||||
|
val title: String
|
||||||
|
): JsonSerializable {
|
||||||
|
companion object {
|
||||||
|
private const val DEVICE_ID = "deviceId"
|
||||||
|
private const val APP_PACKAGE_NAME = "app_package_name"
|
||||||
|
private const val ACTIVITY_CLASS_NAME = "activity_class_name"
|
||||||
|
private const val TITLE = "title"
|
||||||
|
|
||||||
|
fun parse(reader: JsonReader): AppActivity {
|
||||||
|
var deviceId: String? = null
|
||||||
|
var appPackageName: String? = null
|
||||||
|
var activityClassName: String? = null
|
||||||
|
var title: String? = null
|
||||||
|
|
||||||
|
reader.beginObject()
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
when (reader.nextName()) {
|
||||||
|
DEVICE_ID -> deviceId = reader.nextString()
|
||||||
|
APP_PACKAGE_NAME -> appPackageName = reader.nextString()
|
||||||
|
ACTIVITY_CLASS_NAME -> activityClassName = reader.nextString()
|
||||||
|
TITLE -> title = reader.nextString()
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
return AppActivity(
|
||||||
|
deviceId = deviceId!!,
|
||||||
|
appPackageName = appPackageName!!,
|
||||||
|
activityClassName = activityClassName!!,
|
||||||
|
title = title!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(DEVICE_ID).value(deviceId)
|
||||||
|
writer.name(APP_PACKAGE_NAME).value(appPackageName)
|
||||||
|
writer.name(ACTIVITY_CLASS_NAME).value(activityClassName)
|
||||||
|
writer.name(TITLE).value(title)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,7 +44,11 @@ data class Category(
|
||||||
@ColumnInfo(name = "temporarily_blocked")
|
@ColumnInfo(name = "temporarily_blocked")
|
||||||
val temporarilyBlocked: Boolean,
|
val temporarilyBlocked: Boolean,
|
||||||
@ColumnInfo(name = "parent_category_id")
|
@ColumnInfo(name = "parent_category_id")
|
||||||
val parentCategoryId: String
|
val parentCategoryId: String,
|
||||||
|
@ColumnInfo(name = "block_all_notifications")
|
||||||
|
val blockAllNotifications: Boolean,
|
||||||
|
@ColumnInfo(name = "time_warnings")
|
||||||
|
val timeWarnings: Int
|
||||||
): JsonSerializable {
|
): JsonSerializable {
|
||||||
companion object {
|
companion object {
|
||||||
const val MINUTES_PER_DAY = 60 * 24
|
const val MINUTES_PER_DAY = 60 * 24
|
||||||
|
@ -57,6 +61,8 @@ data class Category(
|
||||||
private const val EXTRA_TIME_IN_MILLIS = "extraTimeInMillis"
|
private const val EXTRA_TIME_IN_MILLIS = "extraTimeInMillis"
|
||||||
private const val TEMPORARILY_BLOCKED = "temporarilyBlocked"
|
private const val TEMPORARILY_BLOCKED = "temporarilyBlocked"
|
||||||
private const val PARENT_CATEGORY_ID = "parentCategoryId"
|
private const val PARENT_CATEGORY_ID = "parentCategoryId"
|
||||||
|
private const val BlOCK_ALL_NOTIFICATIONS = "blockAllNotifications"
|
||||||
|
private const val TIME_WARNINGS = "timeWarnings"
|
||||||
|
|
||||||
fun parse(reader: JsonReader): Category {
|
fun parse(reader: JsonReader): Category {
|
||||||
var id: String? = null
|
var id: String? = null
|
||||||
|
@ -67,6 +73,8 @@ data class Category(
|
||||||
var temporarilyBlocked: Boolean? = null
|
var temporarilyBlocked: Boolean? = null
|
||||||
// this field was added later so it has got a default value
|
// this field was added later so it has got a default value
|
||||||
var parentCategoryId = ""
|
var parentCategoryId = ""
|
||||||
|
var blockAllNotifications = false
|
||||||
|
var timeWarnings = 0
|
||||||
|
|
||||||
reader.beginObject()
|
reader.beginObject()
|
||||||
|
|
||||||
|
@ -79,6 +87,8 @@ data class Category(
|
||||||
EXTRA_TIME_IN_MILLIS -> extraTimeInMillis = reader.nextLong()
|
EXTRA_TIME_IN_MILLIS -> extraTimeInMillis = reader.nextLong()
|
||||||
TEMPORARILY_BLOCKED -> temporarilyBlocked = reader.nextBoolean()
|
TEMPORARILY_BLOCKED -> temporarilyBlocked = reader.nextBoolean()
|
||||||
PARENT_CATEGORY_ID -> parentCategoryId = reader.nextString()
|
PARENT_CATEGORY_ID -> parentCategoryId = reader.nextString()
|
||||||
|
BlOCK_ALL_NOTIFICATIONS -> blockAllNotifications = reader.nextBoolean()
|
||||||
|
TIME_WARNINGS -> timeWarnings = reader.nextInt()
|
||||||
else -> reader.skipValue()
|
else -> reader.skipValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,7 +102,9 @@ data class Category(
|
||||||
blockedMinutesInWeek = blockedMinutesInWeek!!,
|
blockedMinutesInWeek = blockedMinutesInWeek!!,
|
||||||
extraTimeInMillis = extraTimeInMillis!!,
|
extraTimeInMillis = extraTimeInMillis!!,
|
||||||
temporarilyBlocked = temporarilyBlocked!!,
|
temporarilyBlocked = temporarilyBlocked!!,
|
||||||
parentCategoryId = parentCategoryId
|
parentCategoryId = parentCategoryId,
|
||||||
|
blockAllNotifications = blockAllNotifications,
|
||||||
|
timeWarnings = timeWarnings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -120,7 +132,22 @@ data class Category(
|
||||||
writer.name(EXTRA_TIME_IN_MILLIS).value(extraTimeInMillis)
|
writer.name(EXTRA_TIME_IN_MILLIS).value(extraTimeInMillis)
|
||||||
writer.name(TEMPORARILY_BLOCKED).value(temporarilyBlocked)
|
writer.name(TEMPORARILY_BLOCKED).value(temporarilyBlocked)
|
||||||
writer.name(PARENT_CATEGORY_ID).value(parentCategoryId)
|
writer.name(PARENT_CATEGORY_ID).value(parentCategoryId)
|
||||||
|
writer.name(BlOCK_ALL_NOTIFICATIONS).value(blockAllNotifications)
|
||||||
|
writer.name(TIME_WARNINGS).value(timeWarnings)
|
||||||
|
|
||||||
writer.endObject()
|
writer.endObject()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object CategoryTimeWarnings {
|
||||||
|
val durationToBitIndex = mapOf(
|
||||||
|
1000L * 60 to 0, // 1 minute
|
||||||
|
1000L * 60 * 3 to 1, // 3 minutes
|
||||||
|
1000L * 60 * 5 to 2, // 5 minutes
|
||||||
|
1000L * 60 * 10 to 3, // 10 minutes
|
||||||
|
1000L * 60 * 15 to 4 // 15 minutes
|
||||||
|
)
|
||||||
|
|
||||||
|
val durations = durationToBitIndex.keys
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,17 @@ data class CategoryApp(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@delegate:Transient
|
||||||
|
val packageNameWithoutActivityName: String by lazy {
|
||||||
|
if (specifiesActivity)
|
||||||
|
packageName.substring(0, packageName.indexOf(":"))
|
||||||
|
else
|
||||||
|
packageName
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
val specifiesActivity = packageName.contains(":")
|
||||||
|
|
||||||
init {
|
init {
|
||||||
IdGenerator.assertIdValid(categoryId)
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
|
||||||
|
|
|
@ -77,30 +77,45 @@ data class ConfigurationItem(
|
||||||
enum class ConfigurationItemType {
|
enum class ConfigurationItemType {
|
||||||
OwnDeviceId,
|
OwnDeviceId,
|
||||||
ShownHints,
|
ShownHints,
|
||||||
WasDeviceLocked
|
WasDeviceLocked,
|
||||||
|
ForegroundAppQueryRange,
|
||||||
|
EnableAlternativeDurationSelection,
|
||||||
|
LastScreenOnTime
|
||||||
}
|
}
|
||||||
|
|
||||||
object ConfigurationItemTypeUtil {
|
object ConfigurationItemTypeUtil {
|
||||||
private const val OWN_DEVICE_ID = 1
|
private const val OWN_DEVICE_ID = 1
|
||||||
private const val SHOWN_HINTS = 2
|
private const val SHOWN_HINTS = 2
|
||||||
private const val WAS_DEVICE_LOCKED = 3
|
private const val WAS_DEVICE_LOCKED = 3
|
||||||
|
private const val FOREGROUND_APP_QUERY_RANGE = 4
|
||||||
|
private const val ENABLE_ALTERNATIVE_DURATION_SELECTION = 5
|
||||||
|
private const val LAST_SCREEN_ON_TIME = 6
|
||||||
|
|
||||||
val TYPES = listOf(
|
val TYPES = listOf(
|
||||||
ConfigurationItemType.OwnDeviceId,
|
ConfigurationItemType.OwnDeviceId,
|
||||||
ConfigurationItemType.ShownHints,
|
ConfigurationItemType.ShownHints,
|
||||||
ConfigurationItemType.WasDeviceLocked
|
ConfigurationItemType.WasDeviceLocked,
|
||||||
|
ConfigurationItemType.ForegroundAppQueryRange,
|
||||||
|
ConfigurationItemType.EnableAlternativeDurationSelection,
|
||||||
|
ConfigurationItemType.LastScreenOnTime
|
||||||
)
|
)
|
||||||
|
|
||||||
fun serialize(value: ConfigurationItemType) = when(value) {
|
fun serialize(value: ConfigurationItemType) = when(value) {
|
||||||
ConfigurationItemType.OwnDeviceId -> OWN_DEVICE_ID
|
ConfigurationItemType.OwnDeviceId -> OWN_DEVICE_ID
|
||||||
ConfigurationItemType.ShownHints -> SHOWN_HINTS
|
ConfigurationItemType.ShownHints -> SHOWN_HINTS
|
||||||
ConfigurationItemType.WasDeviceLocked -> WAS_DEVICE_LOCKED
|
ConfigurationItemType.WasDeviceLocked -> WAS_DEVICE_LOCKED
|
||||||
|
ConfigurationItemType.ForegroundAppQueryRange -> FOREGROUND_APP_QUERY_RANGE
|
||||||
|
ConfigurationItemType.EnableAlternativeDurationSelection -> ENABLE_ALTERNATIVE_DURATION_SELECTION
|
||||||
|
ConfigurationItemType.LastScreenOnTime -> LAST_SCREEN_ON_TIME
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parse(value: Int) = when(value) {
|
fun parse(value: Int) = when(value) {
|
||||||
OWN_DEVICE_ID -> ConfigurationItemType.OwnDeviceId
|
OWN_DEVICE_ID -> ConfigurationItemType.OwnDeviceId
|
||||||
SHOWN_HINTS -> ConfigurationItemType.ShownHints
|
SHOWN_HINTS -> ConfigurationItemType.ShownHints
|
||||||
WAS_DEVICE_LOCKED -> ConfigurationItemType.WasDeviceLocked
|
WAS_DEVICE_LOCKED -> ConfigurationItemType.WasDeviceLocked
|
||||||
|
FOREGROUND_APP_QUERY_RANGE -> ConfigurationItemType.ForegroundAppQueryRange
|
||||||
|
ENABLE_ALTERNATIVE_DURATION_SELECTION -> ConfigurationItemType.EnableAlternativeDurationSelection
|
||||||
|
LAST_SCREEN_ON_TIME -> ConfigurationItemType.LastScreenOnTime
|
||||||
else -> throw IllegalArgumentException()
|
else -> throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,4 +133,6 @@ object HintsToShow {
|
||||||
const val DEVICE_SCREEN_INTRODUCTION = 2L
|
const val DEVICE_SCREEN_INTRODUCTION = 2L
|
||||||
const val CATEGORIES_INTRODUCTION = 4L
|
const val CATEGORIES_INTRODUCTION = 4L
|
||||||
const val TIME_LIMIT_RULE_INTRODUCTION = 8L
|
const val TIME_LIMIT_RULE_INTRODUCTION = 8L
|
||||||
|
const val CONTACTS_INTRO = 16L
|
||||||
|
const val TIMELIMIT_RULE_MUSTREAD = 32L
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,8 +65,24 @@ data class Device(
|
||||||
val manipulationDidReboot: Boolean,
|
val manipulationDidReboot: Boolean,
|
||||||
@ColumnInfo(name = "had_manipulation")
|
@ColumnInfo(name = "had_manipulation")
|
||||||
val hadManipulation: Boolean,
|
val hadManipulation: Boolean,
|
||||||
|
@ColumnInfo(name = "default_user")
|
||||||
|
val defaultUser: String,
|
||||||
|
@ColumnInfo(name = "default_user_timeout")
|
||||||
|
val defaultUserTimeout: Int,
|
||||||
@ColumnInfo(name = "consider_reboot_manipulation")
|
@ColumnInfo(name = "consider_reboot_manipulation")
|
||||||
val considerRebootManipulation: Boolean
|
val considerRebootManipulation: Boolean,
|
||||||
|
@ColumnInfo(name = "current_overlay_permission")
|
||||||
|
val currentOverlayPermission: RuntimePermissionStatus,
|
||||||
|
@ColumnInfo(name = "highest_overlay_permission")
|
||||||
|
val highestOverlayPermission: RuntimePermissionStatus,
|
||||||
|
@ColumnInfo(name = "current_accessibility_service_permission")
|
||||||
|
val accessibilityServiceEnabled: Boolean,
|
||||||
|
@ColumnInfo(name = "was_accessibility_service_permission")
|
||||||
|
val wasAccessibilityServiceEnabled: Boolean,
|
||||||
|
@ColumnInfo(name = "enable_activity_level_blocking")
|
||||||
|
val enableActivityLevelBlocking: Boolean,
|
||||||
|
@ColumnInfo(name = "q_or_later")
|
||||||
|
val qOrLater: Boolean
|
||||||
): JsonSerializable {
|
): JsonSerializable {
|
||||||
companion object {
|
companion object {
|
||||||
private const val ID = "id"
|
private const val ID = "id"
|
||||||
|
@ -85,7 +101,15 @@ data class Device(
|
||||||
private const val TRIED_DISABLING_DEVICE_ADMIN = "tdda"
|
private const val TRIED_DISABLING_DEVICE_ADMIN = "tdda"
|
||||||
private const val MANIPULATION_DID_REBOOT = "mdr"
|
private const val MANIPULATION_DID_REBOOT = "mdr"
|
||||||
private const val HAD_MANIPULATION = "hm"
|
private const val HAD_MANIPULATION = "hm"
|
||||||
|
private const val DEFAULT_USER = "du"
|
||||||
|
private const val DEFAULT_USER_TIMEOUT = "dut"
|
||||||
private const val CONSIDER_REBOOT_A_MANIPULATION = "cram"
|
private const val CONSIDER_REBOOT_A_MANIPULATION = "cram"
|
||||||
|
private const val CURRENT_OVERLAY_PERMISSION = "cop"
|
||||||
|
private const val HIGHEST_OVERLAY_PERMISSION = "hop"
|
||||||
|
private const val ACCESSIBILITY_SERVICE_ENABLED = "ase"
|
||||||
|
private const val WAS_ACCESSIBILITY_SERVICE_ENABLED = "wase"
|
||||||
|
private const val ENABLE_ACTIVITY_LEVEL_BLOCKING = "ealb"
|
||||||
|
private const val Q_OR_LATER = "qol"
|
||||||
|
|
||||||
fun parse(reader: JsonReader): Device {
|
fun parse(reader: JsonReader): Device {
|
||||||
var id: String? = null
|
var id: String? = null
|
||||||
|
@ -104,7 +128,15 @@ data class Device(
|
||||||
var manipulationTriedDisablingDeviceAdmin: Boolean? = null
|
var manipulationTriedDisablingDeviceAdmin: Boolean? = null
|
||||||
var manipulationDidReboot: Boolean = false
|
var manipulationDidReboot: Boolean = false
|
||||||
var hadManipulation: Boolean? = null
|
var hadManipulation: Boolean? = null
|
||||||
|
var defaultUser = ""
|
||||||
|
var defaultUserTimeout = 0
|
||||||
var considerRebootManipulation = false
|
var considerRebootManipulation = false
|
||||||
|
var currentOverlayPermission = RuntimePermissionStatus.NotGranted
|
||||||
|
var highestOverlayPermission = RuntimePermissionStatus.NotGranted
|
||||||
|
var accessibilityServiceEnabled = false
|
||||||
|
var wasAccessibilityServiceEnabled = false
|
||||||
|
var enableActivityLevelBlocking = false
|
||||||
|
var qOrLater = false
|
||||||
|
|
||||||
reader.beginObject()
|
reader.beginObject()
|
||||||
|
|
||||||
|
@ -126,7 +158,15 @@ data class Device(
|
||||||
TRIED_DISABLING_DEVICE_ADMIN -> manipulationTriedDisablingDeviceAdmin = reader.nextBoolean()
|
TRIED_DISABLING_DEVICE_ADMIN -> manipulationTriedDisablingDeviceAdmin = reader.nextBoolean()
|
||||||
MANIPULATION_DID_REBOOT -> manipulationDidReboot = reader.nextBoolean()
|
MANIPULATION_DID_REBOOT -> manipulationDidReboot = reader.nextBoolean()
|
||||||
HAD_MANIPULATION -> hadManipulation = reader.nextBoolean()
|
HAD_MANIPULATION -> hadManipulation = reader.nextBoolean()
|
||||||
|
DEFAULT_USER -> defaultUser = reader.nextString()
|
||||||
|
DEFAULT_USER_TIMEOUT -> defaultUserTimeout = reader.nextInt()
|
||||||
CONSIDER_REBOOT_A_MANIPULATION -> considerRebootManipulation = reader.nextBoolean()
|
CONSIDER_REBOOT_A_MANIPULATION -> considerRebootManipulation = reader.nextBoolean()
|
||||||
|
CURRENT_OVERLAY_PERMISSION -> currentOverlayPermission = RuntimePermissionStatusUtil.parse(reader.nextString())
|
||||||
|
HIGHEST_OVERLAY_PERMISSION -> highestOverlayPermission = RuntimePermissionStatusUtil.parse(reader.nextString())
|
||||||
|
ACCESSIBILITY_SERVICE_ENABLED -> accessibilityServiceEnabled = reader.nextBoolean()
|
||||||
|
WAS_ACCESSIBILITY_SERVICE_ENABLED -> wasAccessibilityServiceEnabled = reader.nextBoolean()
|
||||||
|
ENABLE_ACTIVITY_LEVEL_BLOCKING -> enableActivityLevelBlocking = reader.nextBoolean()
|
||||||
|
Q_OR_LATER -> qOrLater = reader.nextBoolean()
|
||||||
else -> reader.skipValue()
|
else -> reader.skipValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,7 +190,15 @@ data class Device(
|
||||||
manipulationTriedDisablingDeviceAdmin = manipulationTriedDisablingDeviceAdmin!!,
|
manipulationTriedDisablingDeviceAdmin = manipulationTriedDisablingDeviceAdmin!!,
|
||||||
manipulationDidReboot = manipulationDidReboot,
|
manipulationDidReboot = manipulationDidReboot,
|
||||||
hadManipulation = hadManipulation!!,
|
hadManipulation = hadManipulation!!,
|
||||||
considerRebootManipulation = considerRebootManipulation
|
defaultUser = defaultUser,
|
||||||
|
defaultUserTimeout = defaultUserTimeout,
|
||||||
|
considerRebootManipulation = considerRebootManipulation,
|
||||||
|
currentOverlayPermission = currentOverlayPermission,
|
||||||
|
highestOverlayPermission = highestOverlayPermission,
|
||||||
|
accessibilityServiceEnabled = accessibilityServiceEnabled,
|
||||||
|
wasAccessibilityServiceEnabled = wasAccessibilityServiceEnabled,
|
||||||
|
enableActivityLevelBlocking = enableActivityLevelBlocking,
|
||||||
|
qOrLater = qOrLater
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,7 +246,15 @@ data class Device(
|
||||||
writer.name(TRIED_DISABLING_DEVICE_ADMIN).value(manipulationTriedDisablingDeviceAdmin)
|
writer.name(TRIED_DISABLING_DEVICE_ADMIN).value(manipulationTriedDisablingDeviceAdmin)
|
||||||
writer.name(MANIPULATION_DID_REBOOT).value(manipulationDidReboot)
|
writer.name(MANIPULATION_DID_REBOOT).value(manipulationDidReboot)
|
||||||
writer.name(HAD_MANIPULATION).value(hadManipulation)
|
writer.name(HAD_MANIPULATION).value(hadManipulation)
|
||||||
|
writer.name(DEFAULT_USER).value(defaultUser)
|
||||||
|
writer.name(DEFAULT_USER_TIMEOUT).value(defaultUserTimeout)
|
||||||
writer.name(CONSIDER_REBOOT_A_MANIPULATION).value(considerRebootManipulation)
|
writer.name(CONSIDER_REBOOT_A_MANIPULATION).value(considerRebootManipulation)
|
||||||
|
writer.name(CURRENT_OVERLAY_PERMISSION).value(RuntimePermissionStatusUtil.serialize(currentOverlayPermission))
|
||||||
|
writer.name(HIGHEST_OVERLAY_PERMISSION).value(RuntimePermissionStatusUtil.serialize(highestOverlayPermission))
|
||||||
|
writer.name(ACCESSIBILITY_SERVICE_ENABLED).value(accessibilityServiceEnabled)
|
||||||
|
writer.name(WAS_ACCESSIBILITY_SERVICE_ENABLED).value(wasAccessibilityServiceEnabled)
|
||||||
|
writer.name(ENABLE_ACTIVITY_LEVEL_BLOCKING).value(enableActivityLevelBlocking)
|
||||||
|
writer.name(Q_OR_LATER).value(qOrLater)
|
||||||
|
|
||||||
writer.endObject()
|
writer.endObject()
|
||||||
}
|
}
|
||||||
|
@ -211,6 +267,10 @@ data class Device(
|
||||||
val manipulationOfNotificationAccess = currentNotificationAccessPermission != highestNotificationAccessPermission
|
val manipulationOfNotificationAccess = currentNotificationAccessPermission != highestNotificationAccessPermission
|
||||||
@Transient
|
@Transient
|
||||||
val manipulationOfAppVersion = currentAppVersion != highestAppVersion
|
val manipulationOfAppVersion = currentAppVersion != highestAppVersion
|
||||||
|
@Transient
|
||||||
|
val manipulationOfOverlayPermission = currentOverlayPermission != highestOverlayPermission
|
||||||
|
@Transient
|
||||||
|
val manipulationOfAccessibilityService = accessibilityServiceEnabled != wasAccessibilityServiceEnabled
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
val hasActiveManipulationWarning = manipulationOfProtectionLevel ||
|
val hasActiveManipulationWarning = manipulationOfProtectionLevel ||
|
||||||
|
@ -218,8 +278,16 @@ data class Device(
|
||||||
manipulationOfNotificationAccess ||
|
manipulationOfNotificationAccess ||
|
||||||
manipulationOfAppVersion ||
|
manipulationOfAppVersion ||
|
||||||
manipulationTriedDisablingDeviceAdmin ||
|
manipulationTriedDisablingDeviceAdmin ||
|
||||||
manipulationDidReboot
|
manipulationDidReboot ||
|
||||||
|
manipulationOfOverlayPermission ||
|
||||||
|
manipulationOfAccessibilityService
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
val hasAnyManipulation = hasActiveManipulationWarning || hadManipulation
|
val hasAnyManipulation = hasActiveManipulationWarning || hadManipulation
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
val missingPermissionAtQOrLater = qOrLater &&
|
||||||
|
(!accessibilityServiceEnabled) &&
|
||||||
|
(currentOverlayPermission != RuntimePermissionStatus.Granted) &&
|
||||||
|
(currentProtectionLevel != ProtectionLevel.DeviceOwner)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.extensions
|
package io.timelimit.android.extensions
|
||||||
|
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
|
@ -40,3 +42,19 @@ fun EditText.setOnEnterListenr(listener: () -> Unit) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun EditText.addOnTextChangedListener(listener: () -> Unit) {
|
||||||
|
this.addTextChangedListener(object: TextWatcher {
|
||||||
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||||
|
listener()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -19,29 +19,37 @@ import android.graphics.drawable.Drawable
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import io.timelimit.android.data.model.App
|
import io.timelimit.android.data.model.App
|
||||||
|
import io.timelimit.android.data.model.AppActivity
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
abstract class PlatformIntegration(
|
abstract class PlatformIntegration(
|
||||||
val maximumProtectionLevel: ProtectionLevel
|
val maximumProtectionLevel: ProtectionLevel
|
||||||
) {
|
) {
|
||||||
abstract fun getLocalApps(): Collection<App>
|
abstract fun getLocalApps(): Collection<App>
|
||||||
|
abstract fun getLocalAppActivities(deviceId: String): Collection<AppActivity>
|
||||||
abstract fun getLocalAppTitle(packageName: String): String?
|
abstract fun getLocalAppTitle(packageName: String): String?
|
||||||
abstract fun getAppIcon(packageName: String): Drawable?
|
abstract fun getAppIcon(packageName: String): Drawable?
|
||||||
|
abstract fun getLauncherAppPackageName(): String?
|
||||||
abstract fun getCurrentProtectionLevel(): ProtectionLevel
|
abstract fun getCurrentProtectionLevel(): ProtectionLevel
|
||||||
abstract fun getForegroundAppPermissionStatus(): RuntimePermissionStatus
|
abstract fun getForegroundAppPermissionStatus(): RuntimePermissionStatus
|
||||||
abstract fun getDrawOverOtherAppsPermissionStatus(): RuntimePermissionStatus
|
abstract fun getDrawOverOtherAppsPermissionStatus(): RuntimePermissionStatus
|
||||||
abstract fun getNotificationAccessPermissionStatus(): NewPermissionStatus
|
abstract fun getNotificationAccessPermissionStatus(): NewPermissionStatus
|
||||||
|
abstract fun getOverlayPermissionStatus(): RuntimePermissionStatus
|
||||||
|
abstract fun isAccessibilityServiceEnabled(): Boolean
|
||||||
abstract fun disableDeviceAdmin()
|
abstract fun disableDeviceAdmin()
|
||||||
abstract fun trySetLockScreenPassword(password: String): Boolean
|
abstract fun trySetLockScreenPassword(password: String): Boolean
|
||||||
// this must have a fallback if the permission is not granted
|
// this must have a fallback if the permission is not granted
|
||||||
abstract fun showOverlayMessage(text: String)
|
abstract fun showOverlayMessage(text: String)
|
||||||
|
|
||||||
abstract fun showAppLockScreen(currentPackageName: String)
|
abstract fun showAppLockScreen(currentPackageName: String, currentActivityName: String?)
|
||||||
|
abstract fun muteAudioIfPossible(packageName: String)
|
||||||
|
abstract fun setShowBlockingOverlay(show: Boolean)
|
||||||
// this should throw an SecurityException if the permission is missing
|
// this should throw an SecurityException if the permission is missing
|
||||||
abstract suspend fun getForegroundAppPackageName(): String?
|
abstract suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long)
|
||||||
abstract fun setAppStatusMessage(message: AppStatusMessage?)
|
abstract fun setAppStatusMessage(message: AppStatusMessage?)
|
||||||
abstract fun isScreenOn(): Boolean
|
abstract fun isScreenOn(): Boolean
|
||||||
abstract fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean)
|
abstract fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean)
|
||||||
|
abstract fun showTimeWarningNotification(title: String, text: String)
|
||||||
// returns package names for which it was set
|
// returns package names for which it was set
|
||||||
abstract fun setSuspendedApps(packageNames: List<String>, suspend: Boolean): List<String>
|
abstract fun setSuspendedApps(packageNames: List<String>, suspend: Boolean): List<String>
|
||||||
abstract fun stopSuspendingForAllApps()
|
abstract fun stopSuspendingForAllApps()
|
||||||
|
@ -54,6 +62,12 @@ abstract class PlatformIntegration(
|
||||||
var installedAppsChangeListener: Runnable? = null
|
var installedAppsChangeListener: Runnable? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class ForegroundAppSpec(var packageName: String?, var activityName: String?) {
|
||||||
|
companion object {
|
||||||
|
fun newInstance() = ForegroundAppSpec(packageName = null, activityName = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum class ProtectionLevel {
|
enum class ProtectionLevel {
|
||||||
None, SimpleDeviceAdmin, PasswordDeviceAdmin, DeviceOwner
|
None, SimpleDeviceAdmin, PasswordDeviceAdmin, DeviceOwner
|
||||||
}
|
}
|
||||||
|
@ -170,4 +184,9 @@ class NewPermissionStatusConverter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AppStatusMessage(val title: String, val text: String): Parcelable
|
data class AppStatusMessage(
|
||||||
|
val title: String,
|
||||||
|
val text: String,
|
||||||
|
val subtext: String? = null,
|
||||||
|
val showSwitchToDefaultUserOption: Boolean = false
|
||||||
|
): Parcelable
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* 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.integration.platform.android
|
||||||
|
|
||||||
|
import android.accessibilityservice.AccessibilityService
|
||||||
|
import android.view.accessibility.AccessibilityEvent
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
|
||||||
|
class AccessibilityService: AccessibilityService() {
|
||||||
|
companion object {
|
||||||
|
var instance: io.timelimit.android.integration.platform.android.AccessibilityService? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceConnected() {
|
||||||
|
super.onServiceConnected()
|
||||||
|
|
||||||
|
instance = this
|
||||||
|
DefaultAppLogic.with(this) // init
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
|
||||||
|
instance = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInterrupt() {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showHomescreen() {
|
||||||
|
performGlobalAction(GLOBAL_ACTION_HOME)
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,15 +41,12 @@ class AdminReceiver: DeviceAdminReceiver() {
|
||||||
|
|
||||||
override fun onDisableRequested(context: Context, intent: Intent?): CharSequence {
|
override fun onDisableRequested(context: Context, intent: Intent?): CharSequence {
|
||||||
runAsync {
|
runAsync {
|
||||||
val logic = DefaultAppLogic.with(context)
|
|
||||||
|
|
||||||
if (logic.database.config().getOwnDeviceId().waitForNullableValue() != null) {
|
|
||||||
ApplyActionUtil.applyAppLogicAction(
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
TriedDisablingDeviceAdminAction,
|
action = TriedDisablingDeviceAdminAction,
|
||||||
logic
|
appLogic = DefaultAppLogic.with(context),
|
||||||
|
ignoreIfDeviceIsNotConfigured = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return context.getString(R.string.admin_disable_warning)
|
return context.getString(R.string.admin_disable_warning)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ package io.timelimit.android.integration.platform.android
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
|
import android.app.Application
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.admin.DevicePolicyManager
|
import android.app.admin.DevicePolicyManager
|
||||||
|
@ -27,20 +28,27 @@ import android.content.Intent
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.media.session.MediaSessionManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.os.UserManager
|
import android.os.UserManager
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.coroutines.runAsyncExpectForever
|
||||||
import io.timelimit.android.data.model.App
|
import io.timelimit.android.data.model.App
|
||||||
|
import io.timelimit.android.data.model.AppActivity
|
||||||
import io.timelimit.android.integration.platform.*
|
import io.timelimit.android.integration.platform.*
|
||||||
import io.timelimit.android.integration.platform.android.foregroundapp.ForegroundAppHelper
|
import io.timelimit.android.integration.platform.android.foregroundapp.ForegroundAppHelper
|
||||||
import io.timelimit.android.ui.lock.LockActivity
|
import io.timelimit.android.ui.lock.LockActivity
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.channels.consumeEach
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
|
||||||
class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectionLevel) {
|
class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectionLevel) {
|
||||||
|
@ -65,6 +73,7 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
||||||
private val activityManager = this.context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
private val activityManager = this.context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||||
private val notificationManager = this.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
private val notificationManager = this.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
private val deviceAdmin = ComponentName(context.applicationContext, AdminReceiver::class.java)
|
private val deviceAdmin = ComponentName(context.applicationContext, AdminReceiver::class.java)
|
||||||
|
private val overlay = OverlayUtil(context as Application)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
AppsChangeListener.registerBroadcastReceiver(this.context, object : BroadcastReceiver() {
|
AppsChangeListener.registerBroadcastReceiver(this.context, object : BroadcastReceiver() {
|
||||||
|
@ -78,10 +87,20 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
||||||
return AndroidIntegrationApps.getLocalApps(context)
|
return AndroidIntegrationApps.getLocalApps(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLocalAppActivities(deviceId: String): Collection<AppActivity> {
|
||||||
|
return AndroidIntegrationApps.getLocalAppActivities(deviceId, context)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getLocalAppTitle(packageName: String): String? {
|
override fun getLocalAppTitle(packageName: String): String? {
|
||||||
return AndroidIntegrationApps.getAppTitle(packageName, context)
|
return AndroidIntegrationApps.getAppTitle(packageName, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLauncherAppPackageName(): String? {
|
||||||
|
return Intent(Intent.ACTION_MAIN)
|
||||||
|
.addCategory(Intent.CATEGORY_HOME)
|
||||||
|
.resolveActivity(context.packageManager)?.packageName
|
||||||
|
}
|
||||||
|
|
||||||
override fun getAppIcon(packageName: String): Drawable? {
|
override fun getAppIcon(packageName: String): Drawable? {
|
||||||
return AndroidIntegrationApps.getAppIcon(packageName, context)
|
return AndroidIntegrationApps.getAppIcon(packageName, context)
|
||||||
}
|
}
|
||||||
|
@ -90,8 +109,8 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
||||||
return AdminStatus.getAdminStatus(context, policyManager)
|
return AdminStatus.getAdminStatus(context, policyManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getForegroundAppPackageName(): String? {
|
override suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long) {
|
||||||
return foregroundAppHelper.getForegroundAppPackage()
|
foregroundAppHelper.getForegroundApp(result, queryInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getForegroundAppPermissionStatus(): RuntimePermissionStatus {
|
override fun getForegroundAppPermissionStatus(): RuntimePermissionStatus {
|
||||||
|
@ -128,6 +147,30 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getOverlayPermissionStatus(): RuntimePermissionStatus = overlay.getOverlayPermissionStatus()
|
||||||
|
|
||||||
|
override fun isAccessibilityServiceEnabled(): Boolean {
|
||||||
|
val service = context.packageName + "/" + AccessibilityService::class.java.canonicalName
|
||||||
|
|
||||||
|
val accessibilityEnabled = try {
|
||||||
|
Settings.Secure.getInt(context.contentResolver, Settings.Secure.ACCESSIBILITY_ENABLED)
|
||||||
|
} catch (ex: Settings.SettingNotFoundException) {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessibilityEnabled == 1) {
|
||||||
|
val enabledServicesString = Settings.Secure.getString(context.contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)
|
||||||
|
|
||||||
|
if (!enabledServicesString.isNullOrEmpty()) {
|
||||||
|
if (enabledServicesString.split(":").contains(service)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
override fun trySetLockScreenPassword(password: String): Boolean {
|
override fun trySetLockScreenPassword(password: String): Boolean {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Log.d(LOG_TAG, "set password")
|
Log.d(LOG_TAG, "set password")
|
||||||
|
@ -153,17 +196,55 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
||||||
}
|
}
|
||||||
|
|
||||||
private var lastAppStatusMessage: AppStatusMessage? = null
|
private var lastAppStatusMessage: AppStatusMessage? = null
|
||||||
|
private var appStatusMessageChannel = Channel<AppStatusMessage?>(capacity = Channel.CONFLATED)
|
||||||
|
|
||||||
override fun setAppStatusMessage(message: AppStatusMessage?) {
|
override fun setAppStatusMessage(message: AppStatusMessage?) {
|
||||||
if (lastAppStatusMessage != message) {
|
if (lastAppStatusMessage != message) {
|
||||||
lastAppStatusMessage = message
|
lastAppStatusMessage = message
|
||||||
|
appStatusMessageChannel.offer(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
runAsyncExpectForever {
|
||||||
|
appStatusMessageChannel.consumeEach { message ->
|
||||||
BackgroundService.setStatusMessage(message, context)
|
BackgroundService.setStatusMessage(message, context)
|
||||||
|
|
||||||
|
delay(200)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showAppLockScreen(currentPackageName: String) {
|
override fun showAppLockScreen(currentPackageName: String, currentActivityName: String?) {
|
||||||
LockActivity.start(context, currentPackageName)
|
LockActivity.start(context, currentPackageName, currentActivityName)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun muteAudioIfPossible(packageName: String) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
if (getNotificationAccessPermissionStatus() == NewPermissionStatus.Granted) {
|
||||||
|
val manager = context.getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager
|
||||||
|
val sessions = manager.getActiveSessions(ComponentName(context, NotificationListener::class.java))
|
||||||
|
val sessionsOfTheApp = sessions.filter { it.packageName == packageName }
|
||||||
|
sessionsOfTheApp.forEach { session ->
|
||||||
|
session.dispatchMediaButtonEvent(KeyEvent(
|
||||||
|
KeyEvent.ACTION_DOWN,
|
||||||
|
KeyEvent.KEYCODE_MEDIA_STOP
|
||||||
|
))
|
||||||
|
session.dispatchMediaButtonEvent(KeyEvent(
|
||||||
|
KeyEvent.ACTION_UP,
|
||||||
|
KeyEvent.KEYCODE_MEDIA_STOP
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setShowBlockingOverlay(show: Boolean) {
|
||||||
|
if (show) {
|
||||||
|
overlay.show()
|
||||||
|
} else {
|
||||||
|
overlay.hide()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isScreenOn(): Boolean {
|
override fun isScreenOn(): Boolean {
|
||||||
|
@ -176,7 +257,7 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
||||||
|
|
||||||
override fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean) {
|
override fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean) {
|
||||||
if (show) {
|
if (show) {
|
||||||
NotificationChannels.createAppStatusChannel(notificationManager, context)
|
NotificationChannels.createNotificationChannels(notificationManager, context)
|
||||||
|
|
||||||
val actionIntent = PendingIntent.getService(
|
val actionIntent = PendingIntent.getService(
|
||||||
context,
|
context,
|
||||||
|
@ -206,6 +287,25 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun showTimeWarningNotification(title: String, text: String) {
|
||||||
|
NotificationChannels.createNotificationChannels(notificationManager, context)
|
||||||
|
|
||||||
|
notificationManager.notify(
|
||||||
|
NotificationIds.TIME_WARNING,
|
||||||
|
NotificationCompat.Builder(context, NotificationChannels.TIME_WARNING)
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_timelapse)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(text)
|
||||||
|
.setWhen(System.currentTimeMillis())
|
||||||
|
.setShowWhen(true)
|
||||||
|
.setLocalOnly(true)
|
||||||
|
.setAutoCancel(false)
|
||||||
|
.setOngoing(false)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun disableDeviceAdmin() {
|
override fun disableDeviceAdmin() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
if (policyManager.isDeviceOwnerApp(context.packageName)) {
|
if (policyManager.isDeviceOwnerApp(context.packageName)) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import android.provider.ContactsContract
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.provider.Telephony
|
import android.provider.Telephony
|
||||||
import io.timelimit.android.data.model.App
|
import io.timelimit.android.data.model.App
|
||||||
|
import io.timelimit.android.data.model.AppActivity
|
||||||
import io.timelimit.android.data.model.AppRecommendation
|
import io.timelimit.android.data.model.AppRecommendation
|
||||||
|
|
||||||
object AndroidIntegrationApps {
|
object AndroidIntegrationApps {
|
||||||
|
@ -90,6 +91,26 @@ object AndroidIntegrationApps {
|
||||||
return result.values
|
return result.values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getLocalAppActivities(deviceId: String, context: Context): Collection<AppActivity> {
|
||||||
|
return context.packageManager.getInstalledApplications(0).asSequence().map { applicationInfo ->
|
||||||
|
(
|
||||||
|
try {
|
||||||
|
context.packageManager.getPackageInfo(applicationInfo.packageName, PackageManager.GET_ACTIVITIES)?.activities
|
||||||
|
} catch (ex: PackageManager.NameNotFoundException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
?: emptyArray()
|
||||||
|
).map {
|
||||||
|
AppActivity(
|
||||||
|
deviceId = deviceId,
|
||||||
|
appPackageName = applicationInfo.packageName,
|
||||||
|
activityClassName = it.name,
|
||||||
|
title = it.loadLabel(context.packageManager).toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.flatten().toSet()
|
||||||
|
}
|
||||||
|
|
||||||
private fun add(map: MutableMap<String, App>, resolveInfoList: List<ResolveInfo>, recommendation: AppRecommendation, context: Context) {
|
private fun add(map: MutableMap<String, App>, resolveInfoList: List<ResolveInfo>, recommendation: AppRecommendation, context: Context) {
|
||||||
val packageManager = context.packageManager
|
val packageManager = context.packageManager
|
||||||
|
|
||||||
|
|
|
@ -21,12 +21,15 @@ import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.coroutines.runAsync
|
import io.timelimit.android.coroutines.runAsync
|
||||||
import io.timelimit.android.integration.platform.AppStatusMessage
|
import io.timelimit.android.integration.platform.AppStatusMessage
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.sync.actions.SignOutAtDeviceAction
|
||||||
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
import io.timelimit.android.ui.MainActivity
|
import io.timelimit.android.ui.MainActivity
|
||||||
|
|
||||||
class BackgroundService: Service() {
|
class BackgroundService: Service() {
|
||||||
|
@ -34,6 +37,7 @@ class BackgroundService: Service() {
|
||||||
private const val ACTION = "action"
|
private const val ACTION = "action"
|
||||||
private const val ACTION_SET_NOTIFICATION = "set_notification"
|
private const val ACTION_SET_NOTIFICATION = "set_notification"
|
||||||
private const val ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS = "revoke_temporarily_allowed_apps"
|
private const val ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS = "revoke_temporarily_allowed_apps"
|
||||||
|
private const val ACTION_SWITCH_TO_DEFAULT_USER = "switch_to_default_user"
|
||||||
private const val EXTRA_NOTIFICATION = "notification"
|
private const val EXTRA_NOTIFICATION = "notification"
|
||||||
|
|
||||||
fun setStatusMessage(status: AppStatusMessage?, context: Context) {
|
fun setStatusMessage(status: AppStatusMessage?, context: Context) {
|
||||||
|
@ -53,6 +57,16 @@ class BackgroundService: Service() {
|
||||||
|
|
||||||
fun prepareRevokeTemporarilyAllowed(context: Context) = Intent(context, BackgroundService::class.java)
|
fun prepareRevokeTemporarilyAllowed(context: Context) = Intent(context, BackgroundService::class.java)
|
||||||
.putExtra(ACTION, ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS)
|
.putExtra(ACTION, ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS)
|
||||||
|
|
||||||
|
fun prepareSwitchToDefaultUser(context: Context) = Intent(context, BackgroundService::class.java)
|
||||||
|
.putExtra(ACTION, ACTION_SWITCH_TO_DEFAULT_USER)
|
||||||
|
|
||||||
|
fun getOpenAppIntent(context: Context) = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
PendingIntentIds.OPEN_MAIN_APP,
|
||||||
|
Intent(context, MainActivity::class.java),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val notificationManager: NotificationManager by lazy {
|
private val notificationManager: NotificationManager by lazy {
|
||||||
|
@ -68,7 +82,7 @@ class BackgroundService: Service() {
|
||||||
DefaultAppLogic.with(this)
|
DefaultAppLogic.with(this)
|
||||||
|
|
||||||
// create the channel
|
// create the channel
|
||||||
NotificationChannels.createAppStatusChannel(notificationManager, this)
|
NotificationChannels.createNotificationChannels(notificationManager, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
@ -78,18 +92,12 @@ class BackgroundService: Service() {
|
||||||
if (action == ACTION_SET_NOTIFICATION) {
|
if (action == ACTION_SET_NOTIFICATION) {
|
||||||
val appStatusMessage = intent.getParcelableExtra<AppStatusMessage>(EXTRA_NOTIFICATION)
|
val appStatusMessage = intent.getParcelableExtra<AppStatusMessage>(EXTRA_NOTIFICATION)
|
||||||
|
|
||||||
val openAppIntent = PendingIntent.getActivity(
|
|
||||||
this,
|
|
||||||
PendingIntentIds.OPEN_MAIN_APP,
|
|
||||||
Intent(this, MainActivity::class.java),
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
)
|
|
||||||
|
|
||||||
val notification = NotificationCompat.Builder(this, NotificationChannels.APP_STATUS)
|
val notification = NotificationCompat.Builder(this, NotificationChannels.APP_STATUS)
|
||||||
.setSmallIcon(R.drawable.ic_stat_timelapse)
|
.setSmallIcon(R.drawable.ic_stat_timelapse)
|
||||||
.setContentTitle(appStatusMessage.title)
|
.setContentTitle(appStatusMessage.title)
|
||||||
.setContentText(appStatusMessage.text)
|
.setContentText(appStatusMessage.text)
|
||||||
.setContentIntent(openAppIntent)
|
.setSubText(appStatusMessage.subtext)
|
||||||
|
.setContentIntent(getOpenAppIntent(this@BackgroundService))
|
||||||
.setWhen(0)
|
.setWhen(0)
|
||||||
.setShowWhen(false)
|
.setShowWhen(false)
|
||||||
.setSound(null)
|
.setSound(null)
|
||||||
|
@ -98,6 +106,24 @@ class BackgroundService: Service() {
|
||||||
.setAutoCancel(false)
|
.setAutoCancel(false)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.let { builder ->
|
||||||
|
if (appStatusMessage.showSwitchToDefaultUserOption) {
|
||||||
|
builder.addAction(
|
||||||
|
NotificationCompat.Action.Builder(
|
||||||
|
R.drawable.ic_account_circle_black_24dp,
|
||||||
|
getString(R.string.manage_device_default_user_switch_btn),
|
||||||
|
PendingIntent.getService(
|
||||||
|
this@BackgroundService,
|
||||||
|
PendingIntentIds.SWITCH_TO_DEFAULT_USER,
|
||||||
|
prepareSwitchToDefaultUser(this@BackgroundService),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
}
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
if (didPostNotification) {
|
if (didPostNotification) {
|
||||||
|
@ -110,6 +136,16 @@ class BackgroundService: Service() {
|
||||||
runAsync {
|
runAsync {
|
||||||
DefaultAppLogic.with(this@BackgroundService).backgroundTaskLogic.resetTemporarilyAllowedApps()
|
DefaultAppLogic.with(this@BackgroundService).backgroundTaskLogic.resetTemporarilyAllowedApps()
|
||||||
}
|
}
|
||||||
|
} else if (action == ACTION_SWITCH_TO_DEFAULT_USER) {
|
||||||
|
runAsync {
|
||||||
|
val logic = DefaultAppLogic.with(this@BackgroundService)
|
||||||
|
|
||||||
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
|
appLogic = logic,
|
||||||
|
action = SignOutAtDeviceAction,
|
||||||
|
ignoreIfDeviceIsNotConfigured = true
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,14 +25,15 @@ object NotificationIds {
|
||||||
const val APP_STATUS = 1
|
const val APP_STATUS = 1
|
||||||
const val NOTIFICATION_BLOCKED = 2
|
const val NOTIFICATION_BLOCKED = 2
|
||||||
const val REVOKE_TEMPORARILY_ALLOWED_APPS = 3
|
const val REVOKE_TEMPORARILY_ALLOWED_APPS = 3
|
||||||
const val APP_RESET = 4
|
const val TIME_WARNING = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
object NotificationChannels {
|
object NotificationChannels {
|
||||||
const val APP_STATUS = "app status"
|
const val APP_STATUS = "app status"
|
||||||
const val BLOCKED_NOTIFICATIONS_NOTIFICATION = "notification blocked notification"
|
const val BLOCKED_NOTIFICATIONS_NOTIFICATION = "notification blocked notification"
|
||||||
|
const val TIME_WARNING = "time warning"
|
||||||
|
|
||||||
fun createAppStatusChannel(notificationManager: NotificationManager, context: Context) {
|
private fun createAppStatusChannel(notificationManager: NotificationManager, context: Context) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
notificationManager.createNotificationChannel(
|
notificationManager.createNotificationChannel(
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
|
@ -50,7 +51,7 @@ object NotificationChannels {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createBlockedNotificationChannel(notificationManager: NotificationManager, context: Context) {
|
private fun createBlockedNotificationChannel(notificationManager: NotificationManager, context: Context) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
notificationManager.createNotificationChannel(
|
notificationManager.createNotificationChannel(
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
|
@ -63,9 +64,31 @@ object NotificationChannels {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createTimeWarningsNotificationChannel(notificationManager: NotificationManager, context: Context) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
notificationManager.createNotificationChannel(
|
||||||
|
NotificationChannel(
|
||||||
|
NotificationChannels.TIME_WARNING,
|
||||||
|
context.getString(R.string.notification_channel_time_warning_title),
|
||||||
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
|
).apply {
|
||||||
|
description = context.getString(R.string.notification_channel_time_warning_text)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createNotificationChannels(notificationManager: NotificationManager, context: Context) {
|
||||||
|
createAppStatusChannel(notificationManager, context)
|
||||||
|
createBlockedNotificationChannel(notificationManager, context)
|
||||||
|
createTimeWarningsNotificationChannel(notificationManager, context)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object PendingIntentIds {
|
object PendingIntentIds {
|
||||||
const val OPEN_MAIN_APP = 1
|
const val OPEN_MAIN_APP = 1
|
||||||
const val REVOKE_TEMPORARILY_ALLOWED = 2
|
const val REVOKE_TEMPORARILY_ALLOWED = 2
|
||||||
|
const val SWITCH_TO_DEFAULT_USER = 3
|
||||||
|
val DYNAMIC_NOTIFICATION_RANGE = 100..10000
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,17 +35,19 @@ import io.timelimit.android.logic.*
|
||||||
class NotificationListener: NotificationListenerService() {
|
class NotificationListener: NotificationListenerService() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val LOG_TAG = "NotificationListenerLog"
|
private const val LOG_TAG = "NotificationListenerLog"
|
||||||
|
private val SUPPORTS_HIDING_ONGOING_NOTIFICATIONS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||||
}
|
}
|
||||||
|
|
||||||
private val appLogic: AppLogic by lazy { DefaultAppLogic.with(this) }
|
private val appLogic: AppLogic by lazy { DefaultAppLogic.with(this) }
|
||||||
private val blockingReasonUtil: BlockingReasonUtil by lazy { BlockingReasonUtil(appLogic) }
|
private val blockingReasonUtil: BlockingReasonUtil by lazy { BlockingReasonUtil(appLogic) }
|
||||||
private val notificationManager: NotificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
|
private val notificationManager: NotificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
|
||||||
private val queryAppTitleCache: QueryAppTitleCache by lazy { QueryAppTitleCache(appLogic.platformIntegration) }
|
private val queryAppTitleCache: QueryAppTitleCache by lazy { QueryAppTitleCache(appLogic.platformIntegration) }
|
||||||
|
private val lastOngoingNotificationHidden = mutableSetOf<String>()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
NotificationChannels.createBlockedNotificationChannel(notificationManager, this)
|
NotificationChannels.createNotificationChannels(notificationManager, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
||||||
|
@ -58,9 +60,25 @@ class NotificationListener: NotificationListenerService() {
|
||||||
runAsync {
|
runAsync {
|
||||||
val reason = shouldRemoveNotification(sbn)
|
val reason = shouldRemoveNotification(sbn)
|
||||||
|
|
||||||
if (reason != BlockingReason.None) {
|
if (reason == BlockingReason.None) {
|
||||||
|
if (sbn.isOngoing) {
|
||||||
|
lastOngoingNotificationHidden.remove(sbn.packageName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
appLogic.platformIntegration.muteAudioIfPossible(sbn.packageName)
|
||||||
|
|
||||||
val success = try {
|
val success = try {
|
||||||
|
if (sbn.isOngoing && SUPPORTS_HIDING_ONGOING_NOTIFICATIONS) {
|
||||||
|
// only snooze for 5 seconds to show it again soon
|
||||||
|
snoozeNotification(sbn.key, 5000)
|
||||||
|
|
||||||
|
if (!lastOngoingNotificationHidden.add(sbn.packageName)) {
|
||||||
|
// skip showing again a notification that it was blocked
|
||||||
|
return@runAsync
|
||||||
|
}
|
||||||
|
} else {
|
||||||
cancelNotification(sbn.key)
|
cancelNotification(sbn.key)
|
||||||
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
} catch (ex: SecurityException) {
|
} catch (ex: SecurityException) {
|
||||||
|
@ -91,6 +109,7 @@ class NotificationListener: NotificationListenerService() {
|
||||||
BlockingReason.TimeOver -> getString(R.string.lock_reason_short_time_over)
|
BlockingReason.TimeOver -> getString(R.string.lock_reason_short_time_over)
|
||||||
BlockingReason.TimeOverExtraTimeCanBeUsedLater -> getString(R.string.lock_reason_short_time_over)
|
BlockingReason.TimeOverExtraTimeCanBeUsedLater -> getString(R.string.lock_reason_short_time_over)
|
||||||
BlockingReason.BlockedAtThisTime -> getString(R.string.lock_reason_short_blocked_time_area)
|
BlockingReason.BlockedAtThisTime -> getString(R.string.lock_reason_short_blocked_time_area)
|
||||||
|
BlockingReason.NotificationsAreBlocked -> getString(R.string.lock_reason_short_notification_blocking)
|
||||||
BlockingReason.None -> throw IllegalStateException()
|
BlockingReason.None -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -109,25 +128,41 @@ class NotificationListener: NotificationListenerService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun shouldRemoveNotification(sbn: StatusBarNotification): BlockingReason {
|
private suspend fun shouldRemoveNotification(sbn: StatusBarNotification): BlockingReason {
|
||||||
if (sbn.packageName == packageName || sbn.isOngoing) {
|
if (sbn.packageName == packageName) {
|
||||||
return BlockingReason.None
|
return BlockingReason.None
|
||||||
}
|
}
|
||||||
|
|
||||||
val blockingReason = blockingReasonUtil.getBlockingReason(sbn.packageName).waitForNonNullValue()
|
if (sbn.isOngoing && (!SUPPORTS_HIDING_ONGOING_NOTIFICATIONS)) {
|
||||||
|
|
||||||
if (blockingReason == BlockingReason.None) {
|
|
||||||
return BlockingReason.None
|
return BlockingReason.None
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSystemApp(sbn.packageName) && blockingReason == BlockingReason.NotPartOfAnCategory) {
|
val blockingReason = blockingReasonUtil.getBlockingReason(
|
||||||
|
packageName = sbn.packageName,
|
||||||
|
activityName = null
|
||||||
|
).waitForNonNullValue()
|
||||||
|
|
||||||
|
if (blockingReason.areNotificationsBlocked) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "blocking notification of ${sbn.packageName} because notifications are blocked")
|
||||||
|
}
|
||||||
|
|
||||||
|
return BlockingReason.NotificationsAreBlocked
|
||||||
|
}
|
||||||
|
|
||||||
|
return when (blockingReason) {
|
||||||
|
is NoBlockingReason -> BlockingReason.None
|
||||||
|
is BlockedReasonDetails -> {
|
||||||
|
if (isSystemApp(sbn.packageName) && blockingReason.reason == BlockingReason.NotPartOfAnCategory) {
|
||||||
return BlockingReason.None
|
return BlockingReason.None
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Log.d(LOG_TAG, "blocking notification of ${sbn.packageName} because $blockingReason")
|
Log.d(LOG_TAG, "blocking notification of ${sbn.packageName} because ${blockingReason.reason}")
|
||||||
}
|
}
|
||||||
|
|
||||||
return blockingReason
|
return blockingReason.reason
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isSystemApp(packageName: String): Boolean {
|
private fun isSystemApp(packageName: String): Boolean {
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* 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.integration.platform.android
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.graphics.PixelFormat
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.databinding.BlockingOverlayBinding
|
||||||
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
|
|
||||||
|
class OverlayUtil(private var application: Application) {
|
||||||
|
private val windowManager = application.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||||
|
private var currentView: BlockingOverlayBinding? = null
|
||||||
|
|
||||||
|
fun show() {
|
||||||
|
if (currentView != null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getOverlayPermissionStatus() == RuntimePermissionStatus.NotGranted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val view = BlockingOverlayBinding.inflate(LayoutInflater.from(application))
|
||||||
|
|
||||||
|
val params = WindowManager.LayoutParams(
|
||||||
|
WindowManager.LayoutParams.MATCH_PARENT,
|
||||||
|
WindowManager.LayoutParams.MATCH_PARENT,
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||||
|
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
||||||
|
else
|
||||||
|
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
|
||||||
|
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||||
|
PixelFormat.TRANSLUCENT
|
||||||
|
)
|
||||||
|
|
||||||
|
windowManager.addView(view.root, params)
|
||||||
|
currentView = view
|
||||||
|
|
||||||
|
Threads.mainThreadHandler.postDelayed({
|
||||||
|
view.showWarningMessage = true
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hide() {
|
||||||
|
if (currentView == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
windowManager.removeView(currentView!!.root)
|
||||||
|
currentView = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isOverlayShown() = currentView?.root?.isShown ?: false
|
||||||
|
|
||||||
|
fun getOverlayPermissionStatus() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||||
|
if (Settings.canDrawOverlays(application))
|
||||||
|
RuntimePermissionStatus.Granted
|
||||||
|
else
|
||||||
|
RuntimePermissionStatus.NotGranted
|
||||||
|
else
|
||||||
|
RuntimePermissionStatus.NotRequired
|
||||||
|
}
|
|
@ -17,16 +17,21 @@ package io.timelimit.android.integration.platform.android.foregroundapp
|
||||||
|
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import io.timelimit.android.integration.platform.ForegroundAppSpec
|
||||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
|
|
||||||
class CompatForegroundAppHelper(context: Context) : ForegroundAppHelper() {
|
class CompatForegroundAppHelper(context: Context) : ForegroundAppHelper() {
|
||||||
private val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
private val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||||
|
|
||||||
override suspend fun getForegroundAppPackage(): String? {
|
override suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long) {
|
||||||
return try {
|
try {
|
||||||
activityManager.getRunningTasks(1)[0].topActivity.packageName
|
val activity = activityManager.getRunningTasks(1)[0].topActivity
|
||||||
|
|
||||||
|
result.packageName = activity.packageName
|
||||||
|
result.activityName = activity.className
|
||||||
} catch (ex: NullPointerException) {
|
} catch (ex: NullPointerException) {
|
||||||
null
|
result.activityName = null
|
||||||
|
result.packageName = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,11 @@ package io.timelimit.android.integration.platform.android.foregroundapp
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import io.timelimit.android.integration.platform.ForegroundAppSpec
|
||||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
|
|
||||||
abstract class ForegroundAppHelper {
|
abstract class ForegroundAppHelper {
|
||||||
abstract suspend fun getForegroundAppPackage(): String?
|
abstract suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long)
|
||||||
abstract fun getPermissionStatus(): RuntimePermissionStatus
|
abstract fun getPermissionStatus(): RuntimePermissionStatus
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import android.app.usage.UsageStatsManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import io.timelimit.android.coroutines.executeAndWait
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
|
import io.timelimit.android.integration.platform.ForegroundAppSpec
|
||||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
@ -37,11 +38,12 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
|
||||||
|
|
||||||
private var lastQueryTime: Long = 0
|
private var lastQueryTime: Long = 0
|
||||||
private var lastPackage: String? = null
|
private var lastPackage: String? = null
|
||||||
|
private var lastPackageActivity: String? = null
|
||||||
private var lastPackageTime: Long = 0
|
private var lastPackageTime: Long = 0
|
||||||
private val event = UsageEvents.Event()
|
private val event = UsageEvents.Event()
|
||||||
|
|
||||||
@Throws(SecurityException::class)
|
@Throws(SecurityException::class)
|
||||||
override suspend fun getForegroundAppPackage(): String? {
|
override suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long) {
|
||||||
if (getPermissionStatus() == RuntimePermissionStatus.NotGranted) {
|
if (getPermissionStatus() == RuntimePermissionStatus.NotGranted) {
|
||||||
throw SecurityException()
|
throw SecurityException()
|
||||||
}
|
}
|
||||||
|
@ -49,10 +51,11 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
|
||||||
return foregroundAppThread.executeAndWait {
|
return foregroundAppThread.executeAndWait {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
|
|
||||||
if (lastQueryTime > now) {
|
if (lastQueryTime > now || queryInterval >= 1000 * 60 * 60 * 24 /* 1 day */) {
|
||||||
// if the time went backwards, forget everything
|
// if the time went backwards, forget everything
|
||||||
lastQueryTime = 0
|
lastQueryTime = 0
|
||||||
lastPackage = null
|
lastPackage = null
|
||||||
|
lastPackageActivity = null
|
||||||
lastPackageTime = 0
|
lastPackageTime = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +69,7 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
|
||||||
// which seems to provide all data
|
// which seems to provide all data
|
||||||
// update: with 1 second, some App switching events were missed
|
// update: with 1 second, some App switching events were missed
|
||||||
// it seems to always work with 1.5 seconds
|
// it seems to always work with 1.5 seconds
|
||||||
lastQueryTime - 1500
|
lastQueryTime - Math.max(queryInterval, 1500)
|
||||||
}
|
}
|
||||||
|
|
||||||
usageStatsManager.queryEvents(queryStartTime, now)?.let { usageEvents ->
|
usageStatsManager.queryEvents(queryStartTime, now)?.let { usageEvents ->
|
||||||
|
@ -77,6 +80,7 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
|
||||||
if (event.timeStamp > lastPackageTime) {
|
if (event.timeStamp > lastPackageTime) {
|
||||||
lastPackageTime = event.timeStamp
|
lastPackageTime = event.timeStamp
|
||||||
lastPackage = event.packageName
|
lastPackage = event.packageName
|
||||||
|
lastPackageActivity = event.className
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,7 +88,8 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
|
||||||
|
|
||||||
lastQueryTime = now
|
lastQueryTime = now
|
||||||
|
|
||||||
lastPackage
|
result.packageName = lastPackage
|
||||||
|
result.activityName = lastPackageActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ package io.timelimit.android.integration.platform.dummy
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import io.timelimit.android.data.model.App
|
import io.timelimit.android.data.model.App
|
||||||
|
import io.timelimit.android.data.model.AppActivity
|
||||||
import io.timelimit.android.integration.platform.*
|
import io.timelimit.android.integration.platform.*
|
||||||
|
|
||||||
class DummyIntegration(
|
class DummyIntegration(
|
||||||
|
@ -37,6 +38,10 @@ class DummyIntegration(
|
||||||
return localApps
|
return localApps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLocalAppActivities(deviceId: String): Collection<AppActivity> {
|
||||||
|
return emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getLocalAppTitle(packageName: String): String? {
|
override fun getLocalAppTitle(packageName: String): String? {
|
||||||
return localApps.find { it.packageName == packageName }?.title
|
return localApps.find { it.packageName == packageName }?.title
|
||||||
}
|
}
|
||||||
|
@ -45,10 +50,20 @@ class DummyIntegration(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getLauncherAppPackageName(): String? = null
|
||||||
|
|
||||||
override fun getCurrentProtectionLevel(): ProtectionLevel {
|
override fun getCurrentProtectionLevel(): ProtectionLevel {
|
||||||
return protectionLevel
|
return protectionLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getOverlayPermissionStatus(): RuntimePermissionStatus {
|
||||||
|
return RuntimePermissionStatus.NotRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isAccessibilityServiceEnabled(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
override fun getForegroundAppPermissionStatus(): RuntimePermissionStatus {
|
override fun getForegroundAppPermissionStatus(): RuntimePermissionStatus {
|
||||||
return foregroundAppPermission
|
return foregroundAppPermission
|
||||||
}
|
}
|
||||||
|
@ -68,10 +83,18 @@ class DummyIntegration(
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showAppLockScreen(currentPackageName: String) {
|
override fun showAppLockScreen(currentPackageName: String, currentActivityName: String?) {
|
||||||
launchLockScreenForPackage = currentPackageName
|
launchLockScreenForPackage = currentPackageName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun muteAudioIfPossible(packageName: String) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setShowBlockingOverlay(show: Boolean) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
fun getAndResetShowAppLockScreen(): String? {
|
fun getAndResetShowAppLockScreen(): String? {
|
||||||
try {
|
try {
|
||||||
return launchLockScreenForPackage
|
return launchLockScreenForPackage
|
||||||
|
@ -80,12 +103,13 @@ class DummyIntegration(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getForegroundAppPackageName(): String? {
|
override suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long) {
|
||||||
if (foregroundAppPermission == RuntimePermissionStatus.NotGranted) {
|
if (foregroundAppPermission == RuntimePermissionStatus.NotGranted) {
|
||||||
throw SecurityException()
|
throw SecurityException()
|
||||||
}
|
}
|
||||||
|
|
||||||
return foregroundApp
|
result.packageName = foregroundApp
|
||||||
|
result.activityName = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setAppStatusMessage(message: AppStatusMessage?) {
|
override fun setAppStatusMessage(message: AppStatusMessage?) {
|
||||||
|
@ -108,6 +132,10 @@ class DummyIntegration(
|
||||||
showRevokeTemporarilyAllowedNotification = show
|
showRevokeTemporarilyAllowedNotification = show
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun showTimeWarningNotification(title: String, text: String) {
|
||||||
|
// nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
override fun disableDeviceAdmin() {
|
override fun disableDeviceAdmin() {
|
||||||
// nothing to do
|
// nothing to do
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,14 +18,18 @@ package io.timelimit.android.livedata
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
|
||||||
fun LiveData<Boolean>.or(other: LiveData<Boolean>): LiveData<Boolean> {
|
fun LiveData<Boolean>.or(other: LiveData<Boolean>): LiveData<Boolean> {
|
||||||
return mergeLiveData(this, other).map {
|
return this.switchMap { value1 ->
|
||||||
(it.first != null && it.first == true) || ( it.second != null && it.second == true)
|
other.map { value2 ->
|
||||||
|
value1 || value2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun LiveData<Boolean>.and(other: LiveData<Boolean>): LiveData<Boolean> {
|
fun LiveData<Boolean>.and(other: LiveData<Boolean>): LiveData<Boolean> {
|
||||||
return mergeLiveData(this, other).map {
|
return this.switchMap { value1 ->
|
||||||
(it.first != null && it.first == true) && ( it.second != null && it.second == true)
|
other.map { value2 ->
|
||||||
|
value1 && value2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,41 @@ class SingleItemLiveDataCache<T>(private val liveData: LiveData<T>): LiveDataCac
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SingleItemLiveDataCacheWithRequery<T>(private val liveDataCreator: () -> LiveData<T>): LiveDataCache() {
|
||||||
|
private val dummyObserver = Observer<T> {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
private var wasUsed = false
|
||||||
|
private var instance: LiveData<T>? = null
|
||||||
|
|
||||||
|
fun read(): LiveData<T> {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = liveDataCreator()
|
||||||
|
instance!!.observeForever(dummyObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
wasUsed = true
|
||||||
|
|
||||||
|
return instance!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeAllItems() {
|
||||||
|
if (instance != null) {
|
||||||
|
instance!!.removeObserver(dummyObserver)
|
||||||
|
instance = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reportLoopDone() {
|
||||||
|
if (instance != null && !wasUsed) {
|
||||||
|
removeAllItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
wasUsed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
abstract class MultiKeyLiveDataCache<R, K>: LiveDataCache() {
|
abstract class MultiKeyLiveDataCache<R, K>: LiveDataCache() {
|
||||||
class ItemWrapper<R>(val value: LiveData<R>, var used: Boolean)
|
class ItemWrapper<R>(val value: LiveData<R>, var used: Boolean)
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,10 @@ class AppLogic(
|
||||||
}
|
}
|
||||||
}.ignoreUnchanged()
|
}.ignoreUnchanged()
|
||||||
|
|
||||||
|
private val foregroundAppQueryInterval = database.config().getForegroundAppQueryIntervalAsync().apply { observeForever { } }
|
||||||
|
fun getForegroundAppQueryInterval() = foregroundAppQueryInterval.value ?: 0L
|
||||||
|
|
||||||
|
val defaultUserLogic = DefaultUserLogic(this)
|
||||||
val backgroundTaskLogic = BackgroundTaskLogic(this)
|
val backgroundTaskLogic = BackgroundTaskLogic(this)
|
||||||
val appSetupLogic = AppSetupLogic(this)
|
val appSetupLogic = AppSetupLogic(this)
|
||||||
private val syncAppsLogic = SyncInstalledAppsLogic(this)
|
private val syncAppsLogic = SyncInstalledAppsLogic(this)
|
||||||
|
|
|
@ -29,6 +29,7 @@ import io.timelimit.android.integration.platform.NewPermissionStatus
|
||||||
import io.timelimit.android.integration.platform.ProtectionLevel
|
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
import io.timelimit.android.ui.user.create.DefaultCategories
|
import io.timelimit.android.ui.user.create.DefaultCategories
|
||||||
|
import io.timelimit.android.util.AndroidVersion
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class AppSetupLogic(private val appLogic: AppLogic) {
|
class AppSetupLogic(private val appLogic: AppLogic) {
|
||||||
|
@ -84,7 +85,15 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
||||||
manipulationTriedDisablingDeviceAdmin = false,
|
manipulationTriedDisablingDeviceAdmin = false,
|
||||||
manipulationDidReboot = false,
|
manipulationDidReboot = false,
|
||||||
hadManipulation = false,
|
hadManipulation = false,
|
||||||
considerRebootManipulation = false
|
defaultUser = "",
|
||||||
|
defaultUserTimeout = 0,
|
||||||
|
considerRebootManipulation = false,
|
||||||
|
currentOverlayPermission = RuntimePermissionStatus.NotGranted,
|
||||||
|
highestOverlayPermission = RuntimePermissionStatus.NotGranted,
|
||||||
|
accessibilityServiceEnabled = false,
|
||||||
|
wasAccessibilityServiceEnabled = false,
|
||||||
|
enableActivityLevelBlocking = false,
|
||||||
|
qOrLater = AndroidVersion.qOrLater
|
||||||
)
|
)
|
||||||
|
|
||||||
appLogic.database.device().addDeviceSync(device)
|
appLogic.database.device().addDeviceSync(device)
|
||||||
|
@ -139,7 +148,9 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
||||||
blockedMinutesInWeek = ImmutableBitmask((BitSet())),
|
blockedMinutesInWeek = ImmutableBitmask((BitSet())),
|
||||||
extraTimeInMillis = 0,
|
extraTimeInMillis = 0,
|
||||||
temporarilyBlocked = false,
|
temporarilyBlocked = false,
|
||||||
parentCategoryId = ""
|
parentCategoryId = "",
|
||||||
|
blockAllNotifications = false,
|
||||||
|
timeWarnings = 0
|
||||||
))
|
))
|
||||||
|
|
||||||
appLogic.database.category().addCategory(Category(
|
appLogic.database.category().addCategory(Category(
|
||||||
|
@ -149,7 +160,9 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
||||||
blockedMinutesInWeek = defaultCategories.allowedGamesBlockedTimes,
|
blockedMinutesInWeek = defaultCategories.allowedGamesBlockedTimes,
|
||||||
extraTimeInMillis = 0,
|
extraTimeInMillis = 0,
|
||||||
temporarilyBlocked = false,
|
temporarilyBlocked = false,
|
||||||
parentCategoryId = ""
|
parentCategoryId = "",
|
||||||
|
blockAllNotifications = false,
|
||||||
|
timeWarnings = 0
|
||||||
))
|
))
|
||||||
|
|
||||||
// add default allowed apps
|
// add default allowed apps
|
||||||
|
|
|
@ -30,17 +30,23 @@ import io.timelimit.android.data.model.*
|
||||||
import io.timelimit.android.date.DateInTimezone
|
import io.timelimit.android.date.DateInTimezone
|
||||||
import io.timelimit.android.date.getMinuteOfWeek
|
import io.timelimit.android.date.getMinuteOfWeek
|
||||||
import io.timelimit.android.integration.platform.AppStatusMessage
|
import io.timelimit.android.integration.platform.AppStatusMessage
|
||||||
|
import io.timelimit.android.integration.platform.ForegroundAppSpec
|
||||||
import io.timelimit.android.integration.platform.ProtectionLevel
|
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||||
|
import io.timelimit.android.integration.platform.android.AccessibilityService
|
||||||
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
|
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
|
||||||
import io.timelimit.android.livedata.*
|
import io.timelimit.android.livedata.*
|
||||||
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
|
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
|
||||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
|
import io.timelimit.android.util.AndroidVersion
|
||||||
import io.timelimit.android.util.TimeTextUtil
|
import io.timelimit.android.util.TimeTextUtil
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class BackgroundTaskLogic(val appLogic: AppLogic) {
|
class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
|
var pauseBackgroundLoop = false
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val CHECK_PERMISSION_INTERVAL = 10 * 1000L // all 10 seconds
|
private const val CHECK_PERMISSION_INTERVAL = 10 * 1000L // all 10 seconds
|
||||||
private const val BACKGROUND_SERVICE_INTERVAL = 100L // all 100 ms
|
private const val BACKGROUND_SERVICE_INTERVAL = 100L // all 100 ms
|
||||||
|
@ -107,12 +113,15 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val shouldDoAutomaticSignOut = SingleItemLiveDataCacheWithRequery { -> appLogic.defaultUserLogic.hasAutomaticSignOut()}
|
||||||
|
|
||||||
private val liveDataCaches = LiveDataCaches(arrayOf(
|
private val liveDataCaches = LiveDataCaches(arrayOf(
|
||||||
deviceUserEntryLive,
|
deviceUserEntryLive,
|
||||||
childCategories,
|
childCategories,
|
||||||
appCategories,
|
appCategories,
|
||||||
timeLimitRules,
|
timeLimitRules,
|
||||||
usedTimesOfCategoryAndWeekByFirstDayOfWeek
|
usedTimesOfCategoryAndWeekByFirstDayOfWeek,
|
||||||
|
shouldDoAutomaticSignOut
|
||||||
))
|
))
|
||||||
|
|
||||||
private var usedTimeUpdateHelper: UsedTimeItemBatchUpdateHelper? = null
|
private var usedTimeUpdateHelper: UsedTimeItemBatchUpdateHelper? = null
|
||||||
|
@ -125,6 +134,28 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
|
|
||||||
private val appTitleCache = QueryAppTitleCache(appLogic.platformIntegration)
|
private val appTitleCache = QueryAppTitleCache(appLogic.platformIntegration)
|
||||||
|
|
||||||
|
private suspend fun openLockscreen(blockedAppPackageName: String, blockedAppActivityName: String?) {
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
title = appTitleCache.query(blockedAppPackageName),
|
||||||
|
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
||||||
|
))
|
||||||
|
|
||||||
|
appLogic.platformIntegration.setShowBlockingOverlay(true)
|
||||||
|
|
||||||
|
if (appLogic.platformIntegration.isAccessibilityServiceEnabled()) {
|
||||||
|
if (blockedAppPackageName != appLogic.platformIntegration.getLauncherAppPackageName()) {
|
||||||
|
AccessibilityService.instance?.showHomescreen()
|
||||||
|
delay(100)
|
||||||
|
AccessibilityService.instance?.showHomescreen()
|
||||||
|
delay(100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appLogic.platformIntegration.showAppLockScreen(blockedAppPackageName, blockedAppActivityName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val foregroundAppSpec = ForegroundAppSpec.newInstance()
|
||||||
|
|
||||||
private suspend fun backgroundServiceLoop() {
|
private suspend fun backgroundServiceLoop() {
|
||||||
while (true) {
|
while (true) {
|
||||||
// app must be enabled
|
// app must be enabled
|
||||||
|
@ -132,6 +163,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
usedTimeUpdateHelper?.commit(appLogic)
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
liveDataCaches.removeAllItems()
|
liveDataCaches.removeAllItems()
|
||||||
appLogic.platformIntegration.setAppStatusMessage(null)
|
appLogic.platformIntegration.setAppStatusMessage(null)
|
||||||
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
appLogic.enable.waitUntilValueMatches { it == true }
|
appLogic.enable.waitUntilValueMatches { it == true }
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
@ -142,9 +174,31 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
|
|
||||||
if (deviceUserEntry == null || deviceUserEntry.type != UserType.Child) {
|
if (deviceUserEntry == null || deviceUserEntry.type != UserType.Child) {
|
||||||
usedTimeUpdateHelper?.commit(appLogic)
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
val shouldDoAutomaticSignOut = shouldDoAutomaticSignOut.read()
|
||||||
|
|
||||||
|
if (shouldDoAutomaticSignOut.waitForNonNullValue()) {
|
||||||
|
appLogic.defaultUserLogic.reportScreenOn(appLogic.platformIntegration.isScreenOn())
|
||||||
|
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(
|
||||||
|
AppStatusMessage(
|
||||||
|
title = appLogic.context.getString(R.string.background_logic_timeout_title),
|
||||||
|
text = appLogic.context.getString(R.string.background_logic_timeout_text),
|
||||||
|
showSwitchToDefaultUserOption = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
|
|
||||||
|
liveDataCaches.reportLoopDone()
|
||||||
|
appLogic.timeApi.sleep(BACKGROUND_SERVICE_INTERVAL)
|
||||||
|
} else {
|
||||||
liveDataCaches.removeAllItems()
|
liveDataCaches.removeAllItems()
|
||||||
appLogic.platformIntegration.setAppStatusMessage(null)
|
appLogic.platformIntegration.setAppStatusMessage(null)
|
||||||
deviceUserEntryLive.read().waitUntilValueMatches { it != null && it.type == UserType.Child }
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
|
|
||||||
|
val isChildSignedIn = deviceUserEntryLive.read().map { it != null && it.type == UserType.Child }
|
||||||
|
|
||||||
|
isChildSignedIn.or(shouldDoAutomaticSignOut).waitUntilValueMatches { it == true }
|
||||||
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -159,11 +213,17 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
val minuteOfWeek = getMinuteOfWeek(nowTimestamp, nowTimezone)
|
val minuteOfWeek = getMinuteOfWeek(nowTimestamp, nowTimezone)
|
||||||
|
|
||||||
// eventually remove old used time data
|
// eventually remove old used time data
|
||||||
if (dayChangeTracker.reportDayChange(nowDate.dayOfEpoch) == DayChangeTracker.DayChange.NowSinceLongerTime) {
|
run {
|
||||||
UsedTimeDeleter.deleteOldUsedTimeItems(
|
val dayChange = dayChangeTracker.reportDayChange(nowDate.dayOfEpoch)
|
||||||
|
|
||||||
|
fun deleteOldUsedTimes() = UsedTimeDeleter.deleteOldUsedTimeItems(
|
||||||
database = appLogic.database,
|
database = appLogic.database,
|
||||||
date = nowDate
|
date = nowDate
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (dayChange == DayChangeTracker.DayChange.NowSinceLongerTime) {
|
||||||
|
deleteOldUsedTimes()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the categories
|
// get the categories
|
||||||
|
@ -173,33 +233,63 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
// get the current status
|
// get the current status
|
||||||
val isScreenOn = appLogic.platformIntegration.isScreenOn()
|
val isScreenOn = appLogic.platformIntegration.isScreenOn()
|
||||||
|
|
||||||
|
appLogic.defaultUserLogic.reportScreenOn(isScreenOn)
|
||||||
|
|
||||||
if (!isScreenOn) {
|
if (!isScreenOn) {
|
||||||
if (temporarilyAllowedApps.isNotEmpty()) {
|
if (temporarilyAllowedApps.isNotEmpty()) {
|
||||||
resetTemporarilyAllowedApps()
|
resetTemporarilyAllowedApps()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val foregroundAppPackageName = appLogic.platformIntegration.getForegroundAppPackageName()
|
appLogic.platformIntegration.getForegroundApp(foregroundAppSpec, appLogic.getForegroundAppQueryInterval())
|
||||||
|
val foregroundAppPackageName = foregroundAppSpec.packageName
|
||||||
|
val foregroundAppActivityName = foregroundAppSpec.activityName
|
||||||
|
val activityLevelBlocking = appLogic.deviceEntry.value?.enableActivityLevelBlocking ?: false
|
||||||
|
|
||||||
|
fun showStatusMessageWithCurrentAppTitle(text: String, titlePrefix: String? = "") {
|
||||||
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
|
titlePrefix + appTitleCache.query(foregroundAppPackageName ?: "invalid"),
|
||||||
|
text,
|
||||||
|
if (activityLevelBlocking) foregroundAppActivityName?.removePrefix(foregroundAppPackageName ?: "invalid") else null
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
// the following is not executed if the permission is missing
|
// the following is not executed if the permission is missing
|
||||||
|
|
||||||
if (foregroundAppPackageName == BuildConfig.APPLICATION_ID) {
|
if (pauseBackgroundLoop) {
|
||||||
// this app itself runs now -> no need for an status message
|
|
||||||
usedTimeUpdateHelper?.commit(appLogic)
|
|
||||||
appLogic.platformIntegration.setAppStatusMessage(null)
|
|
||||||
} else if (foregroundAppPackageName != null && AndroidIntegrationApps.ignoredApps.contains(foregroundAppPackageName)) {
|
|
||||||
usedTimeUpdateHelper?.commit(appLogic)
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||||
appTitleCache.query(foregroundAppPackageName),
|
title = appLogic.context.getString(R.string.background_logic_paused_title),
|
||||||
appLogic.context.getString(R.string.background_logic_whitelisted)
|
text = appLogic.context.getString(R.string.background_logic_paused_text)
|
||||||
))
|
))
|
||||||
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
|
} else if (
|
||||||
|
(foregroundAppPackageName == BuildConfig.APPLICATION_ID) ||
|
||||||
|
(foregroundAppPackageName != null && AndroidIntegrationApps.ignoredApps.contains(foregroundAppPackageName))) {
|
||||||
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
showStatusMessageWithCurrentAppTitle(
|
||||||
|
text = appLogic.context.getString(R.string.background_logic_whitelisted)
|
||||||
|
)
|
||||||
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
} else if (foregroundAppPackageName != null && temporarilyAllowedApps.contains(foregroundAppPackageName)) {
|
} else if (foregroundAppPackageName != null && temporarilyAllowedApps.contains(foregroundAppPackageName)) {
|
||||||
usedTimeUpdateHelper?.commit(appLogic)
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
showStatusMessageWithCurrentAppTitle(appLogic.context.getString(R.string.background_logic_temporarily_allowed))
|
||||||
appTitleCache.query(foregroundAppPackageName),
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
appLogic.context.getString(R.string.background_logic_temporarily_allowed)
|
|
||||||
))
|
|
||||||
} else if (foregroundAppPackageName != null) {
|
} else if (foregroundAppPackageName != null) {
|
||||||
val appCategory = appCategories.get(Pair(foregroundAppPackageName, categories.map { it.id })).waitForNullableValue()
|
val categoryIds = categories.map { it.id }
|
||||||
|
|
||||||
|
val appCategory = run {
|
||||||
|
val appLevelCategoryLive = appCategories.get(foregroundAppPackageName to categoryIds)
|
||||||
|
|
||||||
|
if (activityLevelBlocking) {
|
||||||
|
val appActivityCategoryLive = appCategories.get("$foregroundAppPackageName:$foregroundAppActivityName" to categoryIds)
|
||||||
|
|
||||||
|
appActivityCategoryLive.waitForNullableValue() ?: appLevelCategoryLive.waitForNullableValue()
|
||||||
|
} else {
|
||||||
|
appLevelCategoryLive.waitForNullableValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val category = categories.find { it.id == appCategory?.categoryId }
|
val category = categories.find { it.id == appCategory?.categoryId }
|
||||||
?: categories.find { it.id == deviceUserEntry.categoryForNotAssignedApps }
|
?: categories.find { it.id == deviceUserEntry.categoryForNotAssignedApps }
|
||||||
val parentCategory = categories.find { it.id == category?.parentCategoryId }
|
val parentCategory = categories.find { it.id == category?.parentCategoryId }
|
||||||
|
@ -207,39 +297,30 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
if (category == null) {
|
if (category == null) {
|
||||||
usedTimeUpdateHelper?.commit(appLogic)
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
|
||||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
if (AndroidIntegrationApps.ignoredApps.contains(foregroundAppPackageName) == false) {
|
||||||
title = appTitleCache.query(foregroundAppPackageName),
|
// don't suspend system apps which are whitelisted in any version
|
||||||
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
|
||||||
))
|
|
||||||
appLogic.platformIntegration.setSuspendedApps(listOf(foregroundAppPackageName), true)
|
appLogic.platformIntegration.setSuspendedApps(listOf(foregroundAppPackageName), true)
|
||||||
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
}
|
||||||
|
|
||||||
|
openLockscreen(foregroundAppPackageName, foregroundAppActivityName)
|
||||||
} else if (category.temporarilyBlocked or (parentCategory?.temporarilyBlocked == true)) {
|
} else if (category.temporarilyBlocked or (parentCategory?.temporarilyBlocked == true)) {
|
||||||
usedTimeUpdateHelper?.commit(appLogic)
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
|
||||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
openLockscreen(foregroundAppPackageName, foregroundAppActivityName)
|
||||||
title = appTitleCache.query(foregroundAppPackageName),
|
|
||||||
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
|
||||||
))
|
|
||||||
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
|
||||||
} else {
|
} else {
|
||||||
// disable time limits temporarily feature
|
// disable time limits temporarily feature
|
||||||
if (nowTimestamp < deviceUserEntry.disableLimitsUntil) {
|
if (nowTimestamp < deviceUserEntry.disableLimitsUntil) {
|
||||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
showStatusMessageWithCurrentAppTitle(appLogic.context.getString(R.string.background_logic_limits_disabled))
|
||||||
title = appTitleCache.query(foregroundAppPackageName),
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
text = appLogic.context.getString(R.string.background_logic_limits_disabled)
|
|
||||||
))
|
|
||||||
} else if (
|
} else if (
|
||||||
// check blocked time areas
|
// check blocked time areas
|
||||||
|
// directly blocked
|
||||||
(category.blockedMinutesInWeek.read(minuteOfWeek)) or
|
(category.blockedMinutesInWeek.read(minuteOfWeek)) or
|
||||||
(parentCategory?.blockedMinutesInWeek?.read(minuteOfWeek) == true)
|
(parentCategory?.blockedMinutesInWeek?.read(minuteOfWeek) == true)
|
||||||
) {
|
) {
|
||||||
usedTimeUpdateHelper?.commit(appLogic)
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
|
||||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
openLockscreen(foregroundAppPackageName, foregroundAppActivityName)
|
||||||
title = appTitleCache.query(foregroundAppPackageName),
|
|
||||||
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
|
||||||
))
|
|
||||||
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
|
||||||
} else {
|
} else {
|
||||||
// check time limits
|
// check time limits
|
||||||
val rules = timeLimitRules.get(category.id).waitForNonNullValue()
|
val rules = timeLimitRules.get(category.id).waitForNonNullValue()
|
||||||
|
@ -251,10 +332,11 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
// unlimited
|
// unlimited
|
||||||
usedTimeUpdateHelper?.commit(appLogic)
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
|
||||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
showStatusMessageWithCurrentAppTitle(
|
||||||
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
|
text = appLogic.context.getString(R.string.background_logic_no_timelimit),
|
||||||
appLogic.context.getString(R.string.background_logic_no_timelimit)
|
titlePrefix = category.title + " - "
|
||||||
))
|
)
|
||||||
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
} else {
|
} else {
|
||||||
val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
|
val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
|
||||||
val parentUsedTimes = parentCategory?.let {
|
val parentUsedTimes = parentCategory?.let {
|
||||||
|
@ -317,42 +399,57 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
|
|
||||||
usedTimeUpdateHelper?.commit(appLogic)
|
usedTimeUpdateHelper?.commit(appLogic)
|
||||||
|
|
||||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
showStatusMessageWithCurrentAppTitle(
|
||||||
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
|
text = appLogic.context.getString(R.string.background_logic_no_timelimit),
|
||||||
appLogic.context.getString(R.string.background_logic_no_timelimit)
|
titlePrefix = category.title + " - "
|
||||||
))
|
)
|
||||||
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
} else {
|
} else {
|
||||||
// time limited
|
// time limited
|
||||||
if (remaining.includingExtraTime > 0) {
|
if (remaining.includingExtraTime > 0) {
|
||||||
|
var subtractExtraTime: Boolean
|
||||||
|
|
||||||
if (remaining.default == 0L) {
|
if (remaining.default == 0L) {
|
||||||
// using extra time
|
// using extra time
|
||||||
|
showStatusMessageWithCurrentAppTitle(
|
||||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
text = appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(remaining.includingExtraTime.toInt(), appLogic.context)),
|
||||||
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
|
titlePrefix = category.title + " - "
|
||||||
appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(remaining.includingExtraTime.toInt(), appLogic.context))
|
|
||||||
))
|
|
||||||
|
|
||||||
if (isScreenOn) {
|
|
||||||
newUsedTimeItemBatchUpdateHelper.addUsedTime(
|
|
||||||
Math.min(previousMainLogicExecutionTime, MAX_USED_TIME_PER_ROUND), // never save more than a second of used time
|
|
||||||
true,
|
|
||||||
appLogic
|
|
||||||
)
|
)
|
||||||
}
|
subtractExtraTime = true
|
||||||
} else {
|
} else {
|
||||||
// using normal contingent
|
// using normal contingent
|
||||||
|
showStatusMessageWithCurrentAppTitle(
|
||||||
|
text = TimeTextUtil.remaining(remaining.default.toInt(), appLogic.context),
|
||||||
|
titlePrefix = category.title + " - "
|
||||||
|
)
|
||||||
|
subtractExtraTime = false
|
||||||
|
}
|
||||||
|
|
||||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
|
|
||||||
TimeTextUtil.remaining(remaining.default.toInt(), appLogic.context)
|
|
||||||
))
|
|
||||||
|
|
||||||
if (isScreenOn) {
|
if (isScreenOn) {
|
||||||
|
// never save more than a second of used time
|
||||||
|
val timeToSubtract = Math.min(previousMainLogicExecutionTime, MAX_USED_TIME_PER_ROUND)
|
||||||
|
|
||||||
newUsedTimeItemBatchUpdateHelper.addUsedTime(
|
newUsedTimeItemBatchUpdateHelper.addUsedTime(
|
||||||
Math.min(previousMainLogicExecutionTime, MAX_USED_TIME_PER_ROUND), // never save more than a second of used time
|
timeToSubtract,
|
||||||
false,
|
subtractExtraTime,
|
||||||
appLogic
|
appLogic
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val oldRemainingTime = remaining.includingExtraTime
|
||||||
|
val newRemainingTime = oldRemainingTime - timeToSubtract
|
||||||
|
|
||||||
|
if (oldRemainingTime / (1000 * 60) != newRemainingTime / (1000 * 60)) {
|
||||||
|
// eventually show remaining time warning
|
||||||
|
val roundedNewTime = ((newRemainingTime / (1000 * 60)) + 1) * (1000 * 60)
|
||||||
|
val flagIndex = CategoryTimeWarnings.durationToBitIndex[roundedNewTime]
|
||||||
|
|
||||||
|
if (flagIndex != null && category.timeWarnings and (1 shl flagIndex) != 0) {
|
||||||
|
appLogic.platformIntegration.showTimeWarningNotification(
|
||||||
|
title = appLogic.context.getString(R.string.time_warning_not_title, category.title),
|
||||||
|
text = TimeTextUtil.remaining(roundedNewTime.toInt(), appLogic.context)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -360,11 +457,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
|
|
||||||
newUsedTimeItemBatchUpdateHelper.commit(appLogic)
|
newUsedTimeItemBatchUpdateHelper.commit(appLogic)
|
||||||
|
|
||||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
openLockscreen(foregroundAppPackageName, foregroundAppActivityName)
|
||||||
title = appTitleCache.query(foregroundAppPackageName),
|
|
||||||
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
|
||||||
))
|
|
||||||
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -375,6 +468,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
appLogic.context.getString(R.string.background_logic_idle_title),
|
appLogic.context.getString(R.string.background_logic_idle_title),
|
||||||
appLogic.context.getString(R.string.background_logic_idle_text)
|
appLogic.context.getString(R.string.background_logic_idle_text)
|
||||||
))
|
))
|
||||||
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
}
|
}
|
||||||
} catch (ex: SecurityException) {
|
} catch (ex: SecurityException) {
|
||||||
// this is handled by an other main loop (with a delay)
|
// this is handled by an other main loop (with a delay)
|
||||||
|
@ -383,6 +477,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
appLogic.context.getString(R.string.background_logic_error),
|
appLogic.context.getString(R.string.background_logic_error),
|
||||||
appLogic.context.getString(R.string.background_logic_error_permission)
|
appLogic.context.getString(R.string.background_logic_error_permission)
|
||||||
))
|
))
|
||||||
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Log.w(LOG_TAG, "exception during running main loop", ex)
|
Log.w(LOG_TAG, "exception during running main loop", ex)
|
||||||
|
@ -392,6 +487,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
appLogic.context.getString(R.string.background_logic_error),
|
appLogic.context.getString(R.string.background_logic_error),
|
||||||
appLogic.context.getString(R.string.background_logic_error_internal)
|
appLogic.context.getString(R.string.background_logic_error_internal)
|
||||||
))
|
))
|
||||||
|
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
liveDataCaches.reportLoopDone()
|
liveDataCaches.reportLoopDone()
|
||||||
|
@ -413,10 +509,11 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
if (deviceEntry != null) {
|
if (deviceEntry != null) {
|
||||||
if (deviceEntry.currentAppVersion != currentAppVersion) {
|
if (deviceEntry.currentAppVersion != currentAppVersion) {
|
||||||
ApplyActionUtil.applyAppLogicAction(
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
UpdateDeviceStatusAction.empty.copy(
|
action = UpdateDeviceStatusAction.empty.copy(
|
||||||
newAppVersion = currentAppVersion
|
newAppVersion = currentAppVersion
|
||||||
),
|
),
|
||||||
appLogic
|
appLogic = appLogic,
|
||||||
|
ignoreIfDeviceIsNotConfigured = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -446,10 +543,11 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
|
|
||||||
if (deviceEntry?.considerRebootManipulation == true) {
|
if (deviceEntry?.considerRebootManipulation == true) {
|
||||||
ApplyActionUtil.applyAppLogicAction(
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
UpdateDeviceStatusAction.empty.copy(
|
action = UpdateDeviceStatusAction.empty.copy(
|
||||||
didReboot = true
|
didReboot = true
|
||||||
),
|
),
|
||||||
appLogic
|
appLogic = appLogic,
|
||||||
|
ignoreIfDeviceIsNotConfigured = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -463,6 +561,9 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
val protectionLevel = appLogic.platformIntegration.getCurrentProtectionLevel()
|
val protectionLevel = appLogic.platformIntegration.getCurrentProtectionLevel()
|
||||||
val usageStatsPermission = appLogic.platformIntegration.getForegroundAppPermissionStatus()
|
val usageStatsPermission = appLogic.platformIntegration.getForegroundAppPermissionStatus()
|
||||||
val notificationAccess = appLogic.platformIntegration.getNotificationAccessPermissionStatus()
|
val notificationAccess = appLogic.platformIntegration.getNotificationAccessPermissionStatus()
|
||||||
|
val overlayPermission = appLogic.platformIntegration.getOverlayPermissionStatus()
|
||||||
|
val accessibilityService = appLogic.platformIntegration.isAccessibilityServiceEnabled()
|
||||||
|
val qOrLater = AndroidVersion.qOrLater
|
||||||
|
|
||||||
var changes = UpdateDeviceStatusAction.empty
|
var changes = UpdateDeviceStatusAction.empty
|
||||||
|
|
||||||
|
@ -488,8 +589,28 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (overlayPermission != deviceEntry.currentOverlayPermission) {
|
||||||
|
changes = changes.copy(
|
||||||
|
newOverlayPermission = overlayPermission
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessibilityService != deviceEntry.accessibilityServiceEnabled) {
|
||||||
|
changes = changes.copy(
|
||||||
|
newAccessibilityServiceEnabled = accessibilityService
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qOrLater && !deviceEntry.qOrLater) {
|
||||||
|
changes = changes.copy(isQOrLaterNow = true)
|
||||||
|
}
|
||||||
|
|
||||||
if (changes != UpdateDeviceStatusAction.empty) {
|
if (changes != UpdateDeviceStatusAction.empty) {
|
||||||
ApplyActionUtil.applyAppLogicAction(changes, appLogic)
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
|
action = changes,
|
||||||
|
appLogic = appLogic,
|
||||||
|
ignoreIfDeviceIsNotConfigured = true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Open TimeLimit Copyright <C> 2019 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -20,10 +20,7 @@ import android.util.SparseLongArray
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Transformations
|
import androidx.lifecycle.Transformations
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.data.model.Category
|
import io.timelimit.android.data.model.*
|
||||||
import io.timelimit.android.data.model.TimeLimitRule
|
|
||||||
import io.timelimit.android.data.model.User
|
|
||||||
import io.timelimit.android.data.model.UserType
|
|
||||||
import io.timelimit.android.date.DateInTimezone
|
import io.timelimit.android.date.DateInTimezone
|
||||||
import io.timelimit.android.date.getMinuteOfWeek
|
import io.timelimit.android.date.getMinuteOfWeek
|
||||||
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
|
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
|
||||||
|
@ -37,69 +34,106 @@ enum class BlockingReason {
|
||||||
TemporarilyBlocked,
|
TemporarilyBlocked,
|
||||||
BlockedAtThisTime,
|
BlockedAtThisTime,
|
||||||
TimeOver,
|
TimeOver,
|
||||||
TimeOverExtraTimeCanBeUsedLater
|
TimeOverExtraTimeCanBeUsedLater,
|
||||||
|
NotificationsAreBlocked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class BlockingLevel {
|
||||||
|
App,
|
||||||
|
Activity
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class BlockingReasonDetail {
|
||||||
|
abstract val areNotificationsBlocked: Boolean
|
||||||
|
}
|
||||||
|
data class NoBlockingReason(
|
||||||
|
override val areNotificationsBlocked: Boolean
|
||||||
|
): BlockingReasonDetail() {
|
||||||
|
companion object {
|
||||||
|
private val instanceWithoutNotificationsBlocked = NoBlockingReason(areNotificationsBlocked = false)
|
||||||
|
private val instanceWithNotificationsBlocked = NoBlockingReason(areNotificationsBlocked = true)
|
||||||
|
|
||||||
|
fun getInstance(areNotificationsBlocked: Boolean) = if (areNotificationsBlocked)
|
||||||
|
instanceWithNotificationsBlocked
|
||||||
|
else
|
||||||
|
instanceWithoutNotificationsBlocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data class BlockedReasonDetails(
|
||||||
|
val reason: BlockingReason,
|
||||||
|
val level: BlockingLevel,
|
||||||
|
val categoryId: String?,
|
||||||
|
override val areNotificationsBlocked: Boolean
|
||||||
|
): BlockingReasonDetail()
|
||||||
|
|
||||||
class BlockingReasonUtil(private val appLogic: AppLogic) {
|
class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val LOG_TAG = "BlockingReason"
|
private const val LOG_TAG = "BlockingReason"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBlockingReason(packageName: String): LiveData<BlockingReason> {
|
private val enableActivityLevelFiltering = appLogic.deviceEntry.map { it?.enableActivityLevelBlocking ?: false }
|
||||||
|
|
||||||
|
fun getBlockingReason(packageName: String, activityName: String?): LiveData<BlockingReasonDetail> {
|
||||||
// check precondition that the app is running
|
// check precondition that the app is running
|
||||||
|
|
||||||
return appLogic.enable.switchMap {
|
return appLogic.enable.switchMap {
|
||||||
enabled ->
|
enabled ->
|
||||||
|
|
||||||
if (enabled == null || enabled == false) {
|
if (enabled == null || enabled == false) {
|
||||||
liveDataFromValue(BlockingReason.None)
|
liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false) as BlockingReasonDetail)
|
||||||
} else {
|
} else {
|
||||||
appLogic.deviceUserEntry.switchMap {
|
appLogic.deviceUserEntry.switchMap {
|
||||||
user ->
|
user ->
|
||||||
|
|
||||||
if (user == null || user.type != UserType.Child) {
|
if (user == null || user.type != UserType.Child) {
|
||||||
liveDataFromValue(BlockingReason.None)
|
liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false) as BlockingReasonDetail)
|
||||||
} else {
|
} else {
|
||||||
getBlockingReasonStep2(packageName, user, TimeZone.getTimeZone(user.timeZone))
|
getBlockingReasonStep2(packageName, activityName, user, TimeZone.getTimeZone(user.timeZone))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getBlockingReasonStep2(packageName: String, child: User, timeZone: TimeZone): LiveData<BlockingReason> {
|
private fun getBlockingReasonStep2(packageName: String, activityName: String?, child: User, timeZone: TimeZone): LiveData<BlockingReasonDetail> {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Log.d(LOG_TAG, "step 2")
|
Log.d(LOG_TAG, "step 2")
|
||||||
}
|
}
|
||||||
|
|
||||||
// check internal whitelist
|
// check internal whitelist
|
||||||
if (packageName == BuildConfig.APPLICATION_ID) {
|
if (packageName == BuildConfig.APPLICATION_ID) {
|
||||||
return liveDataFromValue(BlockingReason.None)
|
return liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false))
|
||||||
} else if (AndroidIntegrationApps.ignoredApps.contains(packageName)) {
|
} else if (AndroidIntegrationApps.ignoredApps.contains(packageName)) {
|
||||||
return liveDataFromValue(BlockingReason.None)
|
return liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false))
|
||||||
} else {
|
} else {
|
||||||
return getBlockingReasonStep3(packageName, child, timeZone)
|
return getBlockingReasonStep3(packageName, activityName, child, timeZone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getBlockingReasonStep3(packageName: String, child: User, timeZone: TimeZone): LiveData<BlockingReason> {
|
private fun getBlockingReasonStep3(packageName: String, activityName: String?, child: User, timeZone: TimeZone): LiveData<BlockingReasonDetail> {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Log.d(LOG_TAG, "step 3")
|
Log.d(LOG_TAG, "step 3")
|
||||||
}
|
}
|
||||||
|
|
||||||
// check temporarily allowed Apps
|
// check temporarily allowed Apps
|
||||||
return appLogic.database.temporarilyAllowedApp().getTemporarilyAllowedApps().switchMap {
|
return appLogic.deviceId.switchMap {
|
||||||
|
if (it != null) {
|
||||||
|
appLogic.database.temporarilyAllowedApp().getTemporarilyAllowedApps()
|
||||||
|
} else {
|
||||||
|
liveDataFromValue(Collections.emptyList())
|
||||||
|
}
|
||||||
|
}.switchMap {
|
||||||
temporarilyAllowedApps ->
|
temporarilyAllowedApps ->
|
||||||
|
|
||||||
if (temporarilyAllowedApps.contains(packageName)) {
|
if (temporarilyAllowedApps.contains(packageName)) {
|
||||||
liveDataFromValue(BlockingReason.None)
|
liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false) as BlockingReasonDetail)
|
||||||
} else {
|
} else {
|
||||||
getBlockingReasonStep4(packageName, child, timeZone)
|
getBlockingReasonStep4(packageName, activityName, child, timeZone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getBlockingReasonStep4(packageName: String, child: User, timeZone: TimeZone): LiveData<BlockingReason> {
|
private fun getBlockingReasonStep4(packageName: String, activityName: String?, child: User, timeZone: TimeZone): LiveData<BlockingReasonDetail> {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Log.d(LOG_TAG, "step 4")
|
Log.d(LOG_TAG, "step 4")
|
||||||
}
|
}
|
||||||
|
@ -107,13 +141,27 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
return appLogic.database.category().getCategoriesByChildId(child.id).switchMap {
|
return appLogic.database.category().getCategoriesByChildId(child.id).switchMap {
|
||||||
childCategories ->
|
childCategories ->
|
||||||
|
|
||||||
Transformations.map(appLogic.database.categoryApp().getCategoryApp(childCategories.map { it.id }, packageName)) {
|
val categoryAppLevel = appLogic.database.categoryApp().getCategoryApp(childCategories.map { it.id }, packageName)
|
||||||
|
val categoryAppActivityLevel = enableActivityLevelFiltering.switchMap {
|
||||||
|
if (it)
|
||||||
|
appLogic.database.categoryApp().getCategoryApp(childCategories.map { it.id }, "$packageName:$activityName")
|
||||||
|
else
|
||||||
|
liveDataFromValue(null as CategoryApp?)
|
||||||
|
}
|
||||||
|
|
||||||
|
val categoryApp = categoryAppLevel.switchMap { appLevel ->
|
||||||
|
categoryAppActivityLevel.map { activityLevel ->
|
||||||
|
activityLevel?.let { it to BlockingLevel.Activity } ?: appLevel?.let { it to BlockingLevel.App }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Transformations.map(categoryApp) {
|
||||||
categoryApp ->
|
categoryApp ->
|
||||||
|
|
||||||
if (categoryApp == null) {
|
if (categoryApp == null) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
childCategories.find { it.id == categoryApp.categoryId }
|
childCategories.find { it.id == categoryApp.first.categoryId }?.let { it to categoryApp.second }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.switchMap {
|
}.switchMap {
|
||||||
|
@ -127,22 +175,52 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
|
|
||||||
defaultCategory.switchMap { categoryEntry2 ->
|
defaultCategory.switchMap { categoryEntry2 ->
|
||||||
if (categoryEntry2 == null) {
|
if (categoryEntry2 == null) {
|
||||||
liveDataFromValue(BlockingReason.NotPartOfAnCategory)
|
liveDataFromValue(
|
||||||
|
BlockedReasonDetails(
|
||||||
|
areNotificationsBlocked = false,
|
||||||
|
level = BlockingLevel.App,
|
||||||
|
reason = BlockingReason.NotPartOfAnCategory,
|
||||||
|
categoryId = null
|
||||||
|
) as BlockingReasonDetail
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
getBlockingReasonStep4Point5(categoryEntry2, child, timeZone, false)
|
getBlockingReasonStep4Point5(categoryEntry2, child, timeZone, false, BlockingLevel.App)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
getBlockingReasonStep4Point5(categoryEntry, child, timeZone, false)
|
getBlockingReasonStep4Point5(categoryEntry.first, child, timeZone, false, categoryEntry.second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getBlockingReasonStep4Point5(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean): LiveData<BlockingReason> {
|
private fun getBlockingReasonStep4Point5(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean, blockingLevel: BlockingLevel): LiveData<BlockingReasonDetail> {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Log.d(LOG_TAG, "step 4.5")
|
Log.d(LOG_TAG, "step 4.5")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val blockNotifications = category.blockAllNotifications
|
||||||
|
|
||||||
|
val nextLevel = getBlockingReasonStep4Point7(category, child, timeZone, isParentCategory, blockingLevel)
|
||||||
|
|
||||||
|
return nextLevel.map { blockingReason ->
|
||||||
|
if (blockingReason == BlockingReason.None) {
|
||||||
|
NoBlockingReason.getInstance(areNotificationsBlocked = blockNotifications)
|
||||||
|
} else {
|
||||||
|
BlockedReasonDetails(
|
||||||
|
areNotificationsBlocked = blockNotifications,
|
||||||
|
level = blockingLevel,
|
||||||
|
reason = blockingReason,
|
||||||
|
categoryId = category.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBlockingReasonStep4Point7(category: Category, child: User, timeZone: TimeZone, isParentCategory: Boolean, blockingLevel: BlockingLevel): LiveData<BlockingReason> {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "step 4.7")
|
||||||
|
}
|
||||||
|
|
||||||
if (category.temporarilyBlocked) {
|
if (category.temporarilyBlocked) {
|
||||||
return liveDataFromValue(BlockingReason.TemporarilyBlocked)
|
return liveDataFromValue(BlockingReason.TemporarilyBlocked)
|
||||||
}
|
}
|
||||||
|
@ -152,8 +230,10 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
if (child.disableLimitsUntil == 0L) {
|
if (child.disableLimitsUntil == 0L) {
|
||||||
areLimitsDisabled = liveDataFromValue(false)
|
areLimitsDisabled = liveDataFromValue(false)
|
||||||
} else {
|
} else {
|
||||||
areLimitsDisabled = timeInMillis.map { timeInMillis ->
|
areLimitsDisabled = getTemporarilyTrustedTimeInMillis().map {
|
||||||
child.disableLimitsUntil > timeInMillis
|
trustedTimeInMillis ->
|
||||||
|
|
||||||
|
trustedTimeInMillis != null && child.disableLimitsUntil > trustedTimeInMillis
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,7 +251,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
if (parentCategory == null) {
|
if (parentCategory == null) {
|
||||||
liveDataFromValue(BlockingReason.None)
|
liveDataFromValue(BlockingReason.None)
|
||||||
} else {
|
} else {
|
||||||
getBlockingReasonStep4Point5(parentCategory, child, timeZone, true)
|
getBlockingReasonStep4Point7(parentCategory, child, timeZone, true, blockingLevel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -185,7 +265,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
Log.d(LOG_TAG, "step 5")
|
Log.d(LOG_TAG, "step 5")
|
||||||
}
|
}
|
||||||
|
|
||||||
return Transformations.switchMap(getMinuteOfWeekLive(appLogic.timeApi, timeZone)) {
|
return Transformations.switchMap(getTrustedMinuteOfWeekLive(appLogic.timeApi, timeZone)) {
|
||||||
trustedMinuteOfWeek ->
|
trustedMinuteOfWeek ->
|
||||||
|
|
||||||
if (category.blockedMinutesInWeek.dataNotToModify.isEmpty) {
|
if (category.blockedMinutesInWeek.dataNotToModify.isEmpty) {
|
||||||
|
@ -203,7 +283,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
Log.d(LOG_TAG, "step 6")
|
Log.d(LOG_TAG, "step 6")
|
||||||
}
|
}
|
||||||
|
|
||||||
return getDateLive(appLogic.timeApi, timeZone).switchMap {
|
return getTrustedDateLive(appLogic.timeApi, timeZone).switchMap {
|
||||||
nowTrustedDate ->
|
nowTrustedDate ->
|
||||||
|
|
||||||
appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id).switchMap {
|
appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id).switchMap {
|
||||||
|
@ -212,12 +292,20 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
if (rules.isEmpty()) {
|
if (rules.isEmpty()) {
|
||||||
liveDataFromValue(BlockingReason.None)
|
liveDataFromValue(BlockingReason.None)
|
||||||
} else {
|
} else {
|
||||||
getBlockingReasonStep7(category, nowTrustedDate, rules)
|
getBlockingReasonStep6(category, nowTrustedDate, rules)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getBlockingReasonStep6(category: Category, nowTrustedDate: DateInTimezone, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "step 6 - 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
return getBlockingReasonStep7(category, nowTrustedDate, rules)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getBlockingReasonStep7(category: Category, nowTrustedDate: DateInTimezone, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
|
private fun getBlockingReasonStep7(category: Category, nowTrustedDate: DateInTimezone, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Log.d(LOG_TAG, "step 7")
|
Log.d(LOG_TAG, "step 7")
|
||||||
|
@ -246,15 +334,89 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val timeInMillis: LiveData<Long> = liveDataFromFunction {
|
private fun getTemporarilyTrustedTimeInMillis(): LiveData<Long?> {
|
||||||
|
return liveDataFromFunction {
|
||||||
appLogic.timeApi.getCurrentTimeInMillis()
|
appLogic.timeApi.getCurrentTimeInMillis()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getMinuteOfWeekLive(api: TimeApi, timeZone: TimeZone): LiveData<Int> = liveDataFromFunction {
|
private fun getTrustedMinuteOfWeekLive(api: TimeApi, timeZone: TimeZone): LiveData<Int> {
|
||||||
getMinuteOfWeek(api.getCurrentTimeInMillis(), timeZone)
|
return object: LiveData<Int>() {
|
||||||
}.ignoreUnchanged()
|
fun update() {
|
||||||
|
val timeInMillis = appLogic.timeApi.getCurrentTimeInMillis()
|
||||||
|
|
||||||
private fun getDateLive(api: TimeApi, timeZone: TimeZone): LiveData<DateInTimezone> = liveDataFromFunction {
|
value = getMinuteOfWeek(timeInMillis, timeZone)
|
||||||
DateInTimezone.newInstance(api.getCurrentTimeInMillis(), timeZone)
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
val scheduledUpdateRunnable = Runnable {
|
||||||
|
update()
|
||||||
|
scheduleUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scheduleUpdate() {
|
||||||
|
api.runDelayed(scheduledUpdateRunnable, 1000L /* every second */)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelScheduledUpdate() {
|
||||||
|
api.cancelScheduledAction(scheduledUpdateRunnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActive() {
|
||||||
|
super.onActive()
|
||||||
|
|
||||||
|
update()
|
||||||
|
scheduleUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInactive() {
|
||||||
|
super.onInactive()
|
||||||
|
|
||||||
|
cancelScheduledUpdate()
|
||||||
|
}
|
||||||
}.ignoreUnchanged()
|
}.ignoreUnchanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTrustedDateLive(api: TimeApi, timeZone: TimeZone): LiveData<DateInTimezone> {
|
||||||
|
return object: LiveData<DateInTimezone>() {
|
||||||
|
fun update() {
|
||||||
|
val timeInMillis = appLogic.timeApi.getCurrentTimeInMillis()
|
||||||
|
|
||||||
|
value = DateInTimezone.newInstance(timeInMillis, timeZone)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
val scheduledUpdateRunnable = Runnable {
|
||||||
|
update()
|
||||||
|
scheduleUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun scheduleUpdate() {
|
||||||
|
api.runDelayed(scheduledUpdateRunnable, 1000L /* every second */)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelScheduledUpdate() {
|
||||||
|
api.cancelScheduledAction(scheduledUpdateRunnable)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActive() {
|
||||||
|
super.onActive()
|
||||||
|
|
||||||
|
update()
|
||||||
|
scheduleUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInactive() {
|
||||||
|
super.onInactive()
|
||||||
|
|
||||||
|
cancelScheduledUpdate()
|
||||||
|
}
|
||||||
|
}.ignoreUnchanged()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
181
app/src/main/java/io/timelimit/android/logic/DefaultUserLogic.kt
Normal file
181
app/src/main/java/io/timelimit/android/logic/DefaultUserLogic.kt
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
/*
|
||||||
|
* 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.logic
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import io.timelimit.android.BuildConfig
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
|
import io.timelimit.android.coroutines.runAsync
|
||||||
|
import io.timelimit.android.data.model.User
|
||||||
|
import io.timelimit.android.livedata.*
|
||||||
|
import io.timelimit.android.sync.actions.SignOutAtDeviceAction
|
||||||
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
class DefaultUserLogic(private val appLogic: AppLogic) {
|
||||||
|
companion object {
|
||||||
|
private const val LOG_TAG = "DefaultUserLogic"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun defaultUserEntry() = appLogic.deviceEntry.map { device ->
|
||||||
|
device?.defaultUser
|
||||||
|
}.ignoreUnchanged().switchMap {
|
||||||
|
if (it != null)
|
||||||
|
appLogic.database.user().getUserByIdLive(it)
|
||||||
|
else
|
||||||
|
liveDataFromValue(null as User?)
|
||||||
|
}
|
||||||
|
private fun hasDefaultUser() = defaultUserEntry().map { it != null }.ignoreUnchanged()
|
||||||
|
private fun defaultUserTimeout() = appLogic.deviceEntry.map { it?.defaultUserTimeout ?: 0 }.ignoreUnchanged()
|
||||||
|
private fun hasDefaultUserTimeout() = defaultUserTimeout().map { it != 0 }.ignoreUnchanged()
|
||||||
|
fun hasAutomaticSignOut() = hasDefaultUser().and(hasDefaultUserTimeout())
|
||||||
|
|
||||||
|
private val logoutLock = Mutex()
|
||||||
|
|
||||||
|
private var lastScreenOnStatus = false
|
||||||
|
private var lastScreenDisableTime = 0L
|
||||||
|
private var lastScreenOnSaveTime = 0L
|
||||||
|
private var restoredLastScreenOnTime: Long? = null
|
||||||
|
private var didRestoreLastDisabledTime = false
|
||||||
|
|
||||||
|
fun reportScreenOn(isScreenOn: Boolean) {
|
||||||
|
if (isScreenOn) {
|
||||||
|
val now = appLogic.timeApi.getCurrentTimeInMillis()
|
||||||
|
|
||||||
|
if (lastScreenOnSaveTime + 1000 * 30 < now) {
|
||||||
|
lastScreenOnSaveTime = now
|
||||||
|
|
||||||
|
Threads.database.submit {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "save last screen on time")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restoredLastScreenOnTime == null) {
|
||||||
|
restoredLastScreenOnTime = appLogic.database.config().getLastScreenOnTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
appLogic.database.config().setLastScreenOnTime(now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isScreenOn != lastScreenOnStatus) {
|
||||||
|
lastScreenOnStatus = isScreenOn
|
||||||
|
|
||||||
|
if (isScreenOn) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "screen was enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
runAsync {
|
||||||
|
logoutLock.withLock {
|
||||||
|
if (lastScreenDisableTime == 0L) {
|
||||||
|
if (!didRestoreLastDisabledTime) {
|
||||||
|
didRestoreLastDisabledTime = true
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "screen disabling time is not known - try to restore time")
|
||||||
|
}
|
||||||
|
|
||||||
|
val nowTime = appLogic.timeApi.getCurrentTimeInMillis()
|
||||||
|
val nowUptime = appLogic.timeApi.getCurrentUptimeInMillis()
|
||||||
|
val savedLastScreenOnTime = restoredLastScreenOnTime ?: kotlin.run {
|
||||||
|
Threads.database.executeAndWait {
|
||||||
|
restoredLastScreenOnTime = appLogic.database.config().getLastScreenOnTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
restoredLastScreenOnTime!!
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "now: $nowTime; uptime: $nowUptime; last screen on time: $savedLastScreenOnTime")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedLastScreenOnTime == 0L) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "no saved value - can not restore")
|
||||||
|
}
|
||||||
|
} else if (savedLastScreenOnTime > nowTime) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "saved last screen on time is in the future - can not restore")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val diffToNow = nowTime - savedLastScreenOnTime
|
||||||
|
val theoreticallyUptimeValue = nowUptime - diffToNow
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "restored last screen on time: diff to now: ${diffToNow / 1000} s; theoretically uptime: ${theoreticallyUptimeValue / 1000} s")
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScreenDisableTime = theoreticallyUptimeValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastScreenDisableTime != 0L) {
|
||||||
|
val now = appLogic.timeApi.getCurrentUptimeInMillis()
|
||||||
|
val diff = now - lastScreenDisableTime
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "screen was disabled for ${diff / 1000} seconds")
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultUser = defaultUserEntry().waitForNullableValue()
|
||||||
|
|
||||||
|
if (defaultUser != null) {
|
||||||
|
if (appLogic.deviceEntry.waitForNullableValue()?.currentUserId == defaultUser.id) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "default user already signed in")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val timeout = defaultUserTimeout().waitForNonNullValue()
|
||||||
|
|
||||||
|
if (diff >= timeout && timeout != 0) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "much time - log out")
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
|
appLogic = appLogic,
|
||||||
|
action = SignOutAtDeviceAction,
|
||||||
|
ignoreIfDeviceIsNotConfigured = true
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "no reason to log out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "has no default user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "screen was disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScreenDisableTime = appLogic.timeApi.getCurrentUptimeInMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,11 +17,10 @@ package io.timelimit.android.logic
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import io.timelimit.android.coroutines.runAsyncExpectForever
|
import io.timelimit.android.coroutines.runAsyncExpectForever
|
||||||
|
import io.timelimit.android.data.model.AppActivity
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
import io.timelimit.android.livedata.*
|
import io.timelimit.android.livedata.*
|
||||||
import io.timelimit.android.sync.actions.AddInstalledAppsAction
|
import io.timelimit.android.sync.actions.*
|
||||||
import io.timelimit.android.sync.actions.InstalledApp
|
|
||||||
import io.timelimit.android.sync.actions.RemoveInstalledAppsAction
|
|
||||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
@ -36,12 +35,13 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
appLogic.platformIntegration.installedAppsChangeListener = Runnable { requestSync() }
|
appLogic.platformIntegration.installedAppsChangeListener = Runnable { requestSync() }
|
||||||
appLogic.deviceEntryIfEnabled.map { it?.id + it?.currentUserId }.ignoreUnchanged().observeForever { requestSync() }
|
appLogic.deviceEntry.map { it?.id + it?.enableActivityLevelBlocking }.ignoreUnchanged().observeForever { requestSync() }
|
||||||
|
|
||||||
runAsyncExpectForever { syncLoop() }
|
runAsyncExpectForever { syncLoop() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun syncLoop() {
|
private suspend fun syncLoop() {
|
||||||
|
requestSync.postValue(true)
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
requestSync.waitUntilValueMatches { it == true }
|
requestSync.waitUntilValueMatches { it == true }
|
||||||
requestSync.value = false
|
requestSync.value = false
|
||||||
|
@ -55,12 +55,10 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
|
||||||
|
|
||||||
private suspend fun doSyncNow() {
|
private suspend fun doSyncNow() {
|
||||||
doSyncLock.withLock {
|
doSyncLock.withLock {
|
||||||
val userEntry = appLogic.deviceUserEntry.waitForNullableValue()
|
val deviceEntry = appLogic.deviceEntry.waitForNullableValue() ?: return@withLock
|
||||||
|
val deviceId = deviceEntry.id
|
||||||
if (userEntry == null || userEntry.type != UserType.Child) {
|
|
||||||
return@withLock
|
|
||||||
}
|
|
||||||
|
|
||||||
|
run {
|
||||||
val currentlyInstalled = appLogic.platformIntegration.getLocalApps().associateBy { app -> app.packageName }
|
val currentlyInstalled = appLogic.platformIntegration.getLocalApps().associateBy { app -> app.packageName }
|
||||||
val currentlySaved = appLogic.database.app().getApps().waitForNonNullValue().associateBy { app -> app.packageName }
|
val currentlySaved = appLogic.database.app().getApps().waitForNonNullValue().associateBy { app -> app.packageName }
|
||||||
|
|
||||||
|
@ -74,16 +72,16 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
|
||||||
// save the changes
|
// save the changes
|
||||||
if (itemsToRemove.isNotEmpty()) {
|
if (itemsToRemove.isNotEmpty()) {
|
||||||
ApplyActionUtil.applyAppLogicAction(
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
RemoveInstalledAppsAction(packageNames = itemsToRemove.keys.toList()),
|
action = RemoveInstalledAppsAction(packageNames = itemsToRemove.keys.toList()),
|
||||||
appLogic
|
appLogic = appLogic,
|
||||||
|
ignoreIfDeviceIsNotConfigured = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemsToAdd.isNotEmpty()) {
|
if (itemsToAdd.isNotEmpty()) {
|
||||||
ApplyActionUtil.applyAppLogicAction(
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
AddInstalledAppsAction(
|
action = AddInstalledAppsAction(
|
||||||
apps = itemsToAdd.map {
|
apps = itemsToAdd.map { (_, app) ->
|
||||||
(_, app) ->
|
|
||||||
|
|
||||||
InstalledApp(
|
InstalledApp(
|
||||||
packageName = app.packageName,
|
packageName = app.packageName,
|
||||||
|
@ -93,9 +91,49 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
appLogic
|
appLogic = appLogic,
|
||||||
|
ignoreIfDeviceIsNotConfigured = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run {
|
||||||
|
fun buildKey(activity: AppActivity) = "${activity.appPackageName}:${activity.activityClassName}"
|
||||||
|
|
||||||
|
val currentlyInstalled = (
|
||||||
|
if (deviceEntry.enableActivityLevelBlocking)
|
||||||
|
appLogic.platformIntegration.getLocalAppActivities(deviceId = deviceId)
|
||||||
|
else
|
||||||
|
emptyList()
|
||||||
|
).associateBy { buildKey(it) }
|
||||||
|
|
||||||
|
val currentlySaved = appLogic.database.appActivity().getAppActivitiesByDeviceIds(deviceIds = listOf(deviceId)).waitForNonNullValue().associateBy { buildKey(it) }
|
||||||
|
|
||||||
|
// skip all items for removal which are still saved locally
|
||||||
|
val itemsToRemove = HashMap(currentlySaved)
|
||||||
|
currentlyInstalled.forEach { (packageName, _) -> itemsToRemove.remove(packageName) }
|
||||||
|
|
||||||
|
// only add items which are not the same locally
|
||||||
|
val itemsToAdd = currentlyInstalled.filter { (packageName, app) -> currentlySaved[packageName] != app }
|
||||||
|
|
||||||
|
// save the changes
|
||||||
|
if (itemsToRemove.isNotEmpty() or itemsToAdd.isNotEmpty()) {
|
||||||
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
|
action = UpdateAppActivitiesAction(
|
||||||
|
removedActivities = itemsToRemove.map { it.value.appPackageName to it.value.activityClassName },
|
||||||
|
updatedOrAddedActivities = itemsToAdd.map { item ->
|
||||||
|
AppActivityItem(
|
||||||
|
packageName = item.value.appPackageName,
|
||||||
|
className = item.value.activityClassName,
|
||||||
|
title = item.value.title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
appLogic = appLogic,
|
||||||
|
ignoreIfDeviceIsNotConfigured = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,13 +104,14 @@ class UsedTimeItemBatchUpdateHelper(
|
||||||
// do nothing
|
// do nothing
|
||||||
} else {
|
} else {
|
||||||
ApplyActionUtil.applyAppLogicAction(
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
AddUsedTimeAction(
|
action = AddUsedTimeAction(
|
||||||
categoryId = childCategoryId,
|
categoryId = childCategoryId,
|
||||||
timeToAdd = timeToAdd,
|
timeToAdd = timeToAdd,
|
||||||
dayOfEpoch = date.dayOfEpoch,
|
dayOfEpoch = date.dayOfEpoch,
|
||||||
extraTimeToSubtract = extraTimeToSubtract
|
extraTimeToSubtract = extraTimeToSubtract
|
||||||
),
|
),
|
||||||
logic
|
appLogic = logic,
|
||||||
|
ignoreIfDeviceIsNotConfigured = true
|
||||||
)
|
)
|
||||||
|
|
||||||
timeToAdd = 0
|
timeToAdd = 0
|
||||||
|
|
|
@ -74,6 +74,24 @@ data class RemoveInstalledAppsAction(val packageNames: List<String>): AppLogicAc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class AppActivityItem (
|
||||||
|
val packageName: String,
|
||||||
|
val className: String,
|
||||||
|
val title: String
|
||||||
|
)
|
||||||
|
data class UpdateAppActivitiesAction(
|
||||||
|
// package name to activity class names
|
||||||
|
val removedActivities: List<Pair<String, String>>,
|
||||||
|
val updatedOrAddedActivities: List<AppActivityItem>
|
||||||
|
): AppLogicAction() {
|
||||||
|
init {
|
||||||
|
if (removedActivities.isEmpty() && updatedOrAddedActivities.isEmpty()) {
|
||||||
|
throw IllegalArgumentException("empty action")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object SignOutAtDeviceAction: AppLogicAction()
|
||||||
|
|
||||||
data class AddCategoryAppsAction(val categoryId: String, val packageNames: List<String>): ParentAction() {
|
data class AddCategoryAppsAction(val categoryId: String, val packageNames: List<String>): ParentAction() {
|
||||||
init {
|
init {
|
||||||
IdGenerator.assertIdValid(categoryId)
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
@ -126,6 +144,11 @@ data class UpdateCategoryTemporarilyBlockedAction(val categoryId: String, val bl
|
||||||
IdGenerator.assertIdValid(categoryId)
|
IdGenerator.assertIdValid(categoryId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
data class UpdateCategoryTimeWarningsAction(val categoryId: String, val enable: Boolean, val flags: Int): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
}
|
||||||
|
}
|
||||||
data class SetCategoryForUnassignedApps(val childId: String, val categoryId: String): ParentAction() {
|
data class SetCategoryForUnassignedApps(val childId: String, val categoryId: String): ParentAction() {
|
||||||
// category id can be empty
|
// category id can be empty
|
||||||
|
|
||||||
|
@ -155,16 +178,22 @@ data class UpdateDeviceStatusAction(
|
||||||
val newProtectionLevel: ProtectionLevel?,
|
val newProtectionLevel: ProtectionLevel?,
|
||||||
val newUsageStatsPermissionStatus: RuntimePermissionStatus?,
|
val newUsageStatsPermissionStatus: RuntimePermissionStatus?,
|
||||||
val newNotificationAccessPermission: NewPermissionStatus?,
|
val newNotificationAccessPermission: NewPermissionStatus?,
|
||||||
|
val newOverlayPermission: RuntimePermissionStatus?,
|
||||||
|
val newAccessibilityServiceEnabled: Boolean?,
|
||||||
val newAppVersion: Int?,
|
val newAppVersion: Int?,
|
||||||
val didReboot: Boolean
|
val didReboot: Boolean,
|
||||||
|
val isQOrLaterNow: Boolean
|
||||||
): AppLogicAction() {
|
): AppLogicAction() {
|
||||||
companion object {
|
companion object {
|
||||||
val empty = UpdateDeviceStatusAction(
|
val empty = UpdateDeviceStatusAction(
|
||||||
newProtectionLevel = null,
|
newProtectionLevel = null,
|
||||||
newUsageStatsPermissionStatus = null,
|
newUsageStatsPermissionStatus = null,
|
||||||
newNotificationAccessPermission = null,
|
newNotificationAccessPermission = null,
|
||||||
|
newOverlayPermission = null,
|
||||||
|
newAccessibilityServiceEnabled = null,
|
||||||
newAppVersion = null,
|
newAppVersion = null,
|
||||||
didReboot = false
|
didReboot = false,
|
||||||
|
isQOrLaterNow = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,6 +211,8 @@ data class IgnoreManipulationAction(
|
||||||
val ignoreAppDowngrade: Boolean,
|
val ignoreAppDowngrade: Boolean,
|
||||||
val ignoreNotificationAccessManipulation: Boolean,
|
val ignoreNotificationAccessManipulation: Boolean,
|
||||||
val ignoreUsageStatsAccessManipulation: Boolean,
|
val ignoreUsageStatsAccessManipulation: Boolean,
|
||||||
|
val ignoreOverlayPermissionManipulation: Boolean,
|
||||||
|
val ignoreAccessibilityServiceManipulation: Boolean,
|
||||||
val ignoreReboot: Boolean,
|
val ignoreReboot: Boolean,
|
||||||
val ignoreHadManipulation: Boolean
|
val ignoreHadManipulation: Boolean
|
||||||
): ParentAction() {
|
): ParentAction() {
|
||||||
|
@ -211,18 +242,50 @@ data class SetDeviceUserAction(val deviceId: String, val userId: String): Parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class SetDeviceDefaultUserAction(val deviceId: String, val defaultUserId: String): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(deviceId)
|
||||||
|
|
||||||
|
if (defaultUserId.isNotEmpty()) {
|
||||||
|
IdGenerator.assertIdValid(defaultUserId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SetDeviceDefaultUserTimeoutAction(val deviceId: String, val timeout: Int): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(deviceId)
|
||||||
|
|
||||||
|
if (timeout < 0) {
|
||||||
|
throw IllegalArgumentException("can not set a negative default user timeout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class SetConsiderRebootManipulationAction(val deviceId: String, val considerRebootManipulation: Boolean): ParentAction() {
|
data class SetConsiderRebootManipulationAction(val deviceId: String, val considerRebootManipulation: Boolean): ParentAction() {
|
||||||
init {
|
init {
|
||||||
IdGenerator.assertIdValid(deviceId)
|
IdGenerator.assertIdValid(deviceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class UpdateEnableActivityLevelBlocking(val deviceId: String, val enable: Boolean): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(deviceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class UpdateCategoryBlockedTimesAction(val categoryId: String, val blockedTimes: ImmutableBitmask): ParentAction() {
|
data class UpdateCategoryBlockedTimesAction(val categoryId: String, val blockedTimes: ImmutableBitmask): ParentAction() {
|
||||||
init {
|
init {
|
||||||
IdGenerator.assertIdValid(categoryId)
|
IdGenerator.assertIdValid(categoryId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class UpdateCategoryBlockAllNotificationsAction(val categoryId: String, val blocked: Boolean): ParentAction() {
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class CreateTimeLimitRuleAction(val rule: TimeLimitRule): ParentAction()
|
data class CreateTimeLimitRuleAction(val rule: TimeLimitRule): ParentAction()
|
||||||
|
|
||||||
data class UpdateTimeLimitRuleAction(val ruleId: String, val dayMask: Byte, val maximumTimeInMillis: Int, val applyToExtraTimeUsage: Boolean): ParentAction() {
|
data class UpdateTimeLimitRuleAction(val ruleId: String, val dayMask: Byte, val maximumTimeInMillis: Int, val applyToExtraTimeUsage: Boolean): ParentAction() {
|
||||||
|
|
|
@ -30,14 +30,40 @@ import io.timelimit.android.sync.actions.dispatch.LocalDatabaseAppLogicActionDis
|
||||||
import io.timelimit.android.sync.actions.dispatch.LocalDatabaseParentActionDispatcher
|
import io.timelimit.android.sync.actions.dispatch.LocalDatabaseParentActionDispatcher
|
||||||
|
|
||||||
object ApplyActionUtil {
|
object ApplyActionUtil {
|
||||||
suspend fun applyAppLogicAction(action: AppLogicAction, appLogic: AppLogic) {
|
suspend fun applyAppLogicAction(
|
||||||
applyAppLogicAction(action, appLogic.database, appLogic.manipulationLogic)
|
action: AppLogicAction,
|
||||||
|
appLogic: AppLogic,
|
||||||
|
ignoreIfDeviceIsNotConfigured: Boolean
|
||||||
|
) {
|
||||||
|
applyAppLogicAction(action, appLogic.database, appLogic.manipulationLogic, ignoreIfDeviceIsNotConfigured)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun applyAppLogicAction(action: AppLogicAction, database: Database, manipulationLogic: ManipulationLogic) {
|
private suspend fun applyAppLogicAction(
|
||||||
|
action: AppLogicAction,
|
||||||
|
database: Database,
|
||||||
|
manipulationLogic: ManipulationLogic,
|
||||||
|
ignoreIfDeviceIsNotConfigured: Boolean
|
||||||
|
) {
|
||||||
|
// uncomment this if you need to know what's dispatching an action
|
||||||
|
/*
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
try {
|
||||||
|
throw Exception()
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Log.d(LOG_TAG, "handling action: $action", ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
Threads.database.executeAndWait {
|
Threads.database.executeAndWait {
|
||||||
database.transaction().use {
|
database.transaction().use {
|
||||||
LocalDatabaseAppLogicActionDispatcher.dispatchAppLogicActionSync(action, database.config().getOwnDeviceIdSync()!!, database, manipulationLogic)
|
val ownDeviceId = database.config().getOwnDeviceIdSync()
|
||||||
|
|
||||||
|
if (ownDeviceId == null && ignoreIfDeviceIsNotConfigured) {
|
||||||
|
return@executeAndWait
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDatabaseAppLogicActionDispatcher.dispatchAppLogicActionSync(action, ownDeviceId!!, database, manipulationLogic)
|
||||||
|
|
||||||
database.setTransactionSuccessful()
|
database.setTransactionSuccessful()
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ package io.timelimit.android.sync.actions.dispatch
|
||||||
|
|
||||||
import io.timelimit.android.data.Database
|
import io.timelimit.android.data.Database
|
||||||
import io.timelimit.android.data.model.App
|
import io.timelimit.android.data.model.App
|
||||||
|
import io.timelimit.android.data.model.AppActivity
|
||||||
import io.timelimit.android.data.model.UsedTimeItem
|
import io.timelimit.android.data.model.UsedTimeItem
|
||||||
import io.timelimit.android.integration.platform.NewPermissionStatusUtil
|
import io.timelimit.android.integration.platform.NewPermissionStatusUtil
|
||||||
import io.timelimit.android.integration.platform.ProtectionLevelUtil
|
import io.timelimit.android.integration.platform.ProtectionLevelUtil
|
||||||
|
@ -148,6 +149,42 @@ object LocalDatabaseAppLogicActionDispatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.newOverlayPermission != null) {
|
||||||
|
if (device.currentOverlayPermission != action.newOverlayPermission) {
|
||||||
|
device = device.copy(
|
||||||
|
currentOverlayPermission = action.newOverlayPermission
|
||||||
|
)
|
||||||
|
|
||||||
|
if (RuntimePermissionStatusUtil.toInt(action.newOverlayPermission) > RuntimePermissionStatusUtil.toInt(device.highestOverlayPermission)) {
|
||||||
|
device = device.copy(
|
||||||
|
highestOverlayPermission = action.newOverlayPermission
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.currentOverlayPermission != device.highestOverlayPermission) {
|
||||||
|
device = device.copy(hadManipulation = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.newAccessibilityServiceEnabled != null) {
|
||||||
|
if (device.accessibilityServiceEnabled != action.newAccessibilityServiceEnabled) {
|
||||||
|
device = device.copy(
|
||||||
|
accessibilityServiceEnabled = action.newAccessibilityServiceEnabled
|
||||||
|
)
|
||||||
|
|
||||||
|
if (action.newAccessibilityServiceEnabled) {
|
||||||
|
device = device.copy(
|
||||||
|
wasAccessibilityServiceEnabled = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.accessibilityServiceEnabled != device.wasAccessibilityServiceEnabled) {
|
||||||
|
device = device.copy(hadManipulation = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (action.newAppVersion != null) {
|
if (action.newAppVersion != null) {
|
||||||
if (device.currentAppVersion != action.newAppVersion) {
|
if (device.currentAppVersion != action.newAppVersion) {
|
||||||
device = device.copy(
|
device = device.copy(
|
||||||
|
@ -167,6 +204,10 @@ object LocalDatabaseAppLogicActionDispatcher {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.isQOrLaterNow && !device.qOrLater) {
|
||||||
|
device = device.copy(qOrLater = true)
|
||||||
|
}
|
||||||
|
|
||||||
database.device().updateDeviceEntry(device)
|
database.device().updateDeviceEntry(device)
|
||||||
|
|
||||||
if (device.hasActiveManipulationWarning) {
|
if (device.hasActiveManipulationWarning) {
|
||||||
|
@ -186,6 +227,52 @@ object LocalDatabaseAppLogicActionDispatcher {
|
||||||
|
|
||||||
manipulationLogic.lockDeviceSync()
|
manipulationLogic.lockDeviceSync()
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
|
is SignOutAtDeviceAction -> {
|
||||||
|
val deviceEntry = database.device().getDeviceByIdSync(database.config().getOwnDeviceIdSync()!!)!!
|
||||||
|
|
||||||
|
if (deviceEntry.defaultUser.isEmpty()) {
|
||||||
|
throw IllegalStateException("can not sign out without configured default user")
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDatabaseParentActionDispatcher.dispatchParentActionSync(
|
||||||
|
SetDeviceUserAction(
|
||||||
|
deviceId = deviceEntry.id,
|
||||||
|
userId = deviceEntry.defaultUser
|
||||||
|
),
|
||||||
|
database
|
||||||
|
)
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
|
is UpdateAppActivitiesAction -> {
|
||||||
|
if (action.updatedOrAddedActivities.isNotEmpty()) {
|
||||||
|
database.appActivity().addAppActivitiesSync(
|
||||||
|
action.updatedOrAddedActivities.map { item ->
|
||||||
|
AppActivity(
|
||||||
|
deviceId = deviceId,
|
||||||
|
appPackageName = item.packageName,
|
||||||
|
activityClassName = item.className,
|
||||||
|
title = item.title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.removedActivities.isNotEmpty()) {
|
||||||
|
action.removedActivities.groupBy { it.first }.entries.forEach { item ->
|
||||||
|
val packageName = item.component1()
|
||||||
|
val activities = item.component2().map { it.second }
|
||||||
|
|
||||||
|
database.appActivity().deleteAppActivitiesSync(
|
||||||
|
deviceId = deviceId,
|
||||||
|
packageName = packageName,
|
||||||
|
activities = activities
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}.let { }
|
}.let { }
|
||||||
|
|
|
@ -74,7 +74,9 @@ object LocalDatabaseParentActionDispatcher {
|
||||||
blockedMinutesInWeek = ImmutableBitmask(BitSet()),
|
blockedMinutesInWeek = ImmutableBitmask(BitSet()),
|
||||||
extraTimeInMillis = 0,
|
extraTimeInMillis = 0,
|
||||||
temporarilyBlocked = false,
|
temporarilyBlocked = false,
|
||||||
parentCategoryId = ""
|
parentCategoryId = "",
|
||||||
|
blockAllNotifications = false,
|
||||||
|
timeWarnings = 0
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
is DeleteCategoryAction -> {
|
is DeleteCategoryAction -> {
|
||||||
|
@ -271,6 +273,14 @@ object LocalDatabaseParentActionDispatcher {
|
||||||
deviceEntry = deviceEntry.copy(highestUsageStatsPermission = deviceEntry.currentUsageStatsPermission)
|
deviceEntry = deviceEntry.copy(highestUsageStatsPermission = deviceEntry.currentUsageStatsPermission)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.ignoreOverlayPermissionManipulation) {
|
||||||
|
deviceEntry = deviceEntry.copy(highestOverlayPermission = deviceEntry.currentOverlayPermission)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.ignoreAccessibilityServiceManipulation) {
|
||||||
|
deviceEntry = deviceEntry.copy(wasAccessibilityServiceEnabled = deviceEntry.accessibilityServiceEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
if (action.ignoreReboot) {
|
if (action.ignoreReboot) {
|
||||||
deviceEntry = deviceEntry.copy(manipulationDidReboot = false)
|
deviceEntry = deviceEntry.copy(manipulationDidReboot = false)
|
||||||
}
|
}
|
||||||
|
@ -328,6 +338,26 @@ object LocalDatabaseParentActionDispatcher {
|
||||||
timezone = action.timezone
|
timezone = action.timezone
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is SetDeviceDefaultUserAction -> {
|
||||||
|
if (action.defaultUserId.isNotEmpty()) {
|
||||||
|
DatabaseValidation.assertUserExists(database, action.defaultUserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
DatabaseValidation.assertDeviceExists(database, action.deviceId)
|
||||||
|
|
||||||
|
database.device().updateDeviceDefaultUser(
|
||||||
|
deviceId = action.deviceId,
|
||||||
|
defaultUserId = action.defaultUserId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is SetDeviceDefaultUserTimeoutAction -> {
|
||||||
|
val deviceEntry = database.device().getDeviceByIdSync(action.deviceId)
|
||||||
|
?: throw IllegalArgumentException("device not found")
|
||||||
|
|
||||||
|
database.device().updateDeviceEntry(deviceEntry.copy(
|
||||||
|
defaultUserTimeout = action.timeout
|
||||||
|
))
|
||||||
|
}
|
||||||
is SetConsiderRebootManipulationAction -> {
|
is SetConsiderRebootManipulationAction -> {
|
||||||
val deviceEntry = database.device().getDeviceByIdSync(action.deviceId)
|
val deviceEntry = database.device().getDeviceByIdSync(action.deviceId)
|
||||||
?: throw IllegalArgumentException("device not found")
|
?: throw IllegalArgumentException("device not found")
|
||||||
|
@ -338,6 +368,45 @@ object LocalDatabaseParentActionDispatcher {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is UpdateCategoryBlockAllNotificationsAction -> {
|
||||||
|
val categoryEntry = database.category().getCategoryByIdSync(action.categoryId)
|
||||||
|
?: throw IllegalArgumentException("can not update notification blocking for non exsistent category")
|
||||||
|
|
||||||
|
database.category().updateCategorySync(
|
||||||
|
categoryEntry.copy(
|
||||||
|
blockAllNotifications = action.blocked
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is UpdateEnableActivityLevelBlocking -> {
|
||||||
|
val deviceEntry = database.device().getDeviceByIdSync(action.deviceId)
|
||||||
|
?: throw IllegalArgumentException("device not found")
|
||||||
|
|
||||||
|
database.device().updateDeviceEntry(
|
||||||
|
deviceEntry.copy(
|
||||||
|
enableActivityLevelBlocking = action.enable
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is UpdateCategoryTimeWarningsAction -> {
|
||||||
|
val categoryEntry = database.category().getCategoryByIdSync(action.categoryId)
|
||||||
|
?: throw IllegalArgumentException("category not found")
|
||||||
|
|
||||||
|
val modified = if (action.enable)
|
||||||
|
categoryEntry.copy(
|
||||||
|
timeWarnings = categoryEntry.timeWarnings or action.flags
|
||||||
|
)
|
||||||
|
else
|
||||||
|
categoryEntry.copy(
|
||||||
|
timeWarnings = categoryEntry.timeWarnings and (action.flags.inv())
|
||||||
|
)
|
||||||
|
|
||||||
|
if (modified != categoryEntry) {
|
||||||
|
database.category().updateCategorySync(modified)
|
||||||
|
}
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
}.let { }
|
}.let { }
|
||||||
|
|
||||||
database.setTransactionSuccessful()
|
database.setTransactionSuccessful()
|
||||||
|
|
|
@ -48,6 +48,8 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
||||||
|
|
||||||
private val currentNavigatorFragment = MutableLiveData<Fragment>()
|
private val currentNavigatorFragment = MutableLiveData<Fragment>()
|
||||||
|
|
||||||
|
override var ignoreStop: Boolean = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
@ -104,7 +106,7 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
|
|
||||||
if (!isChangingConfigurations) {
|
if ((!isChangingConfigurations) && (!ignoreStop)) {
|
||||||
getActivityViewModel().logOut()
|
getActivityViewModel().logOut()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,6 +114,10 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(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().popBackStack(R.id.overviewFragment, true)
|
||||||
getNavController().handleDeepLink(
|
getNavController().handleDeepLink(
|
||||||
getNavController().createDeepLink()
|
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) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
* 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.diagnose
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.RadioButton
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.databinding.DiagnoseForegroundAppFragmentBinding
|
||||||
|
import io.timelimit.android.livedata.liveDataFromValue
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
|
import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
|
import io.timelimit.android.util.TimeTextUtil
|
||||||
|
|
||||||
|
class DiagnoseForegroundAppFragment : Fragment() {
|
||||||
|
companion object {
|
||||||
|
private val buttonIntervals = listOf(
|
||||||
|
0,
|
||||||
|
5 * 1000,
|
||||||
|
30 * 1000,
|
||||||
|
60 * 1000,
|
||||||
|
15 * 60 * 1000,
|
||||||
|
60 * 60 * 1000,
|
||||||
|
24 * 60 * 60 * 1000,
|
||||||
|
7 * 24 * 60 * 60 * 1000
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val activity: ActivityViewModelHolder = activity as ActivityViewModelHolder
|
||||||
|
val binding = DiagnoseForegroundAppFragmentBinding.inflate(inflater, container, false)
|
||||||
|
val auth = activity.getActivityViewModel()
|
||||||
|
val logic = DefaultAppLogic.with(context!!)
|
||||||
|
val currentValue = logic.database.config().getForegroundAppQueryIntervalAsync()
|
||||||
|
val currentId = currentValue.map {
|
||||||
|
val res = buttonIntervals.indexOf(it.toInt())
|
||||||
|
|
||||||
|
if (res == -1)
|
||||||
|
0
|
||||||
|
else
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationFab.manageAuthenticationFab(
|
||||||
|
fab = binding.fab,
|
||||||
|
shouldHighlight = auth.shouldHighlightAuthenticationButton,
|
||||||
|
authenticatedUser = auth.authenticatedUser,
|
||||||
|
doesSupportAuth = liveDataFromValue(true),
|
||||||
|
fragment = this
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.fab.setOnClickListener { activity.showAuthenticationScreen() }
|
||||||
|
|
||||||
|
val allButtons = buttonIntervals.mapIndexed { index, interval ->
|
||||||
|
RadioButton(context!!).apply {
|
||||||
|
id = index
|
||||||
|
|
||||||
|
if (interval == 0) {
|
||||||
|
setText(R.string.diagnose_fga_query_range_min)
|
||||||
|
} else if (interval < 60 * 1000) {
|
||||||
|
text = TimeTextUtil.seconds(interval / 1000, context!!)
|
||||||
|
} else {
|
||||||
|
text = TimeTextUtil.time(interval, context!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allButtons.forEach { binding.radioGroup.addView(it) }
|
||||||
|
|
||||||
|
currentId.observe(this, Observer {
|
||||||
|
binding.radioGroup.check(it)
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.radioGroup.setOnCheckedChangeListener { _, checkedId ->
|
||||||
|
val oldId = currentId.value
|
||||||
|
|
||||||
|
if (oldId != null && checkedId != oldId) {
|
||||||
|
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||||
|
val newValue = buttonIntervals[checkedId]
|
||||||
|
|
||||||
|
Threads.database.execute {
|
||||||
|
logic.database.config().setForegroundAppQueryIntervalSync(newValue.toLong())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding.radioGroup.check(oldId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,13 @@ class DiagnoseMainFragment : Fragment() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.diagnoseFgaButton.setOnClickListener {
|
||||||
|
navigation.safeNavigate(
|
||||||
|
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseForegroundAppFragment(),
|
||||||
|
R.id.diagnoseMainFragment
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,12 +32,18 @@ import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
|
class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
|
||||||
companion object {
|
companion object {
|
||||||
private const val EXTRA_PACKAGE_NAME = "packageName"
|
private const val EXTRA_PACKAGE_NAME = "packageName"
|
||||||
|
private const val EXTRA_ACTIVITY_NAME = "activityName"
|
||||||
private const val LOGIN_DIALOG_TAG = "loginDialog"
|
private const val LOGIN_DIALOG_TAG = "loginDialog"
|
||||||
|
|
||||||
fun start(context: Context, packageName: String) {
|
fun start(context: Context, packageName: String, activityName: String?) {
|
||||||
context.startActivity(
|
context.startActivity(
|
||||||
Intent(context, LockActivity::class.java)
|
Intent(context, LockActivity::class.java)
|
||||||
.putExtra(EXTRA_PACKAGE_NAME, packageName)
|
.putExtra(EXTRA_PACKAGE_NAME, packageName)
|
||||||
|
.apply {
|
||||||
|
if (activityName != null) {
|
||||||
|
putExtra(EXTRA_ACTIVITY_NAME, activityName)
|
||||||
|
}
|
||||||
|
}
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||||
|
@ -45,18 +51,29 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var ignoreStop: Boolean = false
|
||||||
|
|
||||||
val blockedPackageName: String by lazy {
|
val blockedPackageName: String by lazy {
|
||||||
intent.getStringExtra(EXTRA_PACKAGE_NAME)
|
intent.getStringExtra(EXTRA_PACKAGE_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val blockedActivityName: String? by lazy {
|
||||||
|
if (intent.hasExtra(EXTRA_ACTIVITY_NAME))
|
||||||
|
intent.getStringExtra(EXTRA_ACTIVITY_NAME)
|
||||||
|
else
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.lock_activity)
|
setContentView(R.layout.lock_activity)
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
.replace(R.id.container, LockFragment.newInstance(blockedPackageName))
|
.replace(R.id.container, LockFragment.newInstance(blockedPackageName, blockedActivityName))
|
||||||
.commitNow()
|
.commitNow()
|
||||||
|
|
||||||
|
stopMediaPlayback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,12 +100,12 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
|
|
||||||
if (!isChangingConfigurations) {
|
if ((!isChangingConfigurations) && (!ignoreStop)) {
|
||||||
getActivityViewModel().logOut()
|
getActivityViewModel().logOut()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun lockTaskModeWorkaround() {
|
private fun lockTaskModeWorkaround() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
val platformIntegration = DefaultAppLogic.with(this).platformIntegration
|
val platformIntegration = DefaultAppLogic.with(this).platformIntegration
|
||||||
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||||
|
@ -105,6 +122,11 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun stopMediaPlayback() {
|
||||||
|
val platformIntegration = DefaultAppLogic.with(this).platformIntegration
|
||||||
|
platformIntegration.muteAudioIfPossible(blockedPackageName)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
// do nothing because going back would open the blocked app again
|
// do nothing because going back would open the blocked app again
|
||||||
// super.onBackPressed()
|
// super.onBackPressed()
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
package io.timelimit.android.ui.lock
|
package io.timelimit.android.ui.lock
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.database.sqlite.SQLiteConstraintException
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -36,10 +37,7 @@ import io.timelimit.android.data.model.User
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
import io.timelimit.android.databinding.LockFragmentBinding
|
import io.timelimit.android.databinding.LockFragmentBinding
|
||||||
import io.timelimit.android.livedata.*
|
import io.timelimit.android.livedata.*
|
||||||
import io.timelimit.android.logic.AppLogic
|
import io.timelimit.android.logic.*
|
||||||
import io.timelimit.android.logic.BlockingReason
|
|
||||||
import io.timelimit.android.logic.BlockingReasonUtil
|
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
|
||||||
import io.timelimit.android.sync.actions.AddCategoryAppsAction
|
import io.timelimit.android.sync.actions.AddCategoryAppsAction
|
||||||
import io.timelimit.android.sync.actions.IncrementCategoryExtraTimeAction
|
import io.timelimit.android.sync.actions.IncrementCategoryExtraTimeAction
|
||||||
import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction
|
import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction
|
||||||
|
@ -50,27 +48,39 @@ import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs
|
import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs
|
||||||
import io.timelimit.android.ui.manage.child.advanced.managedisabletimelimits.ManageDisableTimelimitsViewHelper
|
import io.timelimit.android.ui.manage.child.advanced.managedisabletimelimits.ManageDisableTimelimitsViewHelper
|
||||||
import io.timelimit.android.ui.manage.child.category.create.CreateCategoryDialogFragment
|
import io.timelimit.android.ui.manage.child.category.create.CreateCategoryDialogFragment
|
||||||
|
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
||||||
|
|
||||||
class LockFragment : Fragment() {
|
class LockFragment : Fragment() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val EXTRA_PACKAGE_NAME = "packageName"
|
private const val EXTRA_PACKAGE_NAME = "packageName"
|
||||||
|
private const val EXTRA_ACTIVITY = "activitiy"
|
||||||
|
|
||||||
fun newInstance(packageName: String): LockFragment {
|
fun newInstance(packageName: String, activity: String?): LockFragment {
|
||||||
val result = LockFragment()
|
val result = LockFragment()
|
||||||
val arguments = Bundle()
|
val arguments = Bundle()
|
||||||
|
|
||||||
arguments.putString(EXTRA_PACKAGE_NAME, packageName)
|
arguments.putString(EXTRA_PACKAGE_NAME, packageName)
|
||||||
|
|
||||||
|
if (activity != null) {
|
||||||
|
arguments.putString(EXTRA_ACTIVITY, activity)
|
||||||
|
}
|
||||||
|
|
||||||
result.arguments = arguments
|
result.arguments = arguments
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val packageName: String by lazy { arguments!!.getString(EXTRA_PACKAGE_NAME) }
|
private val packageName: String by lazy { arguments!!.getString(EXTRA_PACKAGE_NAME)!! }
|
||||||
|
private val activityName: String? by lazy {
|
||||||
|
if (arguments!!.containsKey(EXTRA_ACTIVITY))
|
||||||
|
arguments!!.getString(EXTRA_ACTIVITY)
|
||||||
|
else
|
||||||
|
null
|
||||||
|
}
|
||||||
private val auth: ActivityViewModel by lazy { getActivityViewModel(activity!!) }
|
private val auth: ActivityViewModel by lazy { getActivityViewModel(activity!!) }
|
||||||
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
||||||
private val title: String? by lazy { logic.platformIntegration.getLocalAppTitle(packageName) }
|
private val title: String? by lazy { logic.platformIntegration.getLocalAppTitle(packageName) }
|
||||||
private val blockingReason: LiveData<BlockingReason> by lazy { BlockingReasonUtil(logic).getBlockingReason(packageName) }
|
private val blockingReason: LiveData<BlockingReasonDetail> by lazy { BlockingReasonUtil(logic).getBlockingReason(packageName, activityName) }
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
val binding = LockFragmentBinding.inflate(layoutInflater, container, false)
|
val binding = LockFragmentBinding.inflate(layoutInflater, container, false)
|
||||||
|
@ -83,8 +93,14 @@ class LockFragment : Fragment() {
|
||||||
doesSupportAuth = liveDataFromValue(true)
|
doesSupportAuth = liveDataFromValue(true)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val enableActivityLevelBlocking = logic.deviceEntry.map { it?.enableActivityLevelBlocking ?: false }
|
||||||
|
|
||||||
binding.packageName = packageName
|
binding.packageName = packageName
|
||||||
|
|
||||||
|
enableActivityLevelBlocking.observe(this, Observer {
|
||||||
|
binding.activityName = if (it) activityName?.removePrefix(packageName) else null
|
||||||
|
})
|
||||||
|
|
||||||
if (title != null) {
|
if (title != null) {
|
||||||
binding.appTitle = title
|
binding.appTitle = title
|
||||||
} else {
|
} else {
|
||||||
|
@ -94,11 +110,16 @@ class LockFragment : Fragment() {
|
||||||
binding.appIcon.setImageDrawable(logic.platformIntegration.getAppIcon(packageName))
|
binding.appIcon.setImageDrawable(logic.platformIntegration.getAppIcon(packageName))
|
||||||
|
|
||||||
blockingReason.observe(this, Observer {
|
blockingReason.observe(this, Observer {
|
||||||
if (it == BlockingReason.None) {
|
when (it) {
|
||||||
activity!!.finish()
|
is NoBlockingReason -> activity!!.finish()
|
||||||
} else {
|
is BlockedReasonDetails -> {
|
||||||
binding.reason = it
|
binding.reason = it.reason
|
||||||
|
binding.blockedKindLabel = when (it.level) {
|
||||||
|
BlockingLevel.Activity -> "Activity"
|
||||||
|
BlockingLevel.App -> "App"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}.let { /* require handling all cases */ }
|
||||||
})
|
})
|
||||||
|
|
||||||
val categories = logic.deviceUserEntry.switchMap {
|
val categories = logic.deviceUserEntry.switchMap {
|
||||||
|
@ -124,13 +145,14 @@ class LockFragment : Fragment() {
|
||||||
} else {
|
} else {
|
||||||
val (_, categoryItems) = status
|
val (_, categoryItems) = status
|
||||||
|
|
||||||
Transformations.map(logic.database.categoryApp().getCategoryApp(
|
blockingReason.map { reason ->
|
||||||
categoryItems.map { it.id },
|
if (reason is BlockedReasonDetails) {
|
||||||
packageName
|
reason.categoryId
|
||||||
)) {
|
} else {
|
||||||
appEntry ->
|
null
|
||||||
|
}
|
||||||
categoryItems.find { it.id == appEntry?.categoryId }
|
}.map { categoryId ->
|
||||||
|
categoryItems.find { it.id == categoryId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -196,6 +218,8 @@ class LockFragment : Fragment() {
|
||||||
if (extraTimeToAdd > 0) {
|
if (extraTimeToAdd > 0) {
|
||||||
binding.extraTimeBtnOk.isEnabled = false
|
binding.extraTimeBtnOk.isEnabled = false
|
||||||
|
|
||||||
|
binding.extraTimeSelection.clearNumberPickerFocus()
|
||||||
|
|
||||||
val categoryId = appCategory.waitForNullableValue()?.id
|
val categoryId = appCategory.waitForNullableValue()?.id
|
||||||
|
|
||||||
if (categoryId != null) {
|
if (categoryId != null) {
|
||||||
|
@ -215,6 +239,22 @@ class LockFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logic.database.config().getEnableAlternativeDurationSelectionAsync().observe(this, Observer {
|
||||||
|
binding.extraTimeSelection.enablePickerMode(it)
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.extraTimeSelection.listener = object: SelectTimeSpanViewListener {
|
||||||
|
override fun onTimeSpanChanged(newTimeInMillis: Long) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setEnablePickerMode(enable: Boolean) {
|
||||||
|
Threads.database.execute {
|
||||||
|
logic.database.config().setEnableAlternativeDurationSelectionSync(enable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// bind disable time limits
|
// bind disable time limits
|
||||||
logic.deviceUserEntry.observe(this, Observer {
|
logic.deviceUserEntry.observe(this, Observer {
|
||||||
child ->
|
child ->
|
||||||
|
@ -267,9 +307,16 @@ class LockFragment : Fragment() {
|
||||||
logic.platformIntegration.setSuspendedApps(listOf(packageName), false)
|
logic.platformIntegration.setSuspendedApps(listOf(packageName), false)
|
||||||
|
|
||||||
Threads.database.executeAndWait(Runnable {
|
Threads.database.executeAndWait(Runnable {
|
||||||
|
try {
|
||||||
database.temporarilyAllowedApp().addTemporarilyAllowedAppSync(TemporarilyAllowedApp(
|
database.temporarilyAllowedApp().addTemporarilyAllowedAppSync(TemporarilyAllowedApp(
|
||||||
packageName = packageName
|
packageName = packageName
|
||||||
))
|
))
|
||||||
|
} catch (ex: SQLiteConstraintException) {
|
||||||
|
// ignore this
|
||||||
|
//
|
||||||
|
// this happens when touching that option more than once very fast
|
||||||
|
// or if the device is under load
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ class ActivityViewModel(application: Application): AndroidViewModel(application)
|
||||||
private const val LOG_TAG = "ActivityViewModel"
|
private const val LOG_TAG = "ActivityViewModel"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val logic = DefaultAppLogic.with(application)
|
val logic = DefaultAppLogic.with(application)
|
||||||
private val database = logic.database
|
private val database = logic.database
|
||||||
|
|
||||||
val shouldHighlightAuthenticationButton = MutableLiveData<Boolean>().apply { value = false }
|
val shouldHighlightAuthenticationButton = MutableLiveData<Boolean>().apply { value = false }
|
||||||
|
@ -115,6 +115,8 @@ class ActivityViewModel(application: Application): AndroidViewModel(application)
|
||||||
authenticatedUserMetadata.value = user
|
authenticatedUserMetadata.value = user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAuthenticatedUser() = authenticatedUserMetadata.value
|
||||||
|
|
||||||
fun logOut() {
|
fun logOut() {
|
||||||
authenticatedUserMetadata.value = null
|
authenticatedUserMetadata.value = null
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.app.Activity
|
||||||
interface ActivityViewModelHolder {
|
interface ActivityViewModelHolder {
|
||||||
fun getActivityViewModel(): ActivityViewModel
|
fun getActivityViewModel(): ActivityViewModel
|
||||||
fun showAuthenticationScreen()
|
fun showAuthenticationScreen()
|
||||||
|
var ignoreStop: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getActivityViewModel(activity: Activity): ActivityViewModel {
|
fun getActivityViewModel(activity: Activity): ActivityViewModel {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentTransaction
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.navigation.Navigation
|
import androidx.navigation.Navigation
|
||||||
|
@ -35,6 +36,11 @@ import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
import io.timelimit.android.ui.main.AuthenticationFab
|
import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
import io.timelimit.android.ui.manage.category.apps.CategoryAppsFragment
|
||||||
|
import io.timelimit.android.ui.manage.category.blocked_times.BlockedTimeAreasFragment
|
||||||
|
import io.timelimit.android.ui.manage.category.settings.CategorySettingsFragment
|
||||||
|
import io.timelimit.android.ui.manage.category.timelimit_rules.CategoryTimeLimitRulesFragment
|
||||||
|
import io.timelimit.android.ui.manage.category.usagehistory.UsageHistoryFragment
|
||||||
import kotlinx.android.synthetic.main.fragment_manage_category.*
|
import kotlinx.android.synthetic.main.fragment_manage_category.*
|
||||||
|
|
||||||
class ManageCategoryFragment : Fragment(), FragmentWithCustomTitle {
|
class ManageCategoryFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
|
@ -47,7 +53,6 @@ class ManageCategoryFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
private val user: LiveData<User?> by lazy {
|
private val user: LiveData<User?> by lazy {
|
||||||
logic.database.user().getUserByIdLive(params.childId)
|
logic.database.user().getUserByIdLive(params.childId)
|
||||||
}
|
}
|
||||||
private val adapter: PagerAdapter by lazy { PagerAdapter(childFragmentManager, params) }
|
|
||||||
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||||
private var wereViewsCreated = false
|
private var wereViewsCreated = false
|
||||||
|
|
||||||
|
@ -70,44 +75,29 @@ class ManageCategoryFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
|
|
||||||
val navigation = Navigation.findNavController(view)
|
val navigation = Navigation.findNavController(view)
|
||||||
|
|
||||||
pager.adapter = adapter
|
bottom_navigation_view.setOnNavigationItemReselectedListener { /* ignore */ }
|
||||||
|
bottom_navigation_view.setOnNavigationItemSelectedListener { menuItem ->
|
||||||
bottom_navigation_view.setOnNavigationItemSelectedListener {
|
childFragmentManager.beginTransaction()
|
||||||
menuItem ->
|
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||||
|
.replace(R.id.container, when(menuItem.itemId) {
|
||||||
pager.currentItem = when(menuItem.itemId) {
|
R.id.manage_category_tab_apps -> CategoryAppsFragment.newInstance(params)
|
||||||
R.id.manage_category_tab_apps -> 0
|
R.id.manage_category_tab_time_limit_rules -> CategoryTimeLimitRulesFragment.newInstance(params)
|
||||||
R.id.manage_category_tab_time_limit_rules -> 1
|
R.id.manage_category_tab_blocked_time_areas -> BlockedTimeAreasFragment.newInstance(params)
|
||||||
R.id.manage_category_tab_blocked_time_areas -> 2
|
R.id.manage_category_tab_usage_log -> UsageHistoryFragment.newInstance(params)
|
||||||
R.id.manage_category_tab_usage_log -> 3
|
R.id.manage_category_tab_settings -> CategorySettingsFragment.newInstance(params)
|
||||||
R.id.manage_category_tab_settings -> 4
|
else -> throw IllegalStateException()
|
||||||
else -> 0
|
})
|
||||||
}
|
.commit()
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pager.addOnPageChangeListener(object: ViewPager.OnPageChangeListener {
|
if (childFragmentManager.findFragmentById(R.id.container) == null) {
|
||||||
override fun onPageScrollStateChanged(state: Int) {
|
childFragmentManager.beginTransaction()
|
||||||
// ignore
|
.replace(R.id.container, CategoryAppsFragment.newInstance(params))
|
||||||
|
.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageSelected(position: Int) {
|
|
||||||
bottom_navigation_view.selectedItemId = when(position) {
|
|
||||||
0 -> R.id.manage_category_tab_apps
|
|
||||||
1 -> R.id.manage_category_tab_time_limit_rules
|
|
||||||
2 -> R.id.manage_category_tab_blocked_time_areas
|
|
||||||
3 -> R.id.manage_category_tab_usage_log
|
|
||||||
4 -> R.id.manage_category_tab_settings
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!wereViewsCreated) {
|
if (!wereViewsCreated) {
|
||||||
wereViewsCreated = true
|
wereViewsCreated = true
|
||||||
|
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
/*
|
|
||||||
* Open 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.manage.category
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.fragment.app.FragmentStatePagerAdapter
|
|
||||||
import io.timelimit.android.ui.manage.category.apps.CategoryAppsFragment
|
|
||||||
import io.timelimit.android.ui.manage.category.blocked_times.BlockedTimeAreasFragment
|
|
||||||
import io.timelimit.android.ui.manage.category.settings.CategorySettingsFragment
|
|
||||||
import io.timelimit.android.ui.manage.category.timelimit_rules.CategoryTimeLimitRulesFragment
|
|
||||||
import io.timelimit.android.ui.manage.category.usagehistory.UsageHistoryFragment
|
|
||||||
|
|
||||||
class PagerAdapter(fragmentManager: FragmentManager, private val params: ManageCategoryFragmentArgs): FragmentStatePagerAdapter(fragmentManager) {
|
|
||||||
override fun getCount() = 5
|
|
||||||
|
|
||||||
override fun getItem(position: Int): Fragment = when (position) {
|
|
||||||
0 -> CategoryAppsFragment.newInstance(params)
|
|
||||||
1 -> CategoryTimeLimitRulesFragment.newInstance(params)
|
|
||||||
2 -> BlockedTimeAreasFragment.newInstance(params)
|
|
||||||
3 -> UsageHistoryFragment.newInstance(params)
|
|
||||||
4 -> CategorySettingsFragment.newInstance(params)
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -102,7 +102,7 @@ class AppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
||||||
|
|
||||||
binding.icon.setImageDrawable(
|
binding.icon.setImageDrawable(
|
||||||
DefaultAppLogic.with(binding.root.context)
|
DefaultAppLogic.with(binding.root.context)
|
||||||
.platformIntegration.getAppIcon(item.packageName)
|
.platformIntegration.getAppIcon(item.packageNameWithoutActivityName)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@ class AppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
||||||
open class ViewHolder(view: View): RecyclerView.ViewHolder(view)
|
open class ViewHolder(view: View): RecyclerView.ViewHolder(view)
|
||||||
class AppViewHolder(val binding: FragmentCategoryAppsItemBinding): ViewHolder(binding.root)
|
class AppViewHolder(val binding: FragmentCategoryAppsItemBinding): ViewHolder(binding.root)
|
||||||
|
|
||||||
data class AppEntry(val title: String, val packageName: String)
|
data class AppEntry(val title: String, val packageName: String, val packageNameWithoutActivityName: String)
|
||||||
|
|
||||||
interface Handlers {
|
interface Handlers {
|
||||||
fun onAppClicked(app: AppEntry)
|
fun onAppClicked(app: AppEntry)
|
||||||
|
|
|
@ -42,7 +42,7 @@ class CategoryAppsModel(application: Application): AndroidViewModel(application)
|
||||||
private val appsOfCategoryWithNames = installedApps.switchMap { allApps ->
|
private val appsOfCategoryWithNames = installedApps.switchMap { allApps ->
|
||||||
appsOfThisCategory.map { apps ->
|
appsOfThisCategory.map { apps ->
|
||||||
apps.map { categoryApp ->
|
apps.map { categoryApp ->
|
||||||
categoryApp to allApps.find { app -> app.packageName == categoryApp.packageName }
|
categoryApp to allApps.find { app -> app.packageName == categoryApp.packageNameWithoutActivityName }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,9 +50,9 @@ class CategoryAppsModel(application: Application): AndroidViewModel(application)
|
||||||
val appEntries = appsOfCategoryWithNames.map { apps ->
|
val appEntries = appsOfCategoryWithNames.map { apps ->
|
||||||
apps.map { (app, appEntry) ->
|
apps.map { (app, appEntry) ->
|
||||||
if (appEntry != null) {
|
if (appEntry != null) {
|
||||||
AppEntry(appEntry.title, app.packageName)
|
AppEntry(appEntry.title, app.packageName, app.packageNameWithoutActivityName)
|
||||||
} else {
|
} else {
|
||||||
AppEntry("app not found", app.packageName)
|
AppEntry("app not found", app.packageName, app.packageNameWithoutActivityName)
|
||||||
}
|
}
|
||||||
}.sortedBy { it.title.toLowerCase(Locale.US) }
|
}.sortedBy { it.title.toLowerCase(Locale.US) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import kotlin.properties.Delegates
|
||||||
|
|
||||||
class AddAppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
class AddAppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
||||||
var data: List<App>? by Delegates.observable(null as List<App>?) { _, _, _ -> notifyDataSetChanged() }
|
var data: List<App>? by Delegates.observable(null as List<App>?) { _, _, _ -> notifyDataSetChanged() }
|
||||||
|
var listener: AddAppAdapterListener? = null
|
||||||
var categoryTitleByPackageName: Map<String, String> by Delegates.observable(emptyMap()) { _, _, _ -> notifyDataSetChanged() }
|
var categoryTitleByPackageName: Map<String, String> by Delegates.observable(emptyMap()) { _, _, _ -> notifyDataSetChanged() }
|
||||||
val selectedApps = mutableSetOf<String>()
|
val selectedApps = mutableSetOf<String>()
|
||||||
|
|
||||||
|
@ -35,6 +36,8 @@ class AddAppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
||||||
|
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onAppLongClicked(app: App) = listener?.onAppLongClicked(app) ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -86,6 +89,10 @@ class AddAppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
||||||
|
|
||||||
class ViewHolder(val binding: FragmentAddCategoryAppsItemBinding): RecyclerView.ViewHolder(binding.root)
|
class ViewHolder(val binding: FragmentAddCategoryAppsItemBinding): RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
interface ItemHandlers {
|
interface ItemHandlers: AddAppAdapterListener {
|
||||||
fun onAppClicked(app: App)
|
fun onAppClicked(app: App)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AddAppAdapterListener {
|
||||||
|
fun onAppLongClicked(app: App): Boolean
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package io.timelimit.android.ui.manage.category.apps.add
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
|
@ -27,6 +28,7 @@ import androidx.lifecycle.Observer
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.Database
|
import io.timelimit.android.data.Database
|
||||||
|
import io.timelimit.android.data.model.App
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
import io.timelimit.android.databinding.FragmentAddCategoryAppsBinding
|
import io.timelimit.android.databinding.FragmentAddCategoryAppsBinding
|
||||||
import io.timelimit.android.extensions.showSafe
|
import io.timelimit.android.extensions.showSafe
|
||||||
|
@ -39,6 +41,7 @@ import io.timelimit.android.sync.actions.AddCategoryAppsAction
|
||||||
import io.timelimit.android.ui.main.ActivityViewModel
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
import io.timelimit.android.ui.main.getActivityViewModel
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
import io.timelimit.android.ui.manage.category.ManageCategoryFragmentArgs
|
import io.timelimit.android.ui.manage.category.ManageCategoryFragmentArgs
|
||||||
|
import io.timelimit.android.ui.manage.category.apps.addactivity.AddAppActivitiesDialogFragment
|
||||||
import io.timelimit.android.ui.view.AppFilterView
|
import io.timelimit.android.ui.view.AppFilterView
|
||||||
|
|
||||||
class AddCategoryAppsFragment : DialogFragment() {
|
class AddCategoryAppsFragment : DialogFragment() {
|
||||||
|
@ -168,6 +171,26 @@ class AddCategoryAppsFragment : DialogFragment() {
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adapter.listener = object: AddAppAdapterListener {
|
||||||
|
override fun onAppLongClicked(app: App): Boolean {
|
||||||
|
return if (adapter.selectedApps.isEmpty()) {
|
||||||
|
AddAppActivitiesDialogFragment.newInstance(
|
||||||
|
childId = params.childId,
|
||||||
|
categoryId = params.categoryId,
|
||||||
|
packageName = app.packageName
|
||||||
|
).show(fragmentManager!!)
|
||||||
|
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
Toast.makeText(context, R.string.category_apps_add_dialog_cannot_add_activities_already_sth_selected, Toast.LENGTH_LONG).show()
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return AlertDialog.Builder(context!!, R.style.AppTheme)
|
return AlertDialog.Builder(context!!, R.style.AppTheme)
|
||||||
.setView(binding.root)
|
.setView(binding.root)
|
||||||
.create()
|
.create()
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
* 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.manage.category.apps.addactivity
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.model.UserType
|
||||||
|
import io.timelimit.android.databinding.FragmentAddCategoryActivitiesBinding
|
||||||
|
import io.timelimit.android.extensions.addOnTextChangedListener
|
||||||
|
import io.timelimit.android.extensions.showSafe
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
import io.timelimit.android.livedata.switchMap
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.sync.actions.AddCategoryAppsAction
|
||||||
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
|
|
||||||
|
class AddAppActivitiesDialogFragment: DialogFragment() {
|
||||||
|
companion object {
|
||||||
|
private const val DIALOG_TAG = "AddAppActivitiesDialogFragment"
|
||||||
|
private const val CHILD_ID = "childId"
|
||||||
|
private const val CATEGORY_ID = "categoryId"
|
||||||
|
private const val PACKAGE_NAME = "packageName"
|
||||||
|
private const val SELECTED_ACTIVITIES = "selectedActivities"
|
||||||
|
|
||||||
|
fun newInstance(childId: String, categoryId: String, packageName: String) = AddAppActivitiesDialogFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putString(CHILD_ID, childId)
|
||||||
|
putString(CATEGORY_ID, categoryId)
|
||||||
|
putString(PACKAGE_NAME, packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val adapter = AddAppActivityAdapter()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
adapter.selectedActiviities.clear()
|
||||||
|
savedInstanceState.getStringArray(SELECTED_ACTIVITIES)!!.forEach { adapter.selectedActiviities.add(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
|
outState.putStringArray(SELECTED_ACTIVITIES, adapter.selectedActiviities.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val appPackageName = arguments!!.getString(PACKAGE_NAME)!!
|
||||||
|
val categoryId = arguments!!.getString(CATEGORY_ID)!!
|
||||||
|
val auth = getActivityViewModel(activity!!)
|
||||||
|
val binding = FragmentAddCategoryActivitiesBinding.inflate(LayoutInflater.from(context!!))
|
||||||
|
val searchTerm = MutableLiveData<String>().apply { value = binding.search.text.toString() }
|
||||||
|
binding.search.addOnTextChangedListener { searchTerm.value = binding.search.text.toString() }
|
||||||
|
|
||||||
|
auth.authenticatedUser.observe(this, Observer {
|
||||||
|
if (it?.second?.type != UserType.Parent) {
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
val logic = DefaultAppLogic.with(context!!)
|
||||||
|
val allActivities = logic.database.appActivity().getAppActivitiesByPackageName(appPackageName).map { activities ->
|
||||||
|
activities.distinctBy { it.activityClassName }
|
||||||
|
}
|
||||||
|
val filteredActivities = allActivities.switchMap { activities ->
|
||||||
|
searchTerm.map { term ->
|
||||||
|
if (term.isEmpty()) {
|
||||||
|
activities
|
||||||
|
} else {
|
||||||
|
activities.filter { it.activityClassName.contains(term, ignoreCase = true) or it.title.contains(term, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.recycler.layoutManager = LinearLayoutManager(context!!)
|
||||||
|
binding.recycler.adapter = adapter
|
||||||
|
|
||||||
|
filteredActivities.observe(this, Observer { list ->
|
||||||
|
val selectedActivities = adapter.selectedActiviities
|
||||||
|
val visibleActivities = list.map { it.activityClassName }
|
||||||
|
val hiddenSelectedActivities = selectedActivities.toMutableSet().apply { removeAll(visibleActivities) }.size
|
||||||
|
|
||||||
|
adapter.data = list
|
||||||
|
binding.hiddenEntries = if (hiddenSelectedActivities == 0)
|
||||||
|
null
|
||||||
|
else
|
||||||
|
resources.getQuantityString(R.plurals.category_apps_add_dialog_hidden_entries, hiddenSelectedActivities, hiddenSelectedActivities)
|
||||||
|
})
|
||||||
|
|
||||||
|
val emptyViewText = allActivities.switchMap { all ->
|
||||||
|
filteredActivities.map { filtered ->
|
||||||
|
if (filtered.isNotEmpty())
|
||||||
|
null
|
||||||
|
else if (all.isNotEmpty())
|
||||||
|
getString(R.string.category_apps_add_activity_empty_filtered)
|
||||||
|
else /* (all.isEmpty()) */
|
||||||
|
getString(R.string.category_apps_add_activity_empty_unfiltered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyViewText.observe(this, Observer {
|
||||||
|
binding.emptyViewText = it
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.cancelButton.setOnClickListener { dismissAllowingStateLoss() }
|
||||||
|
binding.addActivitiesButton.setOnClickListener {
|
||||||
|
if (adapter.selectedActiviities.isNotEmpty()) {
|
||||||
|
auth.tryDispatchParentAction(AddCategoryAppsAction(
|
||||||
|
categoryId = categoryId,
|
||||||
|
packageNames = adapter.selectedActiviities.toList().map { "$appPackageName:$it" }
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
|
||||||
|
return AlertDialog.Builder(context!!, R.style.AppTheme)
|
||||||
|
.setView(binding.root)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* 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.manage.category.apps.addactivity
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import io.timelimit.android.data.model.App
|
||||||
|
import io.timelimit.android.data.model.AppActivity
|
||||||
|
import io.timelimit.android.databinding.FragmentAddCategoryActivitiesItemBinding
|
||||||
|
import io.timelimit.android.databinding.FragmentAddCategoryAppsItemBinding
|
||||||
|
import io.timelimit.android.extensions.toggle
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
class AddAppActivityAdapter: RecyclerView.Adapter<ViewHolder>() {
|
||||||
|
var data: List<AppActivity>? by Delegates.observable(null as List<AppActivity>?) { _, _, _ -> notifyDataSetChanged() }
|
||||||
|
val selectedActiviities = mutableSetOf<String>()
|
||||||
|
|
||||||
|
private val itemHandlers = object: ItemHandlers {
|
||||||
|
override fun onActivityClicked(activity: AppActivity) {
|
||||||
|
selectedActiviities.toggle(activity.activityClassName)
|
||||||
|
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
setHasStableIds(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getItem(position: Int): AppActivity {
|
||||||
|
return data!![position]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemId(position: Int): Long {
|
||||||
|
return getItem(position).activityClassName.hashCode().toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = this.data?.size ?: 0
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||||
|
FragmentAddCategoryActivitiesItemBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
).apply { handlers = itemHandlers }
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val item = getItem(position)
|
||||||
|
|
||||||
|
holder.apply {
|
||||||
|
binding.item = item
|
||||||
|
binding.checked = selectedActiviities.contains(item.activityClassName)
|
||||||
|
binding.executePendingBindings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewHolder(val binding: FragmentAddCategoryActivitiesItemBinding): RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
|
interface ItemHandlers {
|
||||||
|
fun onActivityClicked(activity: AppActivity)
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Open 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.manage.category.settings
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import io.timelimit.android.data.model.Category
|
||||||
|
import io.timelimit.android.databinding.CategoryNotificationFilterBinding
|
||||||
|
import io.timelimit.android.sync.actions.UpdateCategoryBlockAllNotificationsAction
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
|
||||||
|
object CategoryNotificationFilter {
|
||||||
|
fun bind(
|
||||||
|
view: CategoryNotificationFilterBinding,
|
||||||
|
auth: ActivityViewModel,
|
||||||
|
categoryLive: LiveData<Category?>,
|
||||||
|
lifecycleOwner: LifecycleOwner
|
||||||
|
) {
|
||||||
|
categoryLive.observe(lifecycleOwner, Observer { category ->
|
||||||
|
val shouldBeChecked = category?.blockAllNotifications ?: false
|
||||||
|
|
||||||
|
view.checkbox.setOnCheckedChangeListener { _, _ -> }
|
||||||
|
view.checkbox.isChecked = shouldBeChecked
|
||||||
|
view.checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
if (isChecked != shouldBeChecked) {
|
||||||
|
if (
|
||||||
|
category != null &&
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
UpdateCategoryBlockAllNotificationsAction(
|
||||||
|
categoryId = category.id,
|
||||||
|
blocked = isChecked
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// ok
|
||||||
|
} else {
|
||||||
|
view.checkbox.isChecked = shouldBeChecked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
import io.timelimit.android.databinding.FragmentCategorySettingsBinding
|
import io.timelimit.android.databinding.FragmentCategorySettingsBinding
|
||||||
import io.timelimit.android.logic.AppLogic
|
import io.timelimit.android.logic.AppLogic
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
@ -30,6 +31,7 @@ import io.timelimit.android.sync.actions.SetCategoryExtraTimeAction
|
||||||
import io.timelimit.android.ui.main.ActivityViewModel
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
import io.timelimit.android.ui.main.getActivityViewModel
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
import io.timelimit.android.ui.manage.category.ManageCategoryFragmentArgs
|
import io.timelimit.android.ui.manage.category.ManageCategoryFragmentArgs
|
||||||
|
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
||||||
|
|
||||||
class CategorySettingsFragment : Fragment() {
|
class CategorySettingsFragment : Fragment() {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -68,6 +70,20 @@ class CategorySettingsFragment : Fragment() {
|
||||||
auth = auth
|
auth = auth
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CategoryNotificationFilter.bind(
|
||||||
|
view = binding.notificationFilter,
|
||||||
|
lifecycleOwner = this,
|
||||||
|
auth = auth,
|
||||||
|
categoryLive = categoryEntry
|
||||||
|
)
|
||||||
|
|
||||||
|
CategoryTimeWarningView.bind(
|
||||||
|
view = binding.timeWarnings,
|
||||||
|
auth = auth,
|
||||||
|
categoryLive = categoryEntry,
|
||||||
|
lifecycleOwner = this
|
||||||
|
)
|
||||||
|
|
||||||
binding.btnDeleteCategory.setOnClickListener { deleteCategory() }
|
binding.btnDeleteCategory.setOnClickListener { deleteCategory() }
|
||||||
binding.editCategoryTitleGo.setOnClickListener { renameCategory() }
|
binding.editCategoryTitleGo.setOnClickListener { renameCategory() }
|
||||||
|
|
||||||
|
@ -82,6 +98,8 @@ class CategorySettingsFragment : Fragment() {
|
||||||
})
|
})
|
||||||
|
|
||||||
binding.extraTimeBtnOk.setOnClickListener {
|
binding.extraTimeBtnOk.setOnClickListener {
|
||||||
|
binding.extraTimeSelection.clearNumberPickerFocus()
|
||||||
|
|
||||||
val newExtraTime = binding.extraTimeSelection.timeInMillis
|
val newExtraTime = binding.extraTimeSelection.timeInMillis
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -96,6 +114,22 @@ class CategorySettingsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appLogic.database.config().getEnableAlternativeDurationSelectionAsync().observe(this, Observer {
|
||||||
|
binding.extraTimeSelection.enablePickerMode(it)
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.extraTimeSelection.listener = object: SelectTimeSpanViewListener {
|
||||||
|
override fun onTimeSpanChanged(newTimeInMillis: Long) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setEnablePickerMode(enable: Boolean) {
|
||||||
|
Threads.database.execute {
|
||||||
|
appLogic.database.config().setEnableAlternativeDurationSelectionSync(enable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
package io.timelimit.android.ui.manage.category.settings
|
||||||
|
|
||||||
|
import android.widget.CheckBox
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import io.timelimit.android.data.model.Category
|
||||||
|
import io.timelimit.android.data.model.CategoryTimeWarnings
|
||||||
|
import io.timelimit.android.databinding.CategoryTimeWarningsViewBinding
|
||||||
|
import io.timelimit.android.sync.actions.UpdateCategoryTimeWarningsAction
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
import io.timelimit.android.util.TimeTextUtil
|
||||||
|
|
||||||
|
object CategoryTimeWarningView {
|
||||||
|
fun bind(
|
||||||
|
view: CategoryTimeWarningsViewBinding,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
categoryLive: LiveData<Category?>,
|
||||||
|
auth: ActivityViewModel
|
||||||
|
) {
|
||||||
|
view.linearLayout.removeAllViews()
|
||||||
|
|
||||||
|
val durationToCheckbox = mutableMapOf<Long, CheckBox>()
|
||||||
|
|
||||||
|
CategoryTimeWarnings.durations.sorted().forEach { duration ->
|
||||||
|
CheckBox(view.root.context).let { checkbox ->
|
||||||
|
checkbox.text = TimeTextUtil.time(duration.toInt(), view.root.context)
|
||||||
|
|
||||||
|
view.linearLayout.addView(checkbox)
|
||||||
|
durationToCheckbox[duration] = checkbox
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryLive.observe(lifecycleOwner, Observer { category ->
|
||||||
|
durationToCheckbox.entries.forEach { (duration, checkbox) ->
|
||||||
|
checkbox.setOnCheckedChangeListener { _, _ -> }
|
||||||
|
|
||||||
|
val flag = (1 shl CategoryTimeWarnings.durationToBitIndex[duration]!!)
|
||||||
|
val enable = (category?.timeWarnings ?: 0) and flag != 0
|
||||||
|
checkbox.isChecked = enable
|
||||||
|
|
||||||
|
checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
if (isChecked != enable && category != null) {
|
||||||
|
if (auth.tryDispatchParentAction(
|
||||||
|
UpdateCategoryTimeWarningsAction(
|
||||||
|
categoryId = category.id,
|
||||||
|
enable = isChecked,
|
||||||
|
flags = flag
|
||||||
|
)
|
||||||
|
)) {
|
||||||
|
// it worked
|
||||||
|
} else {
|
||||||
|
checkbox.isChecked = enable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,17 +26,22 @@ import androidx.lifecycle.Observer
|
||||||
import com.google.android.material.R
|
import com.google.android.material.R
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.coroutines.runAsync
|
||||||
import io.timelimit.android.data.IdGenerator
|
import io.timelimit.android.data.IdGenerator
|
||||||
|
import io.timelimit.android.data.model.HintsToShow
|
||||||
import io.timelimit.android.data.model.TimeLimitRule
|
import io.timelimit.android.data.model.TimeLimitRule
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
import io.timelimit.android.databinding.FragmentEditTimeLimitRuleDialogBinding
|
import io.timelimit.android.databinding.FragmentEditTimeLimitRuleDialogBinding
|
||||||
import io.timelimit.android.extensions.showSafe
|
import io.timelimit.android.extensions.showSafe
|
||||||
|
import io.timelimit.android.livedata.waitForNonNullValue
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.sync.actions.CreateTimeLimitRuleAction
|
import io.timelimit.android.sync.actions.CreateTimeLimitRuleAction
|
||||||
import io.timelimit.android.sync.actions.DeleteTimeLimitRuleAction
|
import io.timelimit.android.sync.actions.DeleteTimeLimitRuleAction
|
||||||
import io.timelimit.android.sync.actions.UpdateTimeLimitRuleAction
|
import io.timelimit.android.sync.actions.UpdateTimeLimitRuleAction
|
||||||
import io.timelimit.android.ui.main.ActivityViewModel
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
import io.timelimit.android.ui.main.getActivityViewModel
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
|
import io.timelimit.android.ui.mustread.MustReadFragment
|
||||||
import io.timelimit.android.ui.view.SelectDayViewHandlers
|
import io.timelimit.android.ui.view.SelectDayViewHandlers
|
||||||
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
@ -84,6 +89,23 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
val database = DefaultAppLogic.with(context!!).database
|
||||||
|
|
||||||
|
runAsync {
|
||||||
|
val wasShown = database.config().wereHintsShown(HintsToShow.TIMELIMIT_RULE_MUSTREAD).waitForNonNullValue()
|
||||||
|
|
||||||
|
if (!wasShown) {
|
||||||
|
MustReadFragment.newInstance(io.timelimit.android.R.string.must_read_timelimit_rules).show(fragmentManager!!)
|
||||||
|
|
||||||
|
Threads.database.execute {
|
||||||
|
database.config().setHintsShownSync(HintsToShow.TIMELIMIT_RULE_MUSTREAD)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
existingRule = savedInstanceState?.getParcelable(PARAM_EXISTING_RULE)
|
existingRule = savedInstanceState?.getParcelable(PARAM_EXISTING_RULE)
|
||||||
?: arguments?.getParcelable<TimeLimitRule?>(PARAM_EXISTING_RULE)
|
?: arguments?.getParcelable<TimeLimitRule?>(PARAM_EXISTING_RULE)
|
||||||
}
|
}
|
||||||
|
@ -92,6 +114,7 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
||||||
val view = FragmentEditTimeLimitRuleDialogBinding.inflate(layoutInflater, container, false)
|
val view = FragmentEditTimeLimitRuleDialogBinding.inflate(layoutInflater, container, false)
|
||||||
val listener = targetFragment as EditTimeLimitRuleDialogFragmentListener
|
val listener = targetFragment as EditTimeLimitRuleDialogFragmentListener
|
||||||
var newRule: TimeLimitRule
|
var newRule: TimeLimitRule
|
||||||
|
val database = DefaultAppLogic.with(context!!).database
|
||||||
|
|
||||||
auth.authenticatedUser.observe(this, Observer {
|
auth.authenticatedUser.observe(this, Observer {
|
||||||
if (it == null || it.second.type != UserType.Parent) {
|
if (it == null || it.second.type != UserType.Parent) {
|
||||||
|
@ -135,7 +158,7 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
||||||
view.timeSpan.timeInMillis = newRule.maximumTimeInMillis.toLong()
|
view.timeSpan.timeInMillis = newRule.maximumTimeInMillis.toLong()
|
||||||
|
|
||||||
val affectedDays = Math.max(0, (0..6).map { (newRule.dayMask.toInt() shr it) and 1 }.sum())
|
val affectedDays = Math.max(0, (0..6).map { (newRule.dayMask.toInt() shr it) and 1 }.sum())
|
||||||
view.timeSpan.maxDays = affectedDays - 1
|
view.timeSpan.maxDays = Math.max(0, affectedDays - 1) // max prevents crash
|
||||||
view.affectsMultipleDays = affectedDays >= 2
|
view.affectsMultipleDays = affectedDays >= 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,6 +183,8 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveRule() {
|
override fun onSaveRule() {
|
||||||
|
view.timeSpan.clearNumberPickerFocus()
|
||||||
|
|
||||||
if (existingRule != null) {
|
if (existingRule != null) {
|
||||||
if (existingRule != newRule) {
|
if (existingRule != newRule) {
|
||||||
if (!auth.tryDispatchParentAction(
|
if (!auth.tryDispatchParentAction(
|
||||||
|
@ -213,10 +238,20 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
||||||
bindRule()
|
bindRule()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setEnablePickerMode(enable: Boolean) {
|
||||||
|
Threads.database.execute {
|
||||||
|
database.config().setEnableAlternativeDurationSelectionSync(enable)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
database.config().getEnableAlternativeDurationSelectionAsync().observe(this, Observer {
|
||||||
|
view.timeSpan.enablePickerMode(it)
|
||||||
|
})
|
||||||
|
|
||||||
if (existingRule != null) {
|
if (existingRule != null) {
|
||||||
DefaultAppLogic.with(context!!).database.timeLimitRules()
|
database.timeLimitRules()
|
||||||
.getTimeLimitRuleByIdLive(existingRule!!.id).observe(this, Observer {
|
.getTimeLimitRuleByIdLive(existingRule!!.id).observe(this, Observer {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
// rule was deleted
|
// rule was deleted
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentTransaction
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.navigation.Navigation
|
import androidx.navigation.Navigation
|
||||||
|
@ -34,11 +35,13 @@ import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
import io.timelimit.android.ui.main.AuthenticationFab
|
import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
import io.timelimit.android.ui.manage.child.advanced.ManageChildAdvancedFragment
|
||||||
|
import io.timelimit.android.ui.manage.child.apps.ChildAppsFragment
|
||||||
|
import io.timelimit.android.ui.manage.child.category.ManageChildCategoriesFragment
|
||||||
import kotlinx.android.synthetic.main.fragment_manage_child.*
|
import kotlinx.android.synthetic.main.fragment_manage_child.*
|
||||||
|
|
||||||
class ManageChildFragment : Fragment(), FragmentWithCustomTitle {
|
class ManageChildFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
private val params: ManageChildFragmentArgs by lazy { ManageChildFragmentArgs.fromBundle(arguments!!) }
|
private val params: ManageChildFragmentArgs by lazy { ManageChildFragmentArgs.fromBundle(arguments!!) }
|
||||||
private val adapter: PagerAdapter by lazy { PagerAdapter(childFragmentManager, params) }
|
|
||||||
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
||||||
private val child: LiveData<User?> by lazy { logic.database.user().getUserByIdLive(params.childId) }
|
private val child: LiveData<User?> by lazy { logic.database.user().getUserByIdLive(params.childId) }
|
||||||
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||||
|
@ -74,39 +77,26 @@ class ManageChildFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pager.adapter = adapter
|
bottom_navigation_view.setOnNavigationItemReselectedListener { /* ignore */ }
|
||||||
|
bottom_navigation_view.setOnNavigationItemSelectedListener { menuItem ->
|
||||||
bottom_navigation_view.setOnNavigationItemSelectedListener {
|
childFragmentManager.beginTransaction()
|
||||||
menuItem ->
|
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||||
|
.replace(R.id.container, when (menuItem.itemId) {
|
||||||
pager.currentItem = when (menuItem.itemId) {
|
R.id.manage_child_tab_categories -> ManageChildCategoriesFragment.newInstance(params)
|
||||||
R.id.manage_child_tab_categories -> 0
|
R.id.manage_child_tab_apps -> ChildAppsFragment.newInstance(params)
|
||||||
R.id.manage_child_tab_apps -> 1
|
R.id.manage_child_tab_manage -> ManageChildAdvancedFragment.newInstance(params)
|
||||||
R.id.manage_child_tab_manage -> 2
|
else -> throw IllegalStateException()
|
||||||
else -> 0
|
})
|
||||||
}
|
.commit()
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pager.addOnPageChangeListener(object: ViewPager.OnPageChangeListener {
|
if (childFragmentManager.findFragmentById(R.id.container) == null) {
|
||||||
override fun onPageScrollStateChanged(state: Int) {
|
childFragmentManager.beginTransaction()
|
||||||
// ignore
|
.replace(R.id.container, ManageChildCategoriesFragment.newInstance(params))
|
||||||
|
.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageSelected(position: Int) {
|
|
||||||
bottom_navigation_view.selectedItemId = when(position) {
|
|
||||||
0 -> R.id.manage_child_tab_categories
|
|
||||||
1 -> R.id.manage_child_tab_apps
|
|
||||||
2 -> R.id.manage_child_tab_manage
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCustomTitle() = child.map { it?.name }
|
override fun getCustomTitle() = child.map { it?.name }
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
/*
|
|
||||||
* Open 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.manage.child
|
|
||||||
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.fragment.app.FragmentStatePagerAdapter
|
|
||||||
import io.timelimit.android.ui.manage.child.advanced.ManageChildAdvancedFragment
|
|
||||||
import io.timelimit.android.ui.manage.child.apps.ChildAppsFragment
|
|
||||||
import io.timelimit.android.ui.manage.child.category.ManageChildCategoriesFragment
|
|
||||||
|
|
||||||
class PagerAdapter(fragmentManager: FragmentManager, private val params: ManageChildFragmentArgs): FragmentStatePagerAdapter(fragmentManager) {
|
|
||||||
override fun getCount() = 3
|
|
||||||
|
|
||||||
override fun getItem(position: Int) = when(position) {
|
|
||||||
0 -> ManageChildCategoriesFragment.newInstance(params)
|
|
||||||
1 -> ChildAppsFragment.newInstance(params)
|
|
||||||
2 -> ManageChildAdvancedFragment.newInstance(params)
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -91,7 +91,10 @@ class ManageChildCategoriesFragment : Fragment() {
|
||||||
|
|
||||||
ItemTouchHelper(object: ItemTouchHelper.Callback() {
|
ItemTouchHelper(object: ItemTouchHelper.Callback() {
|
||||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||||
if (adapter.categories!![viewHolder.adapterPosition] == CategoriesIntroductionHeader) {
|
val index = viewHolder.adapterPosition
|
||||||
|
val item = if (index == RecyclerView.NO_POSITION) null else adapter.categories!![index]
|
||||||
|
|
||||||
|
if (item == CategoriesIntroductionHeader) {
|
||||||
return makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.END) or
|
return makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.END) or
|
||||||
makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END)
|
makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* 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.manage.device.manage
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.model.Device
|
||||||
|
import io.timelimit.android.data.model.User
|
||||||
|
import io.timelimit.android.data.model.UserType
|
||||||
|
import io.timelimit.android.databinding.MissingPermissionViewBinding
|
||||||
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
|
import io.timelimit.android.livedata.mergeLiveData
|
||||||
|
|
||||||
|
object ActivityLaunchPermissionRequiredAndMissing {
|
||||||
|
fun bind(
|
||||||
|
view: MissingPermissionViewBinding,
|
||||||
|
user: LiveData<User?>,
|
||||||
|
device: LiveData<Device?>,
|
||||||
|
lifecycleOwner: LifecycleOwner
|
||||||
|
) {
|
||||||
|
view.title = view.root.context.getString(R.string.activity_launch_permission_required_and_missing_title)
|
||||||
|
|
||||||
|
mergeLiveData(user, device).observe(lifecycleOwner, Observer { (user, device) ->
|
||||||
|
view.showMessage = user?.type == UserType.Child && device?.missingPermissionAtQOrLater ?: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,6 +35,7 @@ import androidx.navigation.Navigation
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.model.Device
|
import io.timelimit.android.data.model.Device
|
||||||
import io.timelimit.android.databinding.FragmentManageDeviceBinding
|
import io.timelimit.android.databinding.FragmentManageDeviceBinding
|
||||||
|
import io.timelimit.android.extensions.safeNavigate
|
||||||
import io.timelimit.android.integration.platform.ProtectionLevel
|
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||||
import io.timelimit.android.integration.platform.android.AdminReceiver
|
import io.timelimit.android.integration.platform.android.AdminReceiver
|
||||||
import io.timelimit.android.livedata.liveDataFromValue
|
import io.timelimit.android.livedata.liveDataFromValue
|
||||||
|
@ -48,6 +49,8 @@ import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
import io.timelimit.android.ui.main.AuthenticationFab
|
import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
import io.timelimit.android.ui.manage.device.manage.feature.ManageDeviceFeaturesFragment
|
||||||
|
import io.timelimit.android.ui.manage.device.manage.permission.ManageDevicePermissionsFragment
|
||||||
|
|
||||||
class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||||
|
@ -70,10 +73,6 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
activityViewModel = auth
|
activityViewModel = auth
|
||||||
)
|
)
|
||||||
|
|
||||||
val userSpinnerAdapter = ArrayAdapter<String>(context!!, android.R.layout.simple_spinner_item).apply {
|
|
||||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
|
||||||
}
|
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
AuthenticationFab.manageAuthenticationFab(
|
AuthenticationFab.manageAuthenticationFab(
|
||||||
fab = binding.fab,
|
fab = binding.fab,
|
||||||
|
@ -83,89 +82,41 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
doesSupportAuth = liveDataFromValue(true)
|
doesSupportAuth = liveDataFromValue(true)
|
||||||
)
|
)
|
||||||
|
|
||||||
// label, id
|
|
||||||
val userListItems = ArrayList<Pair<String, String>>()
|
|
||||||
|
|
||||||
fun bindUserListItems() {
|
|
||||||
userSpinnerAdapter.clear()
|
|
||||||
userSpinnerAdapter.addAll(userListItems.map { it.first })
|
|
||||||
userSpinnerAdapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bindUserListSelection() {
|
|
||||||
val selectedUserId = deviceEntry.value?.currentUserId
|
|
||||||
|
|
||||||
val selectedIndex = userListItems.indexOfFirst { it.second == selectedUserId }
|
|
||||||
|
|
||||||
if (selectedIndex != -1) {
|
|
||||||
binding.userSpinner.setSelection(selectedIndex)
|
|
||||||
} else {
|
|
||||||
val fallbackSelectedIndex = userListItems.indexOfFirst { it.second == "" }
|
|
||||||
|
|
||||||
if (fallbackSelectedIndex != -1) {
|
|
||||||
binding.userSpinner.setSelection(fallbackSelectedIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.handlers = object: ManageDeviceFragmentHandlers {
|
binding.handlers = object: ManageDeviceFragmentHandlers {
|
||||||
override fun openUsageStatsSettings() {
|
override fun showUserScreen() {
|
||||||
if (binding.isThisDevice == true) {
|
navigation.safeNavigate(
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDeviceUserFragment(
|
||||||
startActivity(
|
deviceId = args.deviceId
|
||||||
Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
|
),
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
R.id.manageDeviceFragment
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openNotificationAccessSettings() {
|
override fun showPermissionsScreen() {
|
||||||
if (binding.isThisDevice == true) {
|
navigation.safeNavigate(
|
||||||
try {
|
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDevicePermissionsFragment(
|
||||||
startActivity(
|
deviceId = args.deviceId
|
||||||
Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")
|
),
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
R.id.manageDeviceFragment
|
||||||
)
|
)
|
||||||
} catch (ex: Exception) {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
R.string.error_general,
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun manageDeviceAdmin() {
|
override fun showFeaturesScreen() {
|
||||||
if (binding.isThisDevice == true) {
|
navigation.safeNavigate(
|
||||||
val protectionLevel = logic.platformIntegration.getCurrentProtectionLevel()
|
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDeviceFeaturesFragment(
|
||||||
|
deviceId = args.deviceId
|
||||||
if (protectionLevel == ProtectionLevel.None) {
|
),
|
||||||
if (InformAboutDeviceOwnerDialogFragment.shouldShow) {
|
R.id.manageDeviceFragment
|
||||||
startActivity(
|
|
||||||
Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN)
|
|
||||||
.putExtra(
|
|
||||||
DevicePolicyManager.EXTRA_DEVICE_ADMIN,
|
|
||||||
ComponentName(context!!, AdminReceiver::class.java)
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
InformAboutDeviceOwnerDialogFragment().show(fragmentManager!!)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
startActivity(
|
|
||||||
Intent(Settings.ACTION_SECURITY_SETTINGS)
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun editDeviceTitle() {
|
override fun showManageScreen() {
|
||||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
navigation.safeNavigate(
|
||||||
UpdateDeviceTitleDialogFragment.newInstance(args.deviceId).show(fragmentManager!!)
|
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDeviceAdvancedFragment(
|
||||||
}
|
deviceId = args.deviceId
|
||||||
|
),
|
||||||
|
R.id.manageDeviceFragment
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showAuthenticationScreen() {
|
override fun showAuthenticationScreen() {
|
||||||
|
@ -173,32 +124,6 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.userSpinner.adapter = userSpinnerAdapter
|
|
||||||
binding.userSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
|
|
||||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
|
||||||
val item = userListItems[position]
|
|
||||||
val userId = item.second
|
|
||||||
val device = deviceEntry.value
|
|
||||||
|
|
||||||
if (device != null) {
|
|
||||||
if (device.currentUserId != userId) {
|
|
||||||
if (!auth.tryDispatchParentAction(
|
|
||||||
SetDeviceUserAction(
|
|
||||||
deviceId = args.deviceId,
|
|
||||||
userId = userId
|
|
||||||
)
|
|
||||||
)) {
|
|
||||||
bindUserListSelection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
|
||||||
// nothing to do
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceEntry.observe(this, Observer {
|
deviceEntry.observe(this, Observer {
|
||||||
device ->
|
device ->
|
||||||
|
|
||||||
|
@ -207,7 +132,6 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
} else {
|
} else {
|
||||||
val now = logic.timeApi.getCurrentTimeInMillis()
|
val now = logic.timeApi.getCurrentTimeInMillis()
|
||||||
|
|
||||||
binding.deviceTitle = device.name
|
|
||||||
binding.modelString = device.model
|
binding.modelString = device.model
|
||||||
binding.addedAtString = getString(R.string.manage_device_added_at, DateUtils.getRelativeTimeSpanString(
|
binding.addedAtString = getString(R.string.manage_device_added_at, DateUtils.getRelativeTimeSpanString(
|
||||||
device.addedAt,
|
device.addedAt,
|
||||||
|
@ -215,25 +139,9 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
DateUtils.HOUR_IN_MILLIS
|
DateUtils.HOUR_IN_MILLIS
|
||||||
|
|
||||||
))
|
))
|
||||||
binding.usageStatsAccess = device.currentUsageStatsPermission
|
|
||||||
binding.notificationAccessPermission = device.currentNotificationAccessPermission
|
|
||||||
binding.protectionLevel = device.currentProtectionLevel
|
|
||||||
binding.didAppDowngrade = device.currentAppVersion < device.highestAppVersion
|
binding.didAppDowngrade = device.currentAppVersion < device.highestAppVersion
|
||||||
}
|
binding.permissionCardText = ManageDevicePermissionsFragment.getPreviewText(device, context!!)
|
||||||
})
|
binding.featureCardText = ManageDeviceFeaturesFragment.getPreviewText(device, context!!)
|
||||||
|
|
||||||
mergeLiveData(deviceEntry, userEntries).observe(this, Observer {
|
|
||||||
val (device, users) = it!!
|
|
||||||
|
|
||||||
if (device != null && users != null) {
|
|
||||||
userListItems.clear()
|
|
||||||
userListItems.addAll(
|
|
||||||
users.map { user -> Pair(user.name, user.id) }
|
|
||||||
)
|
|
||||||
userListItems.add(Pair(getString(R.string.manage_device_current_user_none), ""))
|
|
||||||
|
|
||||||
bindUserListItems()
|
|
||||||
bindUserListSelection()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -264,35 +172,27 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
user = userEntry
|
user = userEntry
|
||||||
)
|
)
|
||||||
|
|
||||||
ManageDeviceTroubleshooting.bind(
|
ActivityLaunchPermissionRequiredAndMissing.bind(
|
||||||
view = binding.troubleshootingView,
|
view = binding.activityLaunchPermissionMissing,
|
||||||
userEntry = userEntry,
|
lifecycleOwner = this,
|
||||||
lifecycleOwner = this
|
device = deviceEntry,
|
||||||
|
user = userEntry
|
||||||
)
|
)
|
||||||
|
|
||||||
ManageDeviceRebootManipulationView.bind(
|
userEntry.observe(this, Observer {
|
||||||
view = binding.deviceRebootManipulation,
|
binding.userCardText = it?.name ?: getString(R.string.manage_device_current_user_none)
|
||||||
lifecycleOwner = this,
|
})
|
||||||
deviceEntry = deviceEntry,
|
|
||||||
auth = auth
|
|
||||||
)
|
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
logic.backgroundTaskLogic.syncDeviceStatusAsync()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCustomTitle() = deviceEntry.map { it?.name }
|
override fun getCustomTitle() = deviceEntry.map { it?.name }
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ManageDeviceFragmentHandlers {
|
interface ManageDeviceFragmentHandlers {
|
||||||
fun openUsageStatsSettings()
|
fun showUserScreen()
|
||||||
fun openNotificationAccessSettings()
|
fun showPermissionsScreen()
|
||||||
fun manageDeviceAdmin()
|
fun showFeaturesScreen()
|
||||||
fun editDeviceTitle()
|
fun showManageScreen()
|
||||||
fun showAuthenticationScreen()
|
fun showAuthenticationScreen()
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,8 @@ object ManageDeviceManipulation {
|
||||||
binding.hasManipulatedDeviceAdmin = device?.manipulationOfProtectionLevel ?: false
|
binding.hasManipulatedDeviceAdmin = device?.manipulationOfProtectionLevel ?: false
|
||||||
binding.hasManipulatedUsageStatsAccess = device?.manipulationOfUsageStats ?: false
|
binding.hasManipulatedUsageStatsAccess = device?.manipulationOfUsageStats ?: false
|
||||||
binding.hasManipulatedNotificationAccess = device?.manipulationOfNotificationAccess ?: false
|
binding.hasManipulatedNotificationAccess = device?.manipulationOfNotificationAccess ?: false
|
||||||
|
binding.hasManipulatedOverlayPermission = device?.manipulationOfOverlayPermission ?: false
|
||||||
|
binding.hasManipulatedAccessibilityService = device?.manipulationOfAccessibilityService ?: false
|
||||||
binding.hasManipulationReboot = device?.manipulationDidReboot ?: false
|
binding.hasManipulationReboot = device?.manipulationDidReboot ?: false
|
||||||
binding.hasHadManipulation = (device?.hadManipulation ?: false) and (! (device?.hasActiveManipulationWarning ?: false))
|
binding.hasHadManipulation = (device?.hadManipulation ?: false) and (! (device?.hasActiveManipulationWarning ?: false))
|
||||||
binding.hasAnyManipulation = device?.hasAnyManipulation ?: false
|
binding.hasAnyManipulation = device?.hasAnyManipulation ?: false
|
||||||
|
@ -62,6 +64,8 @@ object ManageDeviceManipulation {
|
||||||
binding.deviceAdminDisabledCheckbox,
|
binding.deviceAdminDisabledCheckbox,
|
||||||
binding.usageAccessCheckbox,
|
binding.usageAccessCheckbox,
|
||||||
binding.notificationAccessCheckbox,
|
binding.notificationAccessCheckbox,
|
||||||
|
binding.overlayPermissionCheckbox,
|
||||||
|
binding.accessibilityServiceCheckbox,
|
||||||
binding.rebootCheckbox,
|
binding.rebootCheckbox,
|
||||||
binding.hadManipulationCheckbox
|
binding.hadManipulationCheckbox
|
||||||
)
|
)
|
||||||
|
@ -80,6 +84,8 @@ object ManageDeviceManipulation {
|
||||||
ignoreNotificationAccessManipulation = binding.notificationAccessCheckbox.isChecked && binding.hasManipulatedNotificationAccess == true,
|
ignoreNotificationAccessManipulation = binding.notificationAccessCheckbox.isChecked && binding.hasManipulatedNotificationAccess == true,
|
||||||
ignoreDeviceAdminManipulationAttempt = binding.deviceAdminDisableAttemptCheckbox.isChecked && binding.hasTriedManipulatingDeviceAdmin == true,
|
ignoreDeviceAdminManipulationAttempt = binding.deviceAdminDisableAttemptCheckbox.isChecked && binding.hasTriedManipulatingDeviceAdmin == true,
|
||||||
ignoreDeviceAdminManipulation = binding.deviceAdminDisabledCheckbox.isChecked && binding.hasManipulatedDeviceAdmin == true,
|
ignoreDeviceAdminManipulation = binding.deviceAdminDisabledCheckbox.isChecked && binding.hasManipulatedDeviceAdmin == true,
|
||||||
|
ignoreOverlayPermissionManipulation = binding.overlayPermissionCheckbox.isChecked && binding.hasManipulatedOverlayPermission == true,
|
||||||
|
ignoreAccessibilityServiceManipulation = binding.accessibilityServiceCheckbox.isChecked && binding.hasManipulatedAccessibilityService == true,
|
||||||
ignoreAppDowngrade = binding.appVersionCheckbox.isChecked && binding.hasManipulatedAppVersion == true,
|
ignoreAppDowngrade = binding.appVersionCheckbox.isChecked && binding.hasManipulatedAppVersion == true,
|
||||||
ignoreReboot = binding.rebootCheckbox.isChecked && binding.hasManipulationReboot == true,
|
ignoreReboot = binding.rebootCheckbox.isChecked && binding.hasManipulationReboot == true,
|
||||||
ignoreHadManipulation = binding.hadManipulationCheckbox.isChecked || (
|
ignoreHadManipulation = binding.hadManipulationCheckbox.isChecked || (
|
||||||
|
|
|
@ -18,20 +18,23 @@ package io.timelimit.android.ui.manage.device.manage
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.model.Device
|
import io.timelimit.android.data.model.Device
|
||||||
import io.timelimit.android.data.model.User
|
import io.timelimit.android.data.model.User
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
import io.timelimit.android.databinding.UsageStatsPermissionRequiredAndMissingBinding
|
import io.timelimit.android.databinding.MissingPermissionViewBinding
|
||||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
import io.timelimit.android.livedata.mergeLiveData
|
import io.timelimit.android.livedata.mergeLiveData
|
||||||
|
|
||||||
object UsageStatsAccessRequiredAndMissing {
|
object UsageStatsAccessRequiredAndMissing {
|
||||||
fun bind(
|
fun bind(
|
||||||
view: UsageStatsPermissionRequiredAndMissingBinding,
|
view: MissingPermissionViewBinding,
|
||||||
user: LiveData<User?>,
|
user: LiveData<User?>,
|
||||||
device: LiveData<Device?>,
|
device: LiveData<Device?>,
|
||||||
lifecycleOwner: LifecycleOwner
|
lifecycleOwner: LifecycleOwner
|
||||||
) {
|
) {
|
||||||
|
view.title = view.root.context.getString(R.string.usage_stats_permission_required_and_missing_title)
|
||||||
|
|
||||||
mergeLiveData(user, device).observe(lifecycleOwner, Observer { (user, device) ->
|
mergeLiveData(user, device).observe(lifecycleOwner, Observer { (user, device) ->
|
||||||
view.showMessage = user?.type == UserType.Child && device?.currentUsageStatsPermission == RuntimePermissionStatus.NotGranted
|
view.showMessage = user?.type == UserType.Child && device?.currentUsageStatsPermission == RuntimePermissionStatus.NotGranted
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* 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.manage.device.manage.advanced
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import io.timelimit.android.databinding.ManageDeviceViewBinding
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
|
||||||
|
object ManageDevice {
|
||||||
|
fun bind(
|
||||||
|
view: ManageDeviceViewBinding,
|
||||||
|
activityViewModel: ActivityViewModel,
|
||||||
|
fragmentManager: FragmentManager,
|
||||||
|
deviceId: String
|
||||||
|
) {
|
||||||
|
view.renameBtn.setOnClickListener {
|
||||||
|
if (activityViewModel.requestAuthenticationOrReturnTrue()) {
|
||||||
|
UpdateDeviceTitleDialogFragment.newInstance(deviceId).show(fragmentManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* 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.manage.device.manage.advanced
|
||||||
|
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.navigation.Navigation
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.model.Device
|
||||||
|
import io.timelimit.android.data.model.User
|
||||||
|
import io.timelimit.android.databinding.ManageDeviceAdvancedFragmentBinding
|
||||||
|
import io.timelimit.android.livedata.ignoreUnchanged
|
||||||
|
import io.timelimit.android.livedata.liveDataFromValue
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
import io.timelimit.android.livedata.switchMap
|
||||||
|
import io.timelimit.android.logic.AppLogic
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
|
import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
|
||||||
|
class ManageDeviceAdvancedFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
|
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||||
|
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
||||||
|
private val auth: ActivityViewModel by lazy { activity.getActivityViewModel() }
|
||||||
|
private val args: ManageDeviceAdvancedFragmentArgs by lazy { ManageDeviceAdvancedFragmentArgs.fromBundle(arguments!!) }
|
||||||
|
private val deviceEntry: LiveData<Device?> by lazy {
|
||||||
|
logic.database.device().getDeviceById(args.deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val binding = ManageDeviceAdvancedFragmentBinding.inflate(inflater, container, false)
|
||||||
|
val navigation = Navigation.findNavController(container!!)
|
||||||
|
|
||||||
|
val userEntry = deviceEntry.switchMap { device ->
|
||||||
|
device?.currentUserId?.let { userId ->
|
||||||
|
logic.database.user().getUserByIdLive(userId)
|
||||||
|
} ?: liveDataFromValue(null as User?)
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationFab.manageAuthenticationFab(
|
||||||
|
fab = binding.fab,
|
||||||
|
shouldHighlight = auth.shouldHighlightAuthenticationButton,
|
||||||
|
authenticatedUser = auth.authenticatedUser,
|
||||||
|
fragment = this,
|
||||||
|
doesSupportAuth = liveDataFromValue(true)
|
||||||
|
)
|
||||||
|
|
||||||
|
ManageDevice.bind(
|
||||||
|
view = binding.manageDevice,
|
||||||
|
activityViewModel = auth,
|
||||||
|
fragmentManager = fragmentManager!!,
|
||||||
|
deviceId = args.deviceId
|
||||||
|
)
|
||||||
|
|
||||||
|
ManageDeviceTroubleshooting.bind(
|
||||||
|
view = binding.troubleshootingView,
|
||||||
|
userEntry = userEntry,
|
||||||
|
lifecycleOwner = this
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.handlers = object: ManageDeviceAdvancedFragmentHandlers {
|
||||||
|
override fun showAuthenticationScreen() {
|
||||||
|
activity.showAuthenticationScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceEntry.observe(this, Observer { device ->
|
||||||
|
if (device == null) {
|
||||||
|
navigation.popBackStack(R.id.overviewFragment, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCustomTitle() = deviceEntry.map { it?.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ManageDeviceAdvancedFragmentHandlers {
|
||||||
|
fun showAuthenticationScreen()
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.ui.manage.device.manage
|
package io.timelimit.android.ui.manage.device.manage.advanced
|
||||||
|
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.ui.manage.device.manage
|
package io.timelimit.android.ui.manage.device.manage.advanced
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* 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.manage.device.manage.defaultuser
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.coroutines.runAsync
|
||||||
|
import io.timelimit.android.data.model.Device
|
||||||
|
import io.timelimit.android.data.model.User
|
||||||
|
import io.timelimit.android.databinding.ManageDeviceDefaultUserBinding
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
import io.timelimit.android.livedata.switchMap
|
||||||
|
import io.timelimit.android.sync.actions.SignOutAtDeviceAction
|
||||||
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
import io.timelimit.android.util.TimeTextUtil
|
||||||
|
|
||||||
|
object ManageDeviceDefaultUser {
|
||||||
|
fun bind(
|
||||||
|
view: ManageDeviceDefaultUserBinding,
|
||||||
|
users: LiveData<List<User>>,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
device: LiveData<Device?>,
|
||||||
|
isThisDevice: LiveData<Boolean>,
|
||||||
|
auth: ActivityViewModel,
|
||||||
|
fragmentManager: FragmentManager
|
||||||
|
) {
|
||||||
|
val context = view.root.context
|
||||||
|
|
||||||
|
device.switchMap { deviceEntry ->
|
||||||
|
users.map { users ->
|
||||||
|
deviceEntry to users.find { it.id == deviceEntry?.defaultUser }
|
||||||
|
}
|
||||||
|
}.observe(lifecycleOwner, Observer { (deviceEntry, defaultUser) ->
|
||||||
|
view.hasDefaultUser = defaultUser != null
|
||||||
|
view.isAlreadyUsingDefaultUser = defaultUser != null && deviceEntry?.currentUserId == defaultUser.id
|
||||||
|
view.defaultUserTitle = defaultUser?.name
|
||||||
|
})
|
||||||
|
|
||||||
|
isThisDevice.observe(lifecycleOwner, Observer {
|
||||||
|
view.isCurrentDevice = it
|
||||||
|
})
|
||||||
|
|
||||||
|
device.observe(lifecycleOwner, Observer { deviceEntry ->
|
||||||
|
view.setDefaultUserButton.setOnClickListener {
|
||||||
|
if (deviceEntry != null && auth.requestAuthenticationOrReturnTrue()) {
|
||||||
|
SetDeviceDefaultUserDialogFragment.newInstance(
|
||||||
|
deviceId = deviceEntry.id
|
||||||
|
).show(fragmentManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.configureAutoLogoutButton.setOnClickListener {
|
||||||
|
if (deviceEntry != null && auth.requestAuthenticationOrReturnTrue()) {
|
||||||
|
SetDeviceDefaultUserTimeoutDialogFragment
|
||||||
|
.newInstance(deviceId = deviceEntry.id)
|
||||||
|
.show(fragmentManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultUserTimeout = deviceEntry?.defaultUserTimeout ?: 0
|
||||||
|
|
||||||
|
view.isAutomaticallySwitchingToDefaultUserEnabled = defaultUserTimeout != 0
|
||||||
|
view.defaultUserSwitchText = if (defaultUserTimeout == 0)
|
||||||
|
context.getString(R.string.manage_device_default_user_timeout_off)
|
||||||
|
else
|
||||||
|
context.getString(
|
||||||
|
R.string.manage_device_default_user_timeout_on,
|
||||||
|
if (defaultUserTimeout < 1000 * 60)
|
||||||
|
TimeTextUtil.seconds(defaultUserTimeout / 1000, context)
|
||||||
|
else
|
||||||
|
TimeTextUtil.time(defaultUserTimeout, context)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
view.switchToDefaultUserButton.setOnClickListener {
|
||||||
|
runAsync {
|
||||||
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
|
action = SignOutAtDeviceAction,
|
||||||
|
appLogic = auth.logic,
|
||||||
|
ignoreIfDeviceIsNotConfigured = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
/*
|
||||||
|
* 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.manage.device.manage.defaultuser
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.CheckedTextView
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.Database
|
||||||
|
import io.timelimit.android.data.model.UserType
|
||||||
|
import io.timelimit.android.databinding.BottomSheetSelectionListBinding
|
||||||
|
import io.timelimit.android.extensions.showSafe
|
||||||
|
import io.timelimit.android.livedata.ignoreUnchanged
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
import io.timelimit.android.livedata.switchMap
|
||||||
|
import io.timelimit.android.logic.AppLogic
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.sync.actions.SetDeviceDefaultUserAction
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
|
|
||||||
|
class SetDeviceDefaultUserDialogFragment: BottomSheetDialogFragment() {
|
||||||
|
companion object {
|
||||||
|
private const val EXTRA_DEVICE_ID = "deviceId"
|
||||||
|
private const val DIALOG_TAG = "sddudf"
|
||||||
|
|
||||||
|
fun newInstance(deviceId: String) = SetDeviceDefaultUserDialogFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putString(EXTRA_DEVICE_ID, deviceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val deviceId: String by lazy { arguments!!.getString(EXTRA_DEVICE_ID) }
|
||||||
|
val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
||||||
|
val database: Database by lazy { logic.database }
|
||||||
|
val auth: ActivityViewModel by lazy { (activity as ActivityViewModelHolder).getActivityViewModel() }
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
auth.authenticatedUser.observe(this, Observer {
|
||||||
|
if (it?.second?.type != UserType.Parent) {
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val binding = BottomSheetSelectionListBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
binding.title = getString(R.string.manage_device_default_user_title)
|
||||||
|
|
||||||
|
val list = binding.list
|
||||||
|
val users = database.user().getAllUsersLive()
|
||||||
|
val deviceEntry = database.device().getDeviceById(deviceId)
|
||||||
|
val currentDefaultUserId = deviceEntry.map { it?.defaultUser }.ignoreUnchanged()
|
||||||
|
|
||||||
|
currentDefaultUserId.switchMap { v1 ->
|
||||||
|
users.map { v2 -> v1 to v2 }
|
||||||
|
}.observe(this, Observer { (defaultUserId, userList) ->
|
||||||
|
list.removeAllViews()
|
||||||
|
|
||||||
|
fun buildRow(): CheckedTextView = LayoutInflater.from(context!!).inflate(
|
||||||
|
android.R.layout.simple_list_item_single_choice,
|
||||||
|
list,
|
||||||
|
false
|
||||||
|
) as CheckedTextView
|
||||||
|
|
||||||
|
val hasDefaultUser = userList.find { it.id == defaultUserId } != null
|
||||||
|
|
||||||
|
userList.forEach { user ->
|
||||||
|
buildRow().let { row ->
|
||||||
|
row.text = user.name
|
||||||
|
row.isChecked = defaultUserId == user.id
|
||||||
|
row.setOnClickListener {
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
SetDeviceDefaultUserAction(
|
||||||
|
deviceId = deviceId,
|
||||||
|
defaultUserId = user.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
list.addView(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildRow().let { row ->
|
||||||
|
row.setText(R.string.manage_device_default_user_selection_none)
|
||||||
|
row.isChecked = !hasDefaultUser
|
||||||
|
row.setOnClickListener {
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
SetDeviceDefaultUserAction(
|
||||||
|
deviceId = deviceId,
|
||||||
|
defaultUserId = ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
list.addView(row)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* 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.manage.device.manage.defaultuser
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.CheckedTextView
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.model.Device
|
||||||
|
import io.timelimit.android.data.model.UserType
|
||||||
|
import io.timelimit.android.databinding.BottomSheetSelectionListBinding
|
||||||
|
import io.timelimit.android.extensions.showSafe
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.sync.actions.SetDeviceDefaultUserTimeoutAction
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
|
import io.timelimit.android.util.TimeTextUtil
|
||||||
|
|
||||||
|
class SetDeviceDefaultUserTimeoutDialogFragment: BottomSheetDialogFragment() {
|
||||||
|
companion object {
|
||||||
|
private const val EXTRA_DEVICE_ID = "deviceId"
|
||||||
|
private const val DIALOG_TAG = "sddutdf"
|
||||||
|
private val OPTIONS = listOf(
|
||||||
|
0,
|
||||||
|
1000 * 5,
|
||||||
|
1000 * 60,
|
||||||
|
1000 * 60 * 5,
|
||||||
|
1000 * 60 * 15,
|
||||||
|
1000 * 60 * 30,
|
||||||
|
1000 * 60 * 60
|
||||||
|
)
|
||||||
|
|
||||||
|
fun newInstance(deviceId: String) = SetDeviceDefaultUserTimeoutDialogFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putString(EXTRA_DEVICE_ID, deviceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val deviceId: String by lazy { arguments!!.getString(EXTRA_DEVICE_ID) }
|
||||||
|
val deviceEntry: LiveData<Device?> by lazy {
|
||||||
|
DefaultAppLogic.with(context!!).database.device().getDeviceById(deviceId)
|
||||||
|
}
|
||||||
|
val auth: ActivityViewModel by lazy { getActivityViewModel(activity!!) }
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
auth.authenticatedUser.observe(this, Observer {
|
||||||
|
if (it?.second?.type != UserType.Parent) {
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
deviceEntry.observe(this, Observer {
|
||||||
|
if (it == null) {
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val binding = BottomSheetSelectionListBinding.inflate(inflater, container, false)
|
||||||
|
binding.title = getString(R.string.manage_device_default_user_timeout_dialog_title)
|
||||||
|
val list = binding.list
|
||||||
|
|
||||||
|
deviceEntry.observe(this, Observer { device ->
|
||||||
|
val timeout = device?.defaultUserTimeout ?: 0
|
||||||
|
|
||||||
|
fun buildRow(): CheckedTextView = LayoutInflater.from(context!!).inflate(
|
||||||
|
android.R.layout.simple_list_item_single_choice,
|
||||||
|
list,
|
||||||
|
false
|
||||||
|
) as CheckedTextView
|
||||||
|
|
||||||
|
list.removeAllViews()
|
||||||
|
|
||||||
|
OPTIONS.forEach { option ->
|
||||||
|
buildRow().let { row ->
|
||||||
|
row.text = if (option == 0)
|
||||||
|
getString(R.string.manage_device_default_user_timeout_dialog_disable)
|
||||||
|
else if (option < 1000 * 60)
|
||||||
|
TimeTextUtil.seconds(option / 1000, context!!)
|
||||||
|
else
|
||||||
|
TimeTextUtil.time(option, context!!)
|
||||||
|
|
||||||
|
row.isChecked = option == timeout
|
||||||
|
row.setOnClickListener {
|
||||||
|
auth.tryDispatchParentAction(SetDeviceDefaultUserTimeoutAction(
|
||||||
|
deviceId = deviceId,
|
||||||
|
timeout = option
|
||||||
|
))
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
list.addView(row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package io.timelimit.android.ui.manage.device.manage.feature
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import io.timelimit.android.data.model.Device
|
||||||
|
import io.timelimit.android.databinding.ManageDeviceActivityLevelBlockingBinding
|
||||||
|
import io.timelimit.android.sync.actions.UpdateEnableActivityLevelBlocking
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
|
||||||
|
object ManageDeviceActivityLevelBlocking {
|
||||||
|
fun bind(
|
||||||
|
view: ManageDeviceActivityLevelBlockingBinding,
|
||||||
|
auth: ActivityViewModel,
|
||||||
|
deviceEntry: LiveData<Device?>,
|
||||||
|
lifecycleOwner: LifecycleOwner
|
||||||
|
) {
|
||||||
|
deviceEntry.observe(lifecycleOwner, Observer { device ->
|
||||||
|
val enable = device?.enableActivityLevelBlocking ?: false
|
||||||
|
|
||||||
|
view.checkbox.setOnCheckedChangeListener { _, _ -> }
|
||||||
|
view.checkbox.isChecked = enable
|
||||||
|
view.checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
if (isChecked != enable) {
|
||||||
|
if (
|
||||||
|
device == null ||
|
||||||
|
(!auth.tryDispatchParentAction(
|
||||||
|
UpdateEnableActivityLevelBlocking(
|
||||||
|
deviceId = device.id,
|
||||||
|
enable = isChecked
|
||||||
|
)
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
view.checkbox.isChecked = enable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* 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.manage.device.manage.feature
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.navigation.Navigation
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.model.Device
|
||||||
|
import io.timelimit.android.databinding.ManageDeviceFeaturesFragmentBinding
|
||||||
|
import io.timelimit.android.livedata.liveDataFromValue
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
import io.timelimit.android.logic.AppLogic
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
|
import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
|
||||||
|
class ManageDeviceFeaturesFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
|
companion object {
|
||||||
|
fun getPreviewText(device: Device, context: Context): String {
|
||||||
|
val featureLabels = mutableListOf<String>()
|
||||||
|
|
||||||
|
if (device.considerRebootManipulation) {
|
||||||
|
featureLabels.add(context.getString(R.string.manage_device_reboot_manipulation_title))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.enableActivityLevelBlocking) {
|
||||||
|
featureLabels.add(context.getString(R.string.manage_device_activity_level_blocking_title))
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (featureLabels.isEmpty()) {
|
||||||
|
context.getString(R.string.manage_device_feature_summary_none)
|
||||||
|
} else {
|
||||||
|
featureLabels.joinToString(separator = ", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||||
|
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
||||||
|
private val auth: ActivityViewModel by lazy { activity.getActivityViewModel() }
|
||||||
|
private val args: ManageDeviceFeaturesFragmentArgs by lazy { ManageDeviceFeaturesFragmentArgs.fromBundle(arguments!!) }
|
||||||
|
private val deviceEntry: LiveData<Device?> by lazy {
|
||||||
|
logic.database.device().getDeviceById(args.deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val navigation = Navigation.findNavController(container!!)
|
||||||
|
val binding = ManageDeviceFeaturesFragmentBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
// auth
|
||||||
|
AuthenticationFab.manageAuthenticationFab(
|
||||||
|
fab = binding.fab,
|
||||||
|
shouldHighlight = auth.shouldHighlightAuthenticationButton,
|
||||||
|
authenticatedUser = auth.authenticatedUser,
|
||||||
|
fragment = this,
|
||||||
|
doesSupportAuth = liveDataFromValue(true)
|
||||||
|
)
|
||||||
|
|
||||||
|
// handlers
|
||||||
|
binding.handlers = object: ManageDeviceFeaturesFragmentHandlers {
|
||||||
|
override fun showAuthenticationScreen() {
|
||||||
|
activity.showAuthenticationScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// going back
|
||||||
|
deviceEntry.observe(this, Observer {
|
||||||
|
device ->
|
||||||
|
|
||||||
|
if (device == null) {
|
||||||
|
navigation.popBackStack(R.id.overviewFragment, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// handle reboot as manipulation
|
||||||
|
ManageDeviceRebootManipulationView.bind(
|
||||||
|
view = binding.deviceRebootManipulation,
|
||||||
|
lifecycleOwner = this,
|
||||||
|
deviceEntry = deviceEntry,
|
||||||
|
auth = auth
|
||||||
|
)
|
||||||
|
|
||||||
|
// activity level blocking
|
||||||
|
ManageDeviceActivityLevelBlocking.bind(
|
||||||
|
view = binding.activityLevelBlocking,
|
||||||
|
auth = auth,
|
||||||
|
deviceEntry = deviceEntry,
|
||||||
|
lifecycleOwner = this
|
||||||
|
)
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCustomTitle() = deviceEntry.map { it?.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ManageDeviceFeaturesFragmentHandlers {
|
||||||
|
fun showAuthenticationScreen()
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.ui.manage.device.manage
|
package io.timelimit.android.ui.manage.device.manage.feature
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.ui.manage.device.manage
|
package io.timelimit.android.ui.manage.device.manage.permission
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.app.admin.DevicePolicyManager
|
import android.app.admin.DevicePolicyManager
|
|
@ -0,0 +1,222 @@
|
||||||
|
/*
|
||||||
|
* 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.manage.device.manage.permission
|
||||||
|
|
||||||
|
import android.app.admin.DevicePolicyManager
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.navigation.Navigation
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.model.Device
|
||||||
|
import io.timelimit.android.databinding.ManageDevicePermissionsFragmentBinding
|
||||||
|
import io.timelimit.android.integration.platform.NewPermissionStatus
|
||||||
|
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||||
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
|
import io.timelimit.android.integration.platform.android.AdminReceiver
|
||||||
|
import io.timelimit.android.livedata.ignoreUnchanged
|
||||||
|
import io.timelimit.android.livedata.liveDataFromValue
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
import io.timelimit.android.logic.AppLogic
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
|
import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
|
||||||
|
class ManageDevicePermissionsFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
|
companion object {
|
||||||
|
fun getPreviewText(device: Device, context: Context): String {
|
||||||
|
val permissionLabels = mutableListOf<String>()
|
||||||
|
|
||||||
|
if (device.currentUsageStatsPermission == RuntimePermissionStatus.Granted) {
|
||||||
|
permissionLabels.add(context.getString(R.string.manage_device_permissions_usagestats_title_short))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.currentNotificationAccessPermission == NewPermissionStatus.Granted) {
|
||||||
|
permissionLabels.add(context.getString(R.string.manage_device_permission_notification_access_title))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.currentProtectionLevel != ProtectionLevel.None) {
|
||||||
|
permissionLabels.add(context.getString(R.string.manage_device_permission_device_admin_title))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.currentOverlayPermission == RuntimePermissionStatus.Granted) {
|
||||||
|
permissionLabels.add(context.getString(R.string.manage_device_permissions_overlay_title))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.accessibilityServiceEnabled) {
|
||||||
|
permissionLabels.add(context.getString(R.string.manage_device_permission_accessibility_title))
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (permissionLabels.isEmpty()) {
|
||||||
|
context.getString(R.string.manage_device_permissions_summary_none)
|
||||||
|
} else {
|
||||||
|
permissionLabels.joinToString(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||||
|
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
||||||
|
private val auth: ActivityViewModel by lazy { activity.getActivityViewModel() }
|
||||||
|
private val args: ManageDevicePermissionsFragmentArgs by lazy { ManageDevicePermissionsFragmentArgs.fromBundle(arguments!!) }
|
||||||
|
private val deviceEntry: LiveData<Device?> by lazy {
|
||||||
|
logic.database.device().getDeviceById(args.deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val navigation = Navigation.findNavController(container!!)
|
||||||
|
val binding = ManageDevicePermissionsFragmentBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
|
// auth
|
||||||
|
AuthenticationFab.manageAuthenticationFab(
|
||||||
|
fab = binding.fab,
|
||||||
|
shouldHighlight = auth.shouldHighlightAuthenticationButton,
|
||||||
|
authenticatedUser = auth.authenticatedUser,
|
||||||
|
fragment = this,
|
||||||
|
doesSupportAuth = liveDataFromValue(true)
|
||||||
|
)
|
||||||
|
|
||||||
|
// handlers
|
||||||
|
binding.handlers = object: ManageDevicePermissionsFragmentHandlers {
|
||||||
|
override fun openUsageStatsSettings() {
|
||||||
|
if (binding.isThisDevice == true) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
startActivity(
|
||||||
|
Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openNotificationAccessSettings() {
|
||||||
|
if (binding.isThisDevice == true) {
|
||||||
|
try {
|
||||||
|
startActivity(
|
||||||
|
Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
R.string.error_general,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openDrawOverOtherAppsScreen() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
startActivity(
|
||||||
|
Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context!!.packageName))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openAccessibilitySettings() {
|
||||||
|
startActivity(
|
||||||
|
Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun manageDeviceAdmin() {
|
||||||
|
if (binding.isThisDevice == true) {
|
||||||
|
val protectionLevel = logic.platformIntegration.getCurrentProtectionLevel()
|
||||||
|
|
||||||
|
if (protectionLevel == ProtectionLevel.None) {
|
||||||
|
if (InformAboutDeviceOwnerDialogFragment.shouldShow) {
|
||||||
|
InformAboutDeviceOwnerDialogFragment().show(fragmentManager!!)
|
||||||
|
} else {
|
||||||
|
startActivity(
|
||||||
|
Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN)
|
||||||
|
.putExtra(
|
||||||
|
DevicePolicyManager.EXTRA_DEVICE_ADMIN,
|
||||||
|
ComponentName(context!!, AdminReceiver::class.java)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startActivity(
|
||||||
|
Intent(Settings.ACTION_SECURITY_SETTINGS)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showAuthenticationScreen() {
|
||||||
|
activity.showAuthenticationScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// is this device
|
||||||
|
val isThisDevice = logic.deviceId.map { ownDeviceId -> ownDeviceId == args.deviceId }.ignoreUnchanged()
|
||||||
|
|
||||||
|
isThisDevice.observe(this, Observer {
|
||||||
|
binding.isThisDevice = it
|
||||||
|
})
|
||||||
|
|
||||||
|
// permissions
|
||||||
|
deviceEntry.observe(this, Observer {
|
||||||
|
device ->
|
||||||
|
|
||||||
|
if (device == null) {
|
||||||
|
navigation.popBackStack(R.id.overviewFragment, false)
|
||||||
|
} else {
|
||||||
|
binding.usageStatsAccess = device.currentUsageStatsPermission
|
||||||
|
binding.notificationAccessPermission = device.currentNotificationAccessPermission
|
||||||
|
binding.protectionLevel = device.currentProtectionLevel
|
||||||
|
binding.overlayPermission = device.currentOverlayPermission
|
||||||
|
binding.accessibilityServiceEnabled = device.accessibilityServiceEnabled
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
logic.backgroundTaskLogic.syncDeviceStatusAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCustomTitle() = deviceEntry.map { it?.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ManageDevicePermissionsFragmentHandlers {
|
||||||
|
fun openUsageStatsSettings()
|
||||||
|
fun openNotificationAccessSettings()
|
||||||
|
fun openDrawOverOtherAppsScreen()
|
||||||
|
fun openAccessibilitySettings()
|
||||||
|
fun manageDeviceAdmin()
|
||||||
|
fun showAuthenticationScreen()
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
/*
|
||||||
|
* 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.manage.device.manage.user
|
||||||
|
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.RadioButton
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.navigation.Navigation
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.model.Device
|
||||||
|
import io.timelimit.android.databinding.ManageDeviceUserFragmentBinding
|
||||||
|
import io.timelimit.android.livedata.ignoreUnchanged
|
||||||
|
import io.timelimit.android.livedata.liveDataFromValue
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
import io.timelimit.android.livedata.mergeLiveData
|
||||||
|
import io.timelimit.android.logic.AppLogic
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.sync.actions.SetDeviceUserAction
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
|
import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
import io.timelimit.android.ui.manage.device.manage.defaultuser.ManageDeviceDefaultUser
|
||||||
|
|
||||||
|
class ManageDeviceUserFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
|
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||||
|
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
||||||
|
private val auth: ActivityViewModel by lazy { activity.getActivityViewModel() }
|
||||||
|
private val args: ManageDeviceUserFragmentArgs by lazy { ManageDeviceUserFragmentArgs.fromBundle(arguments!!) }
|
||||||
|
private val deviceEntry: LiveData<Device?> by lazy {
|
||||||
|
logic.database.device().getDeviceById(args.deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val navigation = Navigation.findNavController(container!!)
|
||||||
|
val binding = ManageDeviceUserFragmentBinding.inflate(inflater, container, false)
|
||||||
|
val userEntries = logic.database.user().getAllUsersLive()
|
||||||
|
var isBindingUserListSelection = false
|
||||||
|
|
||||||
|
// auth
|
||||||
|
AuthenticationFab.manageAuthenticationFab(
|
||||||
|
fab = binding.fab,
|
||||||
|
shouldHighlight = auth.shouldHighlightAuthenticationButton,
|
||||||
|
authenticatedUser = auth.authenticatedUser,
|
||||||
|
fragment = this,
|
||||||
|
doesSupportAuth = liveDataFromValue(true)
|
||||||
|
)
|
||||||
|
|
||||||
|
// label, id
|
||||||
|
val userListItems = ArrayList<Pair<String, String>>()
|
||||||
|
|
||||||
|
fun bindUserListItems() {
|
||||||
|
userListItems.forEachIndexed { index, listItem ->
|
||||||
|
val oldRadio = binding.userList.getChildAt(index) as RadioButton?
|
||||||
|
val radio = oldRadio ?: RadioButton(context!!)
|
||||||
|
|
||||||
|
radio.text = listItem.first
|
||||||
|
|
||||||
|
if (oldRadio == null) {
|
||||||
|
radio.layoutParams = ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
)
|
||||||
|
radio.id = index
|
||||||
|
|
||||||
|
binding.userList.addView(radio)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (binding.userList.childCount > userListItems.size) {
|
||||||
|
binding.userList.removeViewAt(userListItems.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bindUserListSelection() {
|
||||||
|
isBindingUserListSelection = true
|
||||||
|
|
||||||
|
val selectedUserId = deviceEntry.value?.currentUserId
|
||||||
|
val selectedIndex = userListItems.indexOfFirst { it.second == selectedUserId }
|
||||||
|
|
||||||
|
if (selectedIndex != -1) {
|
||||||
|
binding.userList.check(selectedIndex)
|
||||||
|
} else {
|
||||||
|
val fallbackSelectedIndex = userListItems.indexOfFirst { it.second == "" }
|
||||||
|
|
||||||
|
if (fallbackSelectedIndex != -1) {
|
||||||
|
binding.userList.check(fallbackSelectedIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isBindingUserListSelection = false
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.handlers = object: ManageDeviceUserFragmentHandlers {
|
||||||
|
override fun showAuthenticationScreen() {
|
||||||
|
activity.showAuthenticationScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.userList.setOnCheckedChangeListener { _, checkedId ->
|
||||||
|
if (isBindingUserListSelection) {
|
||||||
|
return@setOnCheckedChangeListener
|
||||||
|
}
|
||||||
|
|
||||||
|
val userId = userListItems[checkedId].second
|
||||||
|
val device = deviceEntry.value
|
||||||
|
|
||||||
|
if (device != null && device.currentUserId != userId) {
|
||||||
|
if (!auth.tryDispatchParentAction(
|
||||||
|
SetDeviceUserAction(
|
||||||
|
deviceId = args.deviceId,
|
||||||
|
userId = userId
|
||||||
|
)
|
||||||
|
)) {
|
||||||
|
bindUserListSelection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceEntry.observe(this, Observer {
|
||||||
|
device ->
|
||||||
|
|
||||||
|
if (device == null) {
|
||||||
|
navigation.popBackStack(R.id.overviewFragment, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
val isThisDevice = logic.deviceId.map { ownDeviceId -> ownDeviceId == args.deviceId }.ignoreUnchanged()
|
||||||
|
|
||||||
|
mergeLiveData(deviceEntry, userEntries).observe(this, Observer {
|
||||||
|
val (device, users) = it!!
|
||||||
|
|
||||||
|
if (device != null && users != null) {
|
||||||
|
userListItems.clear()
|
||||||
|
userListItems.addAll(
|
||||||
|
users.map { user -> Pair(user.name, user.id) }
|
||||||
|
)
|
||||||
|
userListItems.add(Pair(getString(R.string.manage_device_current_user_none), ""))
|
||||||
|
|
||||||
|
bindUserListItems()
|
||||||
|
bindUserListSelection()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ManageDeviceDefaultUser.bind(
|
||||||
|
view = binding.defaultUser,
|
||||||
|
device = deviceEntry,
|
||||||
|
users = userEntries,
|
||||||
|
lifecycleOwner = this,
|
||||||
|
isThisDevice = isThisDevice,
|
||||||
|
auth = auth,
|
||||||
|
fragmentManager = fragmentManager!!
|
||||||
|
)
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCustomTitle() = deviceEntry.map { it?.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ManageDeviceUserFragmentHandlers {
|
||||||
|
fun showAuthenticationScreen()
|
||||||
|
}
|
|
@ -36,6 +36,8 @@ class UnlockAfterManipulationActivity : AppCompatActivity(), ActivityViewModelHo
|
||||||
ViewModelProviders.of(this).get(ActivityViewModel::class.java)
|
ViewModelProviders.of(this).get(ActivityViewModel::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var ignoreStop: Boolean = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_unlock_after_manipulation)
|
setContentView(R.layout.activity_unlock_after_manipulation)
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* 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.mustread
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.ViewModelProviders
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.extensions.showSafe
|
||||||
|
|
||||||
|
class MustReadFragment: DialogFragment() {
|
||||||
|
companion object {
|
||||||
|
private const val DIALOG_TAG = "MustReadDialog"
|
||||||
|
private const val MESSAGE = "message"
|
||||||
|
|
||||||
|
fun newInstance(message: Int) = MustReadFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putInt(MESSAGE, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
isCancelable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val model = ViewModelProviders.of(this).get(MustReadModel::class.java)
|
||||||
|
|
||||||
|
val alert = AlertDialog.Builder(context!!, theme)
|
||||||
|
.setMessage(arguments!!.getInt(MESSAGE))
|
||||||
|
.setPositiveButton(R.string.generic_ok) { _, _ -> dismiss() }
|
||||||
|
.create()
|
||||||
|
|
||||||
|
alert.setOnShowListener {
|
||||||
|
val okButton = alert.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||||
|
val okString = getString(R.string.generic_ok)
|
||||||
|
|
||||||
|
model.timer.observe(this, Observer {
|
||||||
|
okButton.isEnabled = it == 0
|
||||||
|
okButton.text = if (it == 0)
|
||||||
|
okString
|
||||||
|
else
|
||||||
|
"$okString ($it)"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return alert
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* 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.mustread
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import io.timelimit.android.coroutines.runAsync
|
||||||
|
import io.timelimit.android.livedata.castDown
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
class MustReadModel: ViewModel() {
|
||||||
|
private val timerInternal = MutableLiveData<Int>()
|
||||||
|
val timer = timerInternal.castDown()
|
||||||
|
|
||||||
|
init {
|
||||||
|
runAsync {
|
||||||
|
for (i in 10 downTo 0) {
|
||||||
|
timerInternal.value = i
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentTransaction
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
@ -35,10 +36,14 @@ import io.timelimit.android.livedata.switchMap
|
||||||
import io.timelimit.android.livedata.waitForNullableValue
|
import io.timelimit.android.livedata.waitForNullableValue
|
||||||
import io.timelimit.android.logic.AppLogic
|
import io.timelimit.android.logic.AppLogic
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
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.ActivityViewModelHolder
|
||||||
import io.timelimit.android.ui.main.AuthenticationFab
|
import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
|
import io.timelimit.android.ui.overview.about.AboutFragment
|
||||||
import io.timelimit.android.ui.overview.about.AboutFragmentParentHandlers
|
import io.timelimit.android.ui.overview.about.AboutFragmentParentHandlers
|
||||||
|
import io.timelimit.android.ui.overview.overview.OverviewFragment
|
||||||
import io.timelimit.android.ui.overview.overview.OverviewFragmentParentHandlers
|
import io.timelimit.android.ui.overview.overview.OverviewFragmentParentHandlers
|
||||||
|
import io.timelimit.android.ui.overview.uninstall.UninstallFragment
|
||||||
import kotlinx.android.synthetic.main.fragment_main.*
|
import kotlinx.android.synthetic.main.fragment_main.*
|
||||||
|
|
||||||
class MainFragment : Fragment(), OverviewFragmentParentHandlers, AboutFragmentParentHandlers {
|
class MainFragment : Fragment(), OverviewFragmentParentHandlers, AboutFragmentParentHandlers {
|
||||||
|
@ -79,7 +84,7 @@ class MainFragment : Fragment(), OverviewFragmentParentHandlers, AboutFragmentPa
|
||||||
}
|
}
|
||||||
}.observe(this, Observer { shouldShowSetup ->
|
}.observe(this, Observer { shouldShowSetup ->
|
||||||
if (shouldShowSetup == true) {
|
if (shouldShowSetup == true) {
|
||||||
pager.post {
|
fab.post {
|
||||||
navigation.safeNavigate(
|
navigation.safeNavigate(
|
||||||
MainFragmentDirections.actionOverviewFragmentToSetupTermsFragment(),
|
MainFragmentDirections.actionOverviewFragmentToSetupTermsFragment(),
|
||||||
R.id.overviewFragment
|
R.id.overviewFragment
|
||||||
|
@ -103,52 +108,41 @@ class MainFragment : Fragment(), OverviewFragmentParentHandlers, AboutFragmentPa
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pager.adapter = adapter
|
fun updateShowFab(selectedItemId: Int) {
|
||||||
|
showAuthButtonLive.value = when (selectedItemId) {
|
||||||
bottom_navigation_view.setOnNavigationItemSelectedListener {
|
R.id.main_tab_overview -> true
|
||||||
menuItem ->
|
R.id.main_tab_contacts -> true
|
||||||
|
R.id.main_tab_uninstall -> true
|
||||||
pager.currentItem = when(menuItem.itemId) {
|
R.id.main_tab_about -> false
|
||||||
R.id.main_tab_overview -> 0
|
else -> throw IllegalStateException()
|
||||||
R.id.main_tab_uninstall -> 1
|
|
||||||
R.id.main_tab_about -> 2
|
|
||||||
else -> 0
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_navigation_view.setOnNavigationItemReselectedListener { /* ignore */ }
|
||||||
|
bottom_navigation_view.setOnNavigationItemSelectedListener { menuItem ->
|
||||||
|
childFragmentManager.beginTransaction()
|
||||||
|
.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()
|
||||||
|
})
|
||||||
|
.commit()
|
||||||
|
|
||||||
|
updateShowFab(menuItem.itemId)
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateShowFab(selectedPage: Int) {
|
if (childFragmentManager.findFragmentById(R.id.container) == null) {
|
||||||
showAuthButtonLive.value = when (selectedPage) {
|
childFragmentManager.beginTransaction()
|
||||||
0 -> true
|
.replace(R.id.container, OverviewFragment())
|
||||||
1 -> true
|
.commit()
|
||||||
2 -> false
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateShowFab(pager.currentItem)
|
updateShowFab(bottom_navigation_view.selectedItemId)
|
||||||
|
|
||||||
pager.addOnPageChangeListener(object: ViewPager.OnPageChangeListener {
|
|
||||||
override fun onPageScrollStateChanged(state: Int) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPageSelected(position: Int) {
|
|
||||||
updateShowFab(position)
|
|
||||||
|
|
||||||
bottom_navigation_view.selectedItemId = when(pager.currentItem) {
|
|
||||||
0 -> R.id.main_tab_overview
|
|
||||||
1 -> R.id.main_tab_uninstall
|
|
||||||
2 -> R.id.main_tab_about
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openAddUserScreen() {
|
override fun openAddUserScreen() {
|
||||||
|
|
|
@ -80,7 +80,10 @@ class OverviewFragment : CoroutineFragment() {
|
||||||
ItemTouchHelper(
|
ItemTouchHelper(
|
||||||
object: ItemTouchHelper.Callback() {
|
object: ItemTouchHelper.Callback() {
|
||||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||||
if (adapter.data!![viewHolder.adapterPosition] == OverviewFragmentHeaderIntro) {
|
val index = viewHolder.adapterPosition
|
||||||
|
val item = if (index == RecyclerView.NO_POSITION) null else adapter.data!![index]
|
||||||
|
|
||||||
|
if (item == OverviewFragmentHeaderIntro) {
|
||||||
return makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.END) or
|
return makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.END) or
|
||||||
makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END)
|
makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -24,7 +24,9 @@ sealed class OverviewFragmentItem
|
||||||
object OverviewFragmentHeaderUsers: OverviewFragmentItem()
|
object OverviewFragmentHeaderUsers: OverviewFragmentItem()
|
||||||
object OverviewFragmentHeaderDevices: OverviewFragmentItem()
|
object OverviewFragmentHeaderDevices: OverviewFragmentItem()
|
||||||
data class OverviewFragmentItemDevice(val device: Device, val deviceUser: User?, val isCurrentDevice: Boolean): OverviewFragmentItem() {
|
data class OverviewFragmentItemDevice(val device: Device, val deviceUser: User?, val isCurrentDevice: Boolean): OverviewFragmentItem() {
|
||||||
val isMissingRequiredPermission = deviceUser?.type == UserType.Child && device.currentUsageStatsPermission == RuntimePermissionStatus.NotGranted
|
val isMissingRequiredPermission = deviceUser?.type == UserType.Child && (
|
||||||
|
device.currentUsageStatsPermission == RuntimePermissionStatus.NotGranted || device.missingPermissionAtQOrLater
|
||||||
|
)
|
||||||
}
|
}
|
||||||
data class OverviewFragmentItemUser(val user: User, val temporarilyBlocked: Boolean, val limitsTemporarilyDisabled: Boolean): OverviewFragmentItem()
|
data class OverviewFragmentItemUser(val user: User, val temporarilyBlocked: Boolean, val limitsTemporarilyDisabled: Boolean): OverviewFragmentItem()
|
||||||
object OverviewFragmentActionAddUser: OverviewFragmentItem()
|
object OverviewFragmentActionAddUser: OverviewFragmentItem()
|
||||||
|
|
|
@ -18,6 +18,7 @@ package io.timelimit.android.ui.setup
|
||||||
import android.app.admin.DevicePolicyManager
|
import android.app.admin.DevicePolicyManager
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
@ -34,7 +35,7 @@ import io.timelimit.android.integration.platform.ProtectionLevel
|
||||||
import io.timelimit.android.integration.platform.android.AdminReceiver
|
import io.timelimit.android.integration.platform.android.AdminReceiver
|
||||||
import io.timelimit.android.logic.AppLogic
|
import io.timelimit.android.logic.AppLogic
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.ui.manage.device.manage.InformAboutDeviceOwnerDialogFragment
|
import io.timelimit.android.ui.manage.device.manage.permission.InformAboutDeviceOwnerDialogFragment
|
||||||
|
|
||||||
class SetupDevicePermissionsFragment : Fragment() {
|
class SetupDevicePermissionsFragment : Fragment() {
|
||||||
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
||||||
|
@ -93,6 +94,22 @@ class SetupDevicePermissionsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun openDrawOverOtherAppsScreen() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
startActivity(
|
||||||
|
Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context!!.packageName))
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openAccessibilitySettings() {
|
||||||
|
startActivity(
|
||||||
|
Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun gotoNextStep() {
|
override fun gotoNextStep() {
|
||||||
navigation.safeNavigate(
|
navigation.safeNavigate(
|
||||||
SetupDevicePermissionsFragmentDirections
|
SetupDevicePermissionsFragmentDirections
|
||||||
|
@ -113,6 +130,8 @@ class SetupDevicePermissionsFragment : Fragment() {
|
||||||
binding.notificationAccessPermission = platform.getNotificationAccessPermissionStatus()
|
binding.notificationAccessPermission = platform.getNotificationAccessPermissionStatus()
|
||||||
binding.protectionLevel = platform.getCurrentProtectionLevel()
|
binding.protectionLevel = platform.getCurrentProtectionLevel()
|
||||||
binding.usageStatsAccess = platform.getForegroundAppPermissionStatus()
|
binding.usageStatsAccess = platform.getForegroundAppPermissionStatus()
|
||||||
|
binding.overlayPermission = platform.getOverlayPermissionStatus()
|
||||||
|
binding.accessibilityServiceEnabled = platform.isAccessibilityServiceEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -126,5 +145,7 @@ interface SetupDevicePermissionsHandlers {
|
||||||
fun manageDeviceAdmin()
|
fun manageDeviceAdmin()
|
||||||
fun openUsageStatsSettings()
|
fun openUsageStatsSettings()
|
||||||
fun openNotificationAccessSettings()
|
fun openNotificationAccessSettings()
|
||||||
|
fun openDrawOverOtherAppsScreen()
|
||||||
|
fun openAccessibilitySettings()
|
||||||
fun gotoNextStep()
|
fun gotoNextStep()
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,7 @@ class AddUserModel(application: Application): AndroidViewModel(application) {
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
|
|
||||||
defaultCategories.generateGamesTimeLimitRules(allowedAppsCategory).forEach { rule ->
|
defaultCategories.generateGamesTimeLimitRules(allowedGamesCategory).forEach { rule ->
|
||||||
actions.add(CreateTimeLimitRuleAction(rule))
|
actions.add(CreateTimeLimitRuleAction(rule))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ package io.timelimit.android.ui.view
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
|
@ -34,14 +35,16 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet): FrameLay
|
||||||
|
|
||||||
var listener: SelectTimeSpanViewListener? = null
|
var listener: SelectTimeSpanViewListener? = null
|
||||||
|
|
||||||
var timeInMillis: Long by Delegates.observable(0L) {
|
var timeInMillis: Long by Delegates.observable(0L) { _, _, _ ->
|
||||||
_, _, _ ->
|
|
||||||
bindTime()
|
bindTime()
|
||||||
listener?.onTimeSpanChanged(timeInMillis)
|
listener?.onTimeSpanChanged(timeInMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxDays: Int by Delegates.observable(0) {
|
var maxDays: Int by Delegates.observable(0) { _, _, _ ->
|
||||||
_, _, _ -> binding.maxDays = maxDays
|
binding.maxDays = maxDays
|
||||||
|
|
||||||
|
binding.dayPicker.maxValue = maxDays
|
||||||
|
binding.dayPickerContainer.visibility = if (maxDays > 0) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -69,6 +72,10 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet): FrameLay
|
||||||
binding.daysText = TimeTextUtil.days(totalDays, context!!)
|
binding.daysText = TimeTextUtil.days(totalDays, context!!)
|
||||||
binding.minutesText = TimeTextUtil.minutes(minutes, context!!)
|
binding.minutesText = TimeTextUtil.minutes(minutes, context!!)
|
||||||
binding.hoursText = TimeTextUtil.hours(hours, context!!)
|
binding.hoursText = TimeTextUtil.hours(hours, context!!)
|
||||||
|
|
||||||
|
binding.minutePicker.value = binding.minutes ?: 0
|
||||||
|
binding.hourPicker.value = binding.hours ?: 0
|
||||||
|
binding.dayPicker.value = binding.days ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readStatusFromBinding() {
|
private fun readStatusFromBinding() {
|
||||||
|
@ -79,7 +86,43 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet): FrameLay
|
||||||
timeInMillis = (((days * 24) + hours) * 60 + minutes) * 1000 * 60
|
timeInMillis = (((days * 24) + hours) * 60 + minutes) * 1000 * 60
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearNumberPickerFocus() {
|
||||||
|
binding.minutePicker.clearFocus()
|
||||||
|
binding.hourPicker.clearFocus()
|
||||||
|
binding.dayPicker.clearFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enablePickerMode(enable: Boolean) {
|
||||||
|
binding.seekbarContainer.visibility = if (enable) View.GONE else View.VISIBLE
|
||||||
|
binding.pickerContainer.visibility = if (enable) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
binding.minutePicker.minValue = 0
|
||||||
|
binding.minutePicker.maxValue = 59
|
||||||
|
|
||||||
|
binding.hourPicker.minValue = 0
|
||||||
|
binding.hourPicker.maxValue = 23
|
||||||
|
|
||||||
|
binding.dayPicker.minValue = 0
|
||||||
|
binding.dayPicker.maxValue = 1
|
||||||
|
binding.dayPickerContainer.visibility = View.GONE
|
||||||
|
|
||||||
|
binding.minutePicker.setOnValueChangedListener { _, _, newValue ->
|
||||||
|
binding.minutes = newValue
|
||||||
|
readStatusFromBinding()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.hourPicker.setOnValueChangedListener { _, _, newValue ->
|
||||||
|
binding.hours = newValue
|
||||||
|
readStatusFromBinding()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.dayPicker.setOnValueChangedListener { _, _, newValue ->
|
||||||
|
binding.days = newValue
|
||||||
|
readStatusFromBinding()
|
||||||
|
}
|
||||||
|
|
||||||
binding.daysSeek.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
|
binding.daysSeek.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
|
||||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||||
binding.days = progress
|
binding.days = progress
|
||||||
|
@ -124,9 +167,15 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet): FrameLay
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
binding.pickerContainer.visibility = GONE
|
||||||
|
|
||||||
|
binding.switchToPickerButton.setOnClickListener { listener?.setEnablePickerMode(true) }
|
||||||
|
binding.switchToSeekbarButton.setOnClickListener { listener?.setEnablePickerMode(false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectTimeSpanViewListener {
|
interface SelectTimeSpanViewListener {
|
||||||
fun onTimeSpanChanged(newTimeInMillis: Long)
|
fun onTimeSpanChanged(newTimeInMillis: Long)
|
||||||
|
fun setEnablePickerMode(enable: Boolean)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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.util
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
object AndroidVersion {
|
||||||
|
val qOrLater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||||
|
}
|
|
@ -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');
|
||||||
|
}}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue