mirror of
https://codeberg.org/timelimit/opentimelimit-android.git
synced 2025-10-03 17:59:46 +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
|
||||
|
||||
- open a ticket here at GitLab
|
||||
- alternativly, send a message to support@timelimit.io
|
||||
- alternatively, send a message to support@timelimit.io
|
||||
|
||||
## 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.
|
||||
Due to that, merge requests are not wanted to avoid licensing issues when adding something from a merge request to the proprietary version.
|
||||
Are possible but only after talking with the developer before developing anything.
|
|
@ -25,18 +25,21 @@ androidExtensions {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 29
|
||||
defaultConfig {
|
||||
applicationId "io.timelimit.android.open"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 5
|
||||
versionName "0.2.3"
|
||||
targetSdkVersion 29
|
||||
versionCode 50
|
||||
versionName "1.5.1"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
kapt {
|
||||
arguments {
|
||||
arg("room.schemaLocation", "$projectDir/schemas".toString())
|
||||
}
|
||||
javacOptions {
|
||||
option("-Xmaxerrs", 500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,7 +66,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
def nav_version = "1.0.0-beta02"
|
||||
def nav_version = "1.0.0"
|
||||
def room_version = "2.0.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.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<!-- suppress DeprecatedClassUsageInspection -->
|
||||
<uses-permission
|
||||
android:name="android.permission.GET_TASKS"
|
||||
|
@ -29,11 +30,14 @@
|
|||
tools:ignore="ProtectedPermissions" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
<uses-feature android:name="android.hardware.telephony" android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
@ -111,6 +115,19 @@
|
|||
</intent-filter>
|
||||
</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>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -28,6 +28,8 @@ interface Database {
|
|||
fun usedTimes(): UsedTimeDao
|
||||
fun user(): UserDao
|
||||
fun temporarilyAllowedApp(): TemporarilyAllowedAppDao
|
||||
fun appActivity(): AppActivityDao
|
||||
fun allowedContact(): AllowedContactDao
|
||||
|
||||
fun beginTransaction()
|
||||
fun setTransactionSuccessful()
|
||||
|
|
|
@ -22,4 +22,28 @@ object DatabaseMigrations {
|
|||
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,
|
||||
TimeLimitRule::class,
|
||||
ConfigurationItem::class,
|
||||
TemporarilyAllowedApp::class
|
||||
], version = 4)
|
||||
TemporarilyAllowedApp::class,
|
||||
AppActivity::class,
|
||||
AllowedContact::class
|
||||
], version = 5)
|
||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||
companion object {
|
||||
private val lock = Object()
|
||||
|
@ -69,7 +71,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
|
|||
.addMigrations(
|
||||
DatabaseMigrations.MIGRATE_TO_V2,
|
||||
DatabaseMigrations.MIGRATE_TO_V3,
|
||||
DatabaseMigrations.MIGRATE_TO_V4
|
||||
DatabaseMigrations.MIGRATE_TO_V4,
|
||||
DatabaseMigrations.MIGRATE_TO_V5
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -37,6 +37,8 @@ object DatabaseBackupLowlevel {
|
|||
private const val TIME_LIMIT_RULE = "timelimitRule"
|
||||
private const val USED_TIME_ITEM = "usedTime"
|
||||
private const val USER = "user"
|
||||
private const val APP_ACTIVITY = "appActivity"
|
||||
private const val ALLOWED_CONTACT = "allowedContact"
|
||||
|
||||
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
||||
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(USED_TIME_ITEM) { offset, pageSize -> database.usedTimes().getUsedTimePageSync(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()
|
||||
}
|
||||
|
@ -168,6 +173,27 @@ object DatabaseBackupLowlevel {
|
|||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
abstract fun updateParentCategory(categoryId: String, parentCategoryId: String)
|
||||
|
||||
@Update
|
||||
abstract fun updateCategorySync(category: Category)
|
||||
}
|
||||
|
||||
data class CategoryShortInfo(
|
||||
|
|
|
@ -112,4 +112,19 @@ abstract class ConfigDao {
|
|||
|
||||
fun wasDeviceLockedSync() = getValueOfKeySync(ConfigurationItemType.WasDeviceLocked) == "true"
|
||||
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")
|
||||
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
|
||||
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")
|
||||
val temporarilyBlocked: Boolean,
|
||||
@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 {
|
||||
companion object {
|
||||
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 TEMPORARILY_BLOCKED = "temporarilyBlocked"
|
||||
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 {
|
||||
var id: String? = null
|
||||
|
@ -67,6 +73,8 @@ data class Category(
|
|||
var temporarilyBlocked: Boolean? = null
|
||||
// this field was added later so it has got a default value
|
||||
var parentCategoryId = ""
|
||||
var blockAllNotifications = false
|
||||
var timeWarnings = 0
|
||||
|
||||
reader.beginObject()
|
||||
|
||||
|
@ -79,6 +87,8 @@ data class Category(
|
|||
EXTRA_TIME_IN_MILLIS -> extraTimeInMillis = reader.nextLong()
|
||||
TEMPORARILY_BLOCKED -> temporarilyBlocked = reader.nextBoolean()
|
||||
PARENT_CATEGORY_ID -> parentCategoryId = reader.nextString()
|
||||
BlOCK_ALL_NOTIFICATIONS -> blockAllNotifications = reader.nextBoolean()
|
||||
TIME_WARNINGS -> timeWarnings = reader.nextInt()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
@ -92,7 +102,9 @@ data class Category(
|
|||
blockedMinutesInWeek = blockedMinutesInWeek!!,
|
||||
extraTimeInMillis = extraTimeInMillis!!,
|
||||
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(TEMPORARILY_BLOCKED).value(temporarilyBlocked)
|
||||
writer.name(PARENT_CATEGORY_ID).value(parentCategoryId)
|
||||
writer.name(BlOCK_ALL_NOTIFICATIONS).value(blockAllNotifications)
|
||||
writer.name(TIME_WARNINGS).value(timeWarnings)
|
||||
|
||||
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 {
|
||||
IdGenerator.assertIdValid(categoryId)
|
||||
|
||||
|
|
|
@ -77,30 +77,45 @@ data class ConfigurationItem(
|
|||
enum class ConfigurationItemType {
|
||||
OwnDeviceId,
|
||||
ShownHints,
|
||||
WasDeviceLocked
|
||||
WasDeviceLocked,
|
||||
ForegroundAppQueryRange,
|
||||
EnableAlternativeDurationSelection,
|
||||
LastScreenOnTime
|
||||
}
|
||||
|
||||
object ConfigurationItemTypeUtil {
|
||||
private const val OWN_DEVICE_ID = 1
|
||||
private const val SHOWN_HINTS = 2
|
||||
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(
|
||||
ConfigurationItemType.OwnDeviceId,
|
||||
ConfigurationItemType.ShownHints,
|
||||
ConfigurationItemType.WasDeviceLocked
|
||||
ConfigurationItemType.WasDeviceLocked,
|
||||
ConfigurationItemType.ForegroundAppQueryRange,
|
||||
ConfigurationItemType.EnableAlternativeDurationSelection,
|
||||
ConfigurationItemType.LastScreenOnTime
|
||||
)
|
||||
|
||||
fun serialize(value: ConfigurationItemType) = when(value) {
|
||||
ConfigurationItemType.OwnDeviceId -> OWN_DEVICE_ID
|
||||
ConfigurationItemType.ShownHints -> SHOWN_HINTS
|
||||
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) {
|
||||
OWN_DEVICE_ID -> ConfigurationItemType.OwnDeviceId
|
||||
SHOWN_HINTS -> ConfigurationItemType.ShownHints
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -118,4 +133,6 @@ object HintsToShow {
|
|||
const val DEVICE_SCREEN_INTRODUCTION = 2L
|
||||
const val CATEGORIES_INTRODUCTION = 4L
|
||||
const val TIME_LIMIT_RULE_INTRODUCTION = 8L
|
||||
const val CONTACTS_INTRO = 16L
|
||||
const val TIMELIMIT_RULE_MUSTREAD = 32L
|
||||
}
|
||||
|
|
|
@ -65,8 +65,24 @@ data class Device(
|
|||
val manipulationDidReboot: Boolean,
|
||||
@ColumnInfo(name = "had_manipulation")
|
||||
val hadManipulation: Boolean,
|
||||
@ColumnInfo(name = "default_user")
|
||||
val defaultUser: String,
|
||||
@ColumnInfo(name = "default_user_timeout")
|
||||
val defaultUserTimeout: Int,
|
||||
@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 {
|
||||
companion object {
|
||||
private const val ID = "id"
|
||||
|
@ -85,7 +101,15 @@ data class Device(
|
|||
private const val TRIED_DISABLING_DEVICE_ADMIN = "tdda"
|
||||
private const val MANIPULATION_DID_REBOOT = "mdr"
|
||||
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 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 {
|
||||
var id: String? = null
|
||||
|
@ -104,7 +128,15 @@ data class Device(
|
|||
var manipulationTriedDisablingDeviceAdmin: Boolean? = null
|
||||
var manipulationDidReboot: Boolean = false
|
||||
var hadManipulation: Boolean? = null
|
||||
var defaultUser = ""
|
||||
var defaultUserTimeout = 0
|
||||
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()
|
||||
|
||||
|
@ -126,7 +158,15 @@ data class Device(
|
|||
TRIED_DISABLING_DEVICE_ADMIN -> manipulationTriedDisablingDeviceAdmin = reader.nextBoolean()
|
||||
MANIPULATION_DID_REBOOT -> manipulationDidReboot = 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()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -150,7 +190,15 @@ data class Device(
|
|||
manipulationTriedDisablingDeviceAdmin = manipulationTriedDisablingDeviceAdmin!!,
|
||||
manipulationDidReboot = manipulationDidReboot,
|
||||
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(MANIPULATION_DID_REBOOT).value(manipulationDidReboot)
|
||||
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(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()
|
||||
}
|
||||
|
@ -211,6 +267,10 @@ data class Device(
|
|||
val manipulationOfNotificationAccess = currentNotificationAccessPermission != highestNotificationAccessPermission
|
||||
@Transient
|
||||
val manipulationOfAppVersion = currentAppVersion != highestAppVersion
|
||||
@Transient
|
||||
val manipulationOfOverlayPermission = currentOverlayPermission != highestOverlayPermission
|
||||
@Transient
|
||||
val manipulationOfAccessibilityService = accessibilityServiceEnabled != wasAccessibilityServiceEnabled
|
||||
|
||||
@Transient
|
||||
val hasActiveManipulationWarning = manipulationOfProtectionLevel ||
|
||||
|
@ -218,8 +278,16 @@ data class Device(
|
|||
manipulationOfNotificationAccess ||
|
||||
manipulationOfAppVersion ||
|
||||
manipulationTriedDisablingDeviceAdmin ||
|
||||
manipulationDidReboot
|
||||
manipulationDidReboot ||
|
||||
manipulationOfOverlayPermission ||
|
||||
manipulationOfAccessibilityService
|
||||
|
||||
@Transient
|
||||
val hasAnyManipulation = hasActiveManipulationWarning || hadManipulation
|
||||
|
||||
@Transient
|
||||
val missingPermissionAtQOrLater = qOrLater &&
|
||||
(!accessibilityServiceEnabled) &&
|
||||
(currentOverlayPermission != RuntimePermissionStatus.Granted) &&
|
||||
(currentProtectionLevel != ProtectionLevel.DeviceOwner)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package io.timelimit.android.extensions
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.EditorInfo
|
||||
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 androidx.room.TypeConverter
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.data.model.AppActivity
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
abstract class PlatformIntegration(
|
||||
val maximumProtectionLevel: ProtectionLevel
|
||||
) {
|
||||
abstract fun getLocalApps(): Collection<App>
|
||||
abstract fun getLocalAppActivities(deviceId: String): Collection<AppActivity>
|
||||
abstract fun getLocalAppTitle(packageName: String): String?
|
||||
abstract fun getAppIcon(packageName: String): Drawable?
|
||||
abstract fun getLauncherAppPackageName(): String?
|
||||
abstract fun getCurrentProtectionLevel(): ProtectionLevel
|
||||
abstract fun getForegroundAppPermissionStatus(): RuntimePermissionStatus
|
||||
abstract fun getDrawOverOtherAppsPermissionStatus(): RuntimePermissionStatus
|
||||
abstract fun getNotificationAccessPermissionStatus(): NewPermissionStatus
|
||||
abstract fun getOverlayPermissionStatus(): RuntimePermissionStatus
|
||||
abstract fun isAccessibilityServiceEnabled(): Boolean
|
||||
abstract fun disableDeviceAdmin()
|
||||
abstract fun trySetLockScreenPassword(password: String): Boolean
|
||||
// this must have a fallback if the permission is not granted
|
||||
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
|
||||
abstract suspend fun getForegroundAppPackageName(): String?
|
||||
abstract suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long)
|
||||
abstract fun setAppStatusMessage(message: AppStatusMessage?)
|
||||
abstract fun isScreenOn(): Boolean
|
||||
abstract fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean)
|
||||
abstract fun showTimeWarningNotification(title: String, text: String)
|
||||
// returns package names for which it was set
|
||||
abstract fun setSuspendedApps(packageNames: List<String>, suspend: Boolean): List<String>
|
||||
abstract fun stopSuspendingForAllApps()
|
||||
|
@ -54,6 +62,12 @@ abstract class PlatformIntegration(
|
|||
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 {
|
||||
None, SimpleDeviceAdmin, PasswordDeviceAdmin, DeviceOwner
|
||||
}
|
||||
|
@ -170,4 +184,9 @@ class NewPermissionStatusConverter {
|
|||
}
|
||||
|
||||
@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 {
|
||||
runAsync {
|
||||
val logic = DefaultAppLogic.with(context)
|
||||
|
||||
if (logic.database.config().getOwnDeviceId().waitForNullableValue() != null) {
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
TriedDisablingDeviceAdminAction,
|
||||
logic
|
||||
action = TriedDisablingDeviceAdminAction,
|
||||
appLogic = DefaultAppLogic.with(context),
|
||||
ignoreIfDeviceIsNotConfigured = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.admin.DevicePolicyManager
|
||||
|
@ -27,20 +28,27 @@ import android.content.Intent
|
|||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.media.session.MediaSessionManager
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.os.UserManager
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.coroutines.runAsyncExpectForever
|
||||
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.android.foregroundapp.ForegroundAppHelper
|
||||
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) {
|
||||
|
@ -65,6 +73,7 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
|||
private val activityManager = this.context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
private val notificationManager = this.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
private val deviceAdmin = ComponentName(context.applicationContext, AdminReceiver::class.java)
|
||||
private val overlay = OverlayUtil(context as Application)
|
||||
|
||||
init {
|
||||
AppsChangeListener.registerBroadcastReceiver(this.context, object : BroadcastReceiver() {
|
||||
|
@ -78,10 +87,20 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
|||
return AndroidIntegrationApps.getLocalApps(context)
|
||||
}
|
||||
|
||||
override fun getLocalAppActivities(deviceId: String): Collection<AppActivity> {
|
||||
return AndroidIntegrationApps.getLocalAppActivities(deviceId, context)
|
||||
}
|
||||
|
||||
override fun getLocalAppTitle(packageName: String): String? {
|
||||
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? {
|
||||
return AndroidIntegrationApps.getAppIcon(packageName, context)
|
||||
}
|
||||
|
@ -90,8 +109,8 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
|||
return AdminStatus.getAdminStatus(context, policyManager)
|
||||
}
|
||||
|
||||
override suspend fun getForegroundAppPackageName(): String? {
|
||||
return foregroundAppHelper.getForegroundAppPackage()
|
||||
override suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long) {
|
||||
foregroundAppHelper.getForegroundApp(result, queryInterval)
|
||||
}
|
||||
|
||||
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 {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "set password")
|
||||
|
@ -153,17 +196,55 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
|||
}
|
||||
|
||||
private var lastAppStatusMessage: AppStatusMessage? = null
|
||||
private var appStatusMessageChannel = Channel<AppStatusMessage?>(capacity = Channel.CONFLATED)
|
||||
|
||||
override fun setAppStatusMessage(message: AppStatusMessage?) {
|
||||
if (lastAppStatusMessage != message) {
|
||||
lastAppStatusMessage = message
|
||||
appStatusMessageChannel.offer(message)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
runAsyncExpectForever {
|
||||
appStatusMessageChannel.consumeEach { message ->
|
||||
BackgroundService.setStatusMessage(message, context)
|
||||
|
||||
delay(200)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun showAppLockScreen(currentPackageName: String) {
|
||||
LockActivity.start(context, currentPackageName)
|
||||
override fun showAppLockScreen(currentPackageName: String, currentActivityName: String?) {
|
||||
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 {
|
||||
|
@ -176,7 +257,7 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
|||
|
||||
override fun setShowNotificationToRevokeTemporarilyAllowedApps(show: Boolean) {
|
||||
if (show) {
|
||||
NotificationChannels.createAppStatusChannel(notificationManager, context)
|
||||
NotificationChannels.createNotificationChannels(notificationManager, context)
|
||||
|
||||
val actionIntent = PendingIntent.getService(
|
||||
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() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (policyManager.isDeviceOwnerApp(context.packageName)) {
|
||||
|
|
|
@ -25,6 +25,7 @@ import android.provider.ContactsContract
|
|||
import android.provider.Settings
|
||||
import android.provider.Telephony
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.data.model.AppActivity
|
||||
import io.timelimit.android.data.model.AppRecommendation
|
||||
|
||||
object AndroidIntegrationApps {
|
||||
|
@ -90,6 +91,26 @@ object AndroidIntegrationApps {
|
|||
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) {
|
||||
val packageManager = context.packageManager
|
||||
|
||||
|
|
|
@ -21,12 +21,15 @@ import android.app.Service
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.integration.platform.AppStatusMessage
|
||||
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
|
||||
|
||||
class BackgroundService: Service() {
|
||||
|
@ -34,6 +37,7 @@ class BackgroundService: Service() {
|
|||
private const val ACTION = "action"
|
||||
private const val ACTION_SET_NOTIFICATION = "set_notification"
|
||||
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"
|
||||
|
||||
fun setStatusMessage(status: AppStatusMessage?, context: Context) {
|
||||
|
@ -53,6 +57,16 @@ class BackgroundService: Service() {
|
|||
|
||||
fun prepareRevokeTemporarilyAllowed(context: Context) = Intent(context, BackgroundService::class.java)
|
||||
.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 {
|
||||
|
@ -68,7 +82,7 @@ class BackgroundService: Service() {
|
|||
DefaultAppLogic.with(this)
|
||||
|
||||
// create the channel
|
||||
NotificationChannels.createAppStatusChannel(notificationManager, this)
|
||||
NotificationChannels.createNotificationChannels(notificationManager, this)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
|
@ -78,18 +92,12 @@ class BackgroundService: Service() {
|
|||
if (action == ACTION_SET_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)
|
||||
.setSmallIcon(R.drawable.ic_stat_timelapse)
|
||||
.setContentTitle(appStatusMessage.title)
|
||||
.setContentText(appStatusMessage.text)
|
||||
.setContentIntent(openAppIntent)
|
||||
.setSubText(appStatusMessage.subtext)
|
||||
.setContentIntent(getOpenAppIntent(this@BackgroundService))
|
||||
.setWhen(0)
|
||||
.setShowWhen(false)
|
||||
.setSound(null)
|
||||
|
@ -98,6 +106,24 @@ class BackgroundService: Service() {
|
|||
.setAutoCancel(false)
|
||||
.setOngoing(true)
|
||||
.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()
|
||||
|
||||
if (didPostNotification) {
|
||||
|
@ -110,6 +136,16 @@ class BackgroundService: Service() {
|
|||
runAsync {
|
||||
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 NOTIFICATION_BLOCKED = 2
|
||||
const val REVOKE_TEMPORARILY_ALLOWED_APPS = 3
|
||||
const val APP_RESET = 4
|
||||
const val TIME_WARNING = 4
|
||||
}
|
||||
|
||||
object NotificationChannels {
|
||||
const val APP_STATUS = "app status"
|
||||
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) {
|
||||
notificationManager.createNotificationChannel(
|
||||
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) {
|
||||
notificationManager.createNotificationChannel(
|
||||
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 {
|
||||
const val OPEN_MAIN_APP = 1
|
||||
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() {
|
||||
companion object {
|
||||
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 blockingReasonUtil: BlockingReasonUtil by lazy { BlockingReasonUtil(appLogic) }
|
||||
private val notificationManager: NotificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
|
||||
private val queryAppTitleCache: QueryAppTitleCache by lazy { QueryAppTitleCache(appLogic.platformIntegration) }
|
||||
private val lastOngoingNotificationHidden = mutableSetOf<String>()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
NotificationChannels.createBlockedNotificationChannel(notificationManager, this)
|
||||
NotificationChannels.createNotificationChannels(notificationManager, this)
|
||||
}
|
||||
|
||||
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
||||
|
@ -58,9 +60,25 @@ class NotificationListener: NotificationListenerService() {
|
|||
runAsync {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
true
|
||||
} catch (ex: SecurityException) {
|
||||
|
@ -91,6 +109,7 @@ class NotificationListener: NotificationListenerService() {
|
|||
BlockingReason.TimeOver -> 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.NotificationsAreBlocked -> getString(R.string.lock_reason_short_notification_blocking)
|
||||
BlockingReason.None -> throw IllegalStateException()
|
||||
}
|
||||
)
|
||||
|
@ -109,25 +128,41 @@ class NotificationListener: NotificationListenerService() {
|
|||
}
|
||||
|
||||
private suspend fun shouldRemoveNotification(sbn: StatusBarNotification): BlockingReason {
|
||||
if (sbn.packageName == packageName || sbn.isOngoing) {
|
||||
if (sbn.packageName == packageName) {
|
||||
return BlockingReason.None
|
||||
}
|
||||
|
||||
val blockingReason = blockingReasonUtil.getBlockingReason(sbn.packageName).waitForNonNullValue()
|
||||
|
||||
if (blockingReason == BlockingReason.None) {
|
||||
if (sbn.isOngoing && (!SUPPORTS_HIDING_ONGOING_NOTIFICATIONS)) {
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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.content.Context
|
||||
import io.timelimit.android.integration.platform.ForegroundAppSpec
|
||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||
|
||||
class CompatForegroundAppHelper(context: Context) : ForegroundAppHelper() {
|
||||
private val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
|
||||
override suspend fun getForegroundAppPackage(): String? {
|
||||
return try {
|
||||
activityManager.getRunningTasks(1)[0].topActivity.packageName
|
||||
override suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long) {
|
||||
try {
|
||||
val activity = activityManager.getRunningTasks(1)[0].topActivity
|
||||
|
||||
result.packageName = activity.packageName
|
||||
result.activityName = activity.className
|
||||
} 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.os.Build
|
||||
import io.timelimit.android.integration.platform.ForegroundAppSpec
|
||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||
|
||||
abstract class ForegroundAppHelper {
|
||||
abstract suspend fun getForegroundAppPackage(): String?
|
||||
abstract suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long)
|
||||
abstract fun getPermissionStatus(): RuntimePermissionStatus
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -22,6 +22,7 @@ import android.app.usage.UsageStatsManager
|
|||
import android.content.Context
|
||||
import android.os.Build
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.integration.platform.ForegroundAppSpec
|
||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
@ -37,11 +38,12 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
|
|||
|
||||
private var lastQueryTime: Long = 0
|
||||
private var lastPackage: String? = null
|
||||
private var lastPackageActivity: String? = null
|
||||
private var lastPackageTime: Long = 0
|
||||
private val event = UsageEvents.Event()
|
||||
|
||||
@Throws(SecurityException::class)
|
||||
override suspend fun getForegroundAppPackage(): String? {
|
||||
override suspend fun getForegroundApp(result: ForegroundAppSpec, queryInterval: Long) {
|
||||
if (getPermissionStatus() == RuntimePermissionStatus.NotGranted) {
|
||||
throw SecurityException()
|
||||
}
|
||||
|
@ -49,10 +51,11 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
|
|||
return foregroundAppThread.executeAndWait {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
if (lastQueryTime > now) {
|
||||
if (lastQueryTime > now || queryInterval >= 1000 * 60 * 60 * 24 /* 1 day */) {
|
||||
// if the time went backwards, forget everything
|
||||
lastQueryTime = 0
|
||||
lastPackage = null
|
||||
lastPackageActivity = null
|
||||
lastPackageTime = 0
|
||||
}
|
||||
|
||||
|
@ -66,7 +69,7 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
|
|||
// which seems to provide all data
|
||||
// update: with 1 second, some App switching events were missed
|
||||
// it seems to always work with 1.5 seconds
|
||||
lastQueryTime - 1500
|
||||
lastQueryTime - Math.max(queryInterval, 1500)
|
||||
}
|
||||
|
||||
usageStatsManager.queryEvents(queryStartTime, now)?.let { usageEvents ->
|
||||
|
@ -77,6 +80,7 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
|
|||
if (event.timeStamp > lastPackageTime) {
|
||||
lastPackageTime = event.timeStamp
|
||||
lastPackage = event.packageName
|
||||
lastPackageActivity = event.className
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +88,8 @@ class LollipopForegroundAppHelper(private val context: Context) : ForegroundAppH
|
|||
|
||||
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 io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.data.model.AppActivity
|
||||
import io.timelimit.android.integration.platform.*
|
||||
|
||||
class DummyIntegration(
|
||||
|
@ -37,6 +38,10 @@ class DummyIntegration(
|
|||
return localApps
|
||||
}
|
||||
|
||||
override fun getLocalAppActivities(deviceId: String): Collection<AppActivity> {
|
||||
return emptySet()
|
||||
}
|
||||
|
||||
override fun getLocalAppTitle(packageName: String): String? {
|
||||
return localApps.find { it.packageName == packageName }?.title
|
||||
}
|
||||
|
@ -45,10 +50,20 @@ class DummyIntegration(
|
|||
return null
|
||||
}
|
||||
|
||||
override fun getLauncherAppPackageName(): String? = null
|
||||
|
||||
override fun getCurrentProtectionLevel(): ProtectionLevel {
|
||||
return protectionLevel
|
||||
}
|
||||
|
||||
override fun getOverlayPermissionStatus(): RuntimePermissionStatus {
|
||||
return RuntimePermissionStatus.NotRequired
|
||||
}
|
||||
|
||||
override fun isAccessibilityServiceEnabled(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getForegroundAppPermissionStatus(): RuntimePermissionStatus {
|
||||
return foregroundAppPermission
|
||||
}
|
||||
|
@ -68,10 +83,18 @@ class DummyIntegration(
|
|||
// do nothing
|
||||
}
|
||||
|
||||
override fun showAppLockScreen(currentPackageName: String) {
|
||||
override fun showAppLockScreen(currentPackageName: String, currentActivityName: String?) {
|
||||
launchLockScreenForPackage = currentPackageName
|
||||
}
|
||||
|
||||
override fun muteAudioIfPossible(packageName: String) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
override fun setShowBlockingOverlay(show: Boolean) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
fun getAndResetShowAppLockScreen(): String? {
|
||||
try {
|
||||
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) {
|
||||
throw SecurityException()
|
||||
}
|
||||
|
||||
return foregroundApp
|
||||
result.packageName = foregroundApp
|
||||
result.activityName = null
|
||||
}
|
||||
|
||||
override fun setAppStatusMessage(message: AppStatusMessage?) {
|
||||
|
@ -108,6 +132,10 @@ class DummyIntegration(
|
|||
showRevokeTemporarilyAllowedNotification = show
|
||||
}
|
||||
|
||||
override fun showTimeWarningNotification(title: String, text: String) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
override fun disableDeviceAdmin() {
|
||||
// nothing to do
|
||||
}
|
||||
|
|
|
@ -18,14 +18,18 @@ package io.timelimit.android.livedata
|
|||
import androidx.lifecycle.LiveData
|
||||
|
||||
fun LiveData<Boolean>.or(other: LiveData<Boolean>): LiveData<Boolean> {
|
||||
return mergeLiveData(this, other).map {
|
||||
(it.first != null && it.first == true) || ( it.second != null && it.second == true)
|
||||
return this.switchMap { value1 ->
|
||||
other.map { value2 ->
|
||||
value1 || value2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun LiveData<Boolean>.and(other: LiveData<Boolean>): LiveData<Boolean> {
|
||||
return mergeLiveData(this, other).map {
|
||||
(it.first != null && it.first == true) && ( it.second != null && it.second == true)
|
||||
return this.switchMap { value1 ->
|
||||
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() {
|
||||
class ItemWrapper<R>(val value: LiveData<R>, var used: Boolean)
|
||||
|
||||
|
|
|
@ -65,6 +65,10 @@ class AppLogic(
|
|||
}
|
||||
}.ignoreUnchanged()
|
||||
|
||||
private val foregroundAppQueryInterval = database.config().getForegroundAppQueryIntervalAsync().apply { observeForever { } }
|
||||
fun getForegroundAppQueryInterval() = foregroundAppQueryInterval.value ?: 0L
|
||||
|
||||
val defaultUserLogic = DefaultUserLogic(this)
|
||||
val backgroundTaskLogic = BackgroundTaskLogic(this)
|
||||
val appSetupLogic = AppSetupLogic(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.RuntimePermissionStatus
|
||||
import io.timelimit.android.ui.user.create.DefaultCategories
|
||||
import io.timelimit.android.util.AndroidVersion
|
||||
import java.util.*
|
||||
|
||||
class AppSetupLogic(private val appLogic: AppLogic) {
|
||||
|
@ -84,7 +85,15 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
|||
manipulationTriedDisablingDeviceAdmin = false,
|
||||
manipulationDidReboot = 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)
|
||||
|
@ -139,7 +148,9 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
|||
blockedMinutesInWeek = ImmutableBitmask((BitSet())),
|
||||
extraTimeInMillis = 0,
|
||||
temporarilyBlocked = false,
|
||||
parentCategoryId = ""
|
||||
parentCategoryId = "",
|
||||
blockAllNotifications = false,
|
||||
timeWarnings = 0
|
||||
))
|
||||
|
||||
appLogic.database.category().addCategory(Category(
|
||||
|
@ -149,7 +160,9 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
|||
blockedMinutesInWeek = defaultCategories.allowedGamesBlockedTimes,
|
||||
extraTimeInMillis = 0,
|
||||
temporarilyBlocked = false,
|
||||
parentCategoryId = ""
|
||||
parentCategoryId = "",
|
||||
blockAllNotifications = false,
|
||||
timeWarnings = 0
|
||||
))
|
||||
|
||||
// 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.getMinuteOfWeek
|
||||
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.android.AccessibilityService
|
||||
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import io.timelimit.android.util.AndroidVersion
|
||||
import io.timelimit.android.util.TimeTextUtil
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.*
|
||||
|
||||
class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||
var pauseBackgroundLoop = false
|
||||
|
||||
companion object {
|
||||
private const val CHECK_PERMISSION_INTERVAL = 10 * 1000L // all 10 seconds
|
||||
private const val BACKGROUND_SERVICE_INTERVAL = 100L // all 100 ms
|
||||
|
@ -107,12 +113,15 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
}
|
||||
}
|
||||
|
||||
private val shouldDoAutomaticSignOut = SingleItemLiveDataCacheWithRequery { -> appLogic.defaultUserLogic.hasAutomaticSignOut()}
|
||||
|
||||
private val liveDataCaches = LiveDataCaches(arrayOf(
|
||||
deviceUserEntryLive,
|
||||
childCategories,
|
||||
appCategories,
|
||||
timeLimitRules,
|
||||
usedTimesOfCategoryAndWeekByFirstDayOfWeek
|
||||
usedTimesOfCategoryAndWeekByFirstDayOfWeek,
|
||||
shouldDoAutomaticSignOut
|
||||
))
|
||||
|
||||
private var usedTimeUpdateHelper: UsedTimeItemBatchUpdateHelper? = null
|
||||
|
@ -125,6 +134,28 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
|
||||
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() {
|
||||
while (true) {
|
||||
// app must be enabled
|
||||
|
@ -132,6 +163,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
liveDataCaches.removeAllItems()
|
||||
appLogic.platformIntegration.setAppStatusMessage(null)
|
||||
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||
appLogic.enable.waitUntilValueMatches { it == true }
|
||||
|
||||
continue
|
||||
|
@ -142,9 +174,31 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
|
||||
if (deviceUserEntry == null || deviceUserEntry.type != UserType.Child) {
|
||||
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()
|
||||
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
|
||||
}
|
||||
|
@ -159,11 +213,17 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
val minuteOfWeek = getMinuteOfWeek(nowTimestamp, nowTimezone)
|
||||
|
||||
// eventually remove old used time data
|
||||
if (dayChangeTracker.reportDayChange(nowDate.dayOfEpoch) == DayChangeTracker.DayChange.NowSinceLongerTime) {
|
||||
UsedTimeDeleter.deleteOldUsedTimeItems(
|
||||
run {
|
||||
val dayChange = dayChangeTracker.reportDayChange(nowDate.dayOfEpoch)
|
||||
|
||||
fun deleteOldUsedTimes() = UsedTimeDeleter.deleteOldUsedTimeItems(
|
||||
database = appLogic.database,
|
||||
date = nowDate
|
||||
)
|
||||
|
||||
if (dayChange == DayChangeTracker.DayChange.NowSinceLongerTime) {
|
||||
deleteOldUsedTimes()
|
||||
}
|
||||
}
|
||||
|
||||
// get the categories
|
||||
|
@ -173,33 +233,63 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
// get the current status
|
||||
val isScreenOn = appLogic.platformIntegration.isScreenOn()
|
||||
|
||||
appLogic.defaultUserLogic.reportScreenOn(isScreenOn)
|
||||
|
||||
if (!isScreenOn) {
|
||||
if (temporarilyAllowedApps.isNotEmpty()) {
|
||||
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
|
||||
|
||||
if (foregroundAppPackageName == BuildConfig.APPLICATION_ID) {
|
||||
// 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)) {
|
||||
if (pauseBackgroundLoop) {
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
appTitleCache.query(foregroundAppPackageName),
|
||||
appLogic.context.getString(R.string.background_logic_whitelisted)
|
||||
title = appLogic.context.getString(R.string.background_logic_paused_title),
|
||||
text = appLogic.context.getString(R.string.background_logic_paused_text)
|
||||
))
|
||||
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||
} else if (
|
||||
(foregroundAppPackageName == BuildConfig.APPLICATION_ID) ||
|
||||
(foregroundAppPackageName != null && AndroidIntegrationApps.ignoredApps.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)) {
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
appTitleCache.query(foregroundAppPackageName),
|
||||
appLogic.context.getString(R.string.background_logic_temporarily_allowed)
|
||||
))
|
||||
showStatusMessageWithCurrentAppTitle(appLogic.context.getString(R.string.background_logic_temporarily_allowed))
|
||||
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||
} 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 }
|
||||
?: categories.find { it.id == deviceUserEntry.categoryForNotAssignedApps }
|
||||
val parentCategory = categories.find { it.id == category?.parentCategoryId }
|
||||
|
@ -207,39 +297,30 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
if (category == null) {
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
title = appTitleCache.query(foregroundAppPackageName),
|
||||
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
||||
))
|
||||
if (AndroidIntegrationApps.ignoredApps.contains(foregroundAppPackageName) == false) {
|
||||
// don't suspend system apps which are whitelisted in any version
|
||||
appLogic.platformIntegration.setSuspendedApps(listOf(foregroundAppPackageName), true)
|
||||
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
||||
}
|
||||
|
||||
openLockscreen(foregroundAppPackageName, foregroundAppActivityName)
|
||||
} else if (category.temporarilyBlocked or (parentCategory?.temporarilyBlocked == true)) {
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
title = appTitleCache.query(foregroundAppPackageName),
|
||||
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
||||
))
|
||||
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
||||
openLockscreen(foregroundAppPackageName, foregroundAppActivityName)
|
||||
} else {
|
||||
// disable time limits temporarily feature
|
||||
if (nowTimestamp < deviceUserEntry.disableLimitsUntil) {
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
title = appTitleCache.query(foregroundAppPackageName),
|
||||
text = appLogic.context.getString(R.string.background_logic_limits_disabled)
|
||||
))
|
||||
showStatusMessageWithCurrentAppTitle(appLogic.context.getString(R.string.background_logic_limits_disabled))
|
||||
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||
} else if (
|
||||
// check blocked time areas
|
||||
// directly blocked
|
||||
(category.blockedMinutesInWeek.read(minuteOfWeek)) or
|
||||
(parentCategory?.blockedMinutesInWeek?.read(minuteOfWeek) == true)
|
||||
) {
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
title = appTitleCache.query(foregroundAppPackageName),
|
||||
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
||||
))
|
||||
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
||||
openLockscreen(foregroundAppPackageName, foregroundAppActivityName)
|
||||
} else {
|
||||
// check time limits
|
||||
val rules = timeLimitRules.get(category.id).waitForNonNullValue()
|
||||
|
@ -251,10 +332,11 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
// unlimited
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
|
||||
appLogic.context.getString(R.string.background_logic_no_timelimit)
|
||||
))
|
||||
showStatusMessageWithCurrentAppTitle(
|
||||
text = appLogic.context.getString(R.string.background_logic_no_timelimit),
|
||||
titlePrefix = category.title + " - "
|
||||
)
|
||||
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||
} else {
|
||||
val usedTimes = usedTimesOfCategoryAndWeekByFirstDayOfWeek.get(Pair(category.id, nowDate.dayOfEpoch - nowDate.dayOfWeek)).waitForNonNullValue()
|
||||
val parentUsedTimes = parentCategory?.let {
|
||||
|
@ -317,42 +399,57 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
|
||||
usedTimeUpdateHelper?.commit(appLogic)
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
|
||||
appLogic.context.getString(R.string.background_logic_no_timelimit)
|
||||
))
|
||||
showStatusMessageWithCurrentAppTitle(
|
||||
text = appLogic.context.getString(R.string.background_logic_no_timelimit),
|
||||
titlePrefix = category.title + " - "
|
||||
)
|
||||
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||
} else {
|
||||
// time limited
|
||||
if (remaining.includingExtraTime > 0) {
|
||||
var subtractExtraTime: Boolean
|
||||
|
||||
if (remaining.default == 0L) {
|
||||
// using extra time
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
|
||||
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
|
||||
showStatusMessageWithCurrentAppTitle(
|
||||
text = appLogic.context.getString(R.string.background_logic_using_extra_time, TimeTextUtil.remaining(remaining.includingExtraTime.toInt(), appLogic.context)),
|
||||
titlePrefix = category.title + " - "
|
||||
)
|
||||
}
|
||||
subtractExtraTime = true
|
||||
} else {
|
||||
// using normal contingent
|
||||
showStatusMessageWithCurrentAppTitle(
|
||||
text = TimeTextUtil.remaining(remaining.default.toInt(), appLogic.context),
|
||||
titlePrefix = category.title + " - "
|
||||
)
|
||||
subtractExtraTime = false
|
||||
}
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
category.title + " - " + appTitleCache.query(foregroundAppPackageName),
|
||||
TimeTextUtil.remaining(remaining.default.toInt(), appLogic.context)
|
||||
))
|
||||
|
||||
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||
if (isScreenOn) {
|
||||
// never save more than a second of used time
|
||||
val timeToSubtract = Math.min(previousMainLogicExecutionTime, MAX_USED_TIME_PER_ROUND)
|
||||
|
||||
newUsedTimeItemBatchUpdateHelper.addUsedTime(
|
||||
Math.min(previousMainLogicExecutionTime, MAX_USED_TIME_PER_ROUND), // never save more than a second of used time
|
||||
false,
|
||||
timeToSubtract,
|
||||
subtractExtraTime,
|
||||
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 {
|
||||
|
@ -360,11 +457,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
|
||||
newUsedTimeItemBatchUpdateHelper.commit(appLogic)
|
||||
|
||||
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
|
||||
title = appTitleCache.query(foregroundAppPackageName),
|
||||
text = appLogic.context.getString(R.string.background_logic_opening_lockscreen)
|
||||
))
|
||||
appLogic.platformIntegration.showAppLockScreen(foregroundAppPackageName)
|
||||
openLockscreen(foregroundAppPackageName, foregroundAppActivityName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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_text)
|
||||
))
|
||||
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||
}
|
||||
} catch (ex: SecurityException) {
|
||||
// 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_permission)
|
||||
))
|
||||
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
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_internal)
|
||||
))
|
||||
appLogic.platformIntegration.setShowBlockingOverlay(false)
|
||||
}
|
||||
|
||||
liveDataCaches.reportLoopDone()
|
||||
|
@ -413,10 +509,11 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
if (deviceEntry != null) {
|
||||
if (deviceEntry.currentAppVersion != currentAppVersion) {
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
UpdateDeviceStatusAction.empty.copy(
|
||||
action = UpdateDeviceStatusAction.empty.copy(
|
||||
newAppVersion = currentAppVersion
|
||||
),
|
||||
appLogic
|
||||
appLogic = appLogic,
|
||||
ignoreIfDeviceIsNotConfigured = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -446,10 +543,11 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
|
||||
if (deviceEntry?.considerRebootManipulation == true) {
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
UpdateDeviceStatusAction.empty.copy(
|
||||
action = UpdateDeviceStatusAction.empty.copy(
|
||||
didReboot = true
|
||||
),
|
||||
appLogic
|
||||
appLogic = appLogic,
|
||||
ignoreIfDeviceIsNotConfigured = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -463,6 +561,9 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
val protectionLevel = appLogic.platformIntegration.getCurrentProtectionLevel()
|
||||
val usageStatsPermission = appLogic.platformIntegration.getForegroundAppPermissionStatus()
|
||||
val notificationAccess = appLogic.platformIntegration.getNotificationAccessPermissionStatus()
|
||||
val overlayPermission = appLogic.platformIntegration.getOverlayPermissionStatus()
|
||||
val accessibilityService = appLogic.platformIntegration.isAccessibilityServiceEnabled()
|
||||
val qOrLater = AndroidVersion.qOrLater
|
||||
|
||||
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) {
|
||||
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
|
||||
* 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.Transformations
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.data.model.Category
|
||||
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.data.model.*
|
||||
import io.timelimit.android.date.DateInTimezone
|
||||
import io.timelimit.android.date.getMinuteOfWeek
|
||||
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
|
||||
|
@ -37,69 +34,106 @@ enum class BlockingReason {
|
|||
TemporarilyBlocked,
|
||||
BlockedAtThisTime,
|
||||
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) {
|
||||
companion object {
|
||||
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
|
||||
|
||||
return appLogic.enable.switchMap {
|
||||
enabled ->
|
||||
|
||||
if (enabled == null || enabled == false) {
|
||||
liveDataFromValue(BlockingReason.None)
|
||||
liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false) as BlockingReasonDetail)
|
||||
} else {
|
||||
appLogic.deviceUserEntry.switchMap {
|
||||
user ->
|
||||
|
||||
if (user == null || user.type != UserType.Child) {
|
||||
liveDataFromValue(BlockingReason.None)
|
||||
liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false) as BlockingReasonDetail)
|
||||
} 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) {
|
||||
Log.d(LOG_TAG, "step 2")
|
||||
}
|
||||
|
||||
// check internal whitelist
|
||||
if (packageName == BuildConfig.APPLICATION_ID) {
|
||||
return liveDataFromValue(BlockingReason.None)
|
||||
return liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false))
|
||||
} else if (AndroidIntegrationApps.ignoredApps.contains(packageName)) {
|
||||
return liveDataFromValue(BlockingReason.None)
|
||||
return liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false))
|
||||
} 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) {
|
||||
Log.d(LOG_TAG, "step 3")
|
||||
}
|
||||
|
||||
// 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 ->
|
||||
|
||||
if (temporarilyAllowedApps.contains(packageName)) {
|
||||
liveDataFromValue(BlockingReason.None)
|
||||
liveDataFromValue(NoBlockingReason.getInstance(areNotificationsBlocked = false) as BlockingReasonDetail)
|
||||
} 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) {
|
||||
Log.d(LOG_TAG, "step 4")
|
||||
}
|
||||
|
@ -107,13 +141,27 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
return appLogic.database.category().getCategoriesByChildId(child.id).switchMap {
|
||||
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 ->
|
||||
|
||||
if (categoryApp == null) {
|
||||
null
|
||||
} else {
|
||||
childCategories.find { it.id == categoryApp.categoryId }
|
||||
childCategories.find { it.id == categoryApp.first.categoryId }?.let { it to categoryApp.second }
|
||||
}
|
||||
}
|
||||
}.switchMap {
|
||||
|
@ -127,22 +175,52 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
|
||||
defaultCategory.switchMap { categoryEntry2 ->
|
||||
if (categoryEntry2 == null) {
|
||||
liveDataFromValue(BlockingReason.NotPartOfAnCategory)
|
||||
liveDataFromValue(
|
||||
BlockedReasonDetails(
|
||||
areNotificationsBlocked = false,
|
||||
level = BlockingLevel.App,
|
||||
reason = BlockingReason.NotPartOfAnCategory,
|
||||
categoryId = null
|
||||
) as BlockingReasonDetail
|
||||
)
|
||||
} else {
|
||||
getBlockingReasonStep4Point5(categoryEntry2, child, timeZone, false)
|
||||
getBlockingReasonStep4Point5(categoryEntry2, child, timeZone, false, BlockingLevel.App)
|
||||
}
|
||||
}
|
||||
} 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) {
|
||||
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) {
|
||||
return liveDataFromValue(BlockingReason.TemporarilyBlocked)
|
||||
}
|
||||
|
@ -152,8 +230,10 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
if (child.disableLimitsUntil == 0L) {
|
||||
areLimitsDisabled = liveDataFromValue(false)
|
||||
} else {
|
||||
areLimitsDisabled = timeInMillis.map { timeInMillis ->
|
||||
child.disableLimitsUntil > timeInMillis
|
||||
areLimitsDisabled = getTemporarilyTrustedTimeInMillis().map {
|
||||
trustedTimeInMillis ->
|
||||
|
||||
trustedTimeInMillis != null && child.disableLimitsUntil > trustedTimeInMillis
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,7 +251,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
if (parentCategory == null) {
|
||||
liveDataFromValue(BlockingReason.None)
|
||||
} else {
|
||||
getBlockingReasonStep4Point5(parentCategory, child, timeZone, true)
|
||||
getBlockingReasonStep4Point7(parentCategory, child, timeZone, true, blockingLevel)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -185,7 +265,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
Log.d(LOG_TAG, "step 5")
|
||||
}
|
||||
|
||||
return Transformations.switchMap(getMinuteOfWeekLive(appLogic.timeApi, timeZone)) {
|
||||
return Transformations.switchMap(getTrustedMinuteOfWeekLive(appLogic.timeApi, timeZone)) {
|
||||
trustedMinuteOfWeek ->
|
||||
|
||||
if (category.blockedMinutesInWeek.dataNotToModify.isEmpty) {
|
||||
|
@ -203,7 +283,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
Log.d(LOG_TAG, "step 6")
|
||||
}
|
||||
|
||||
return getDateLive(appLogic.timeApi, timeZone).switchMap {
|
||||
return getTrustedDateLive(appLogic.timeApi, timeZone).switchMap {
|
||||
nowTrustedDate ->
|
||||
|
||||
appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id).switchMap {
|
||||
|
@ -212,12 +292,20 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
|
|||
if (rules.isEmpty()) {
|
||||
liveDataFromValue(BlockingReason.None)
|
||||
} 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> {
|
||||
if (BuildConfig.DEBUG) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMinuteOfWeekLive(api: TimeApi, timeZone: TimeZone): LiveData<Int> = liveDataFromFunction {
|
||||
getMinuteOfWeek(api.getCurrentTimeInMillis(), timeZone)
|
||||
}.ignoreUnchanged()
|
||||
private fun getTrustedMinuteOfWeekLive(api: TimeApi, timeZone: TimeZone): LiveData<Int> {
|
||||
return object: LiveData<Int>() {
|
||||
fun update() {
|
||||
val timeInMillis = appLogic.timeApi.getCurrentTimeInMillis()
|
||||
|
||||
private fun getDateLive(api: TimeApi, timeZone: TimeZone): LiveData<DateInTimezone> = liveDataFromFunction {
|
||||
DateInTimezone.newInstance(api.getCurrentTimeInMillis(), timeZone)
|
||||
value = getMinuteOfWeek(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()
|
||||
}
|
||||
|
||||
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 io.timelimit.android.coroutines.runAsyncExpectForever
|
||||
import io.timelimit.android.data.model.AppActivity
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.sync.actions.AddInstalledAppsAction
|
||||
import io.timelimit.android.sync.actions.InstalledApp
|
||||
import io.timelimit.android.sync.actions.RemoveInstalledAppsAction
|
||||
import io.timelimit.android.sync.actions.*
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
@ -36,12 +35,13 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
|
|||
|
||||
init {
|
||||
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() }
|
||||
}
|
||||
|
||||
private suspend fun syncLoop() {
|
||||
requestSync.postValue(true)
|
||||
|
||||
while (true) {
|
||||
requestSync.waitUntilValueMatches { it == true }
|
||||
requestSync.value = false
|
||||
|
@ -55,12 +55,10 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
|
|||
|
||||
private suspend fun doSyncNow() {
|
||||
doSyncLock.withLock {
|
||||
val userEntry = appLogic.deviceUserEntry.waitForNullableValue()
|
||||
|
||||
if (userEntry == null || userEntry.type != UserType.Child) {
|
||||
return@withLock
|
||||
}
|
||||
val deviceEntry = appLogic.deviceEntry.waitForNullableValue() ?: return@withLock
|
||||
val deviceId = deviceEntry.id
|
||||
|
||||
run {
|
||||
val currentlyInstalled = appLogic.platformIntegration.getLocalApps().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
|
||||
if (itemsToRemove.isNotEmpty()) {
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
RemoveInstalledAppsAction(packageNames = itemsToRemove.keys.toList()),
|
||||
appLogic
|
||||
action = RemoveInstalledAppsAction(packageNames = itemsToRemove.keys.toList()),
|
||||
appLogic = appLogic,
|
||||
ignoreIfDeviceIsNotConfigured = true
|
||||
)
|
||||
}
|
||||
|
||||
if (itemsToAdd.isNotEmpty()) {
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
AddInstalledAppsAction(
|
||||
apps = itemsToAdd.map {
|
||||
(_, app) ->
|
||||
action = AddInstalledAppsAction(
|
||||
apps = itemsToAdd.map { (_, app) ->
|
||||
|
||||
InstalledApp(
|
||||
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
|
||||
} else {
|
||||
ApplyActionUtil.applyAppLogicAction(
|
||||
AddUsedTimeAction(
|
||||
action = AddUsedTimeAction(
|
||||
categoryId = childCategoryId,
|
||||
timeToAdd = timeToAdd,
|
||||
dayOfEpoch = date.dayOfEpoch,
|
||||
extraTimeToSubtract = extraTimeToSubtract
|
||||
),
|
||||
logic
|
||||
appLogic = logic,
|
||||
ignoreIfDeviceIsNotConfigured = true
|
||||
)
|
||||
|
||||
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() {
|
||||
init {
|
||||
IdGenerator.assertIdValid(categoryId)
|
||||
|
@ -126,6 +144,11 @@ data class UpdateCategoryTemporarilyBlockedAction(val categoryId: String, val bl
|
|||
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() {
|
||||
// category id can be empty
|
||||
|
||||
|
@ -155,16 +178,22 @@ data class UpdateDeviceStatusAction(
|
|||
val newProtectionLevel: ProtectionLevel?,
|
||||
val newUsageStatsPermissionStatus: RuntimePermissionStatus?,
|
||||
val newNotificationAccessPermission: NewPermissionStatus?,
|
||||
val newOverlayPermission: RuntimePermissionStatus?,
|
||||
val newAccessibilityServiceEnabled: Boolean?,
|
||||
val newAppVersion: Int?,
|
||||
val didReboot: Boolean
|
||||
val didReboot: Boolean,
|
||||
val isQOrLaterNow: Boolean
|
||||
): AppLogicAction() {
|
||||
companion object {
|
||||
val empty = UpdateDeviceStatusAction(
|
||||
newProtectionLevel = null,
|
||||
newUsageStatsPermissionStatus = null,
|
||||
newNotificationAccessPermission = null,
|
||||
newOverlayPermission = null,
|
||||
newAccessibilityServiceEnabled = null,
|
||||
newAppVersion = null,
|
||||
didReboot = false
|
||||
didReboot = false,
|
||||
isQOrLaterNow = false
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -182,6 +211,8 @@ data class IgnoreManipulationAction(
|
|||
val ignoreAppDowngrade: Boolean,
|
||||
val ignoreNotificationAccessManipulation: Boolean,
|
||||
val ignoreUsageStatsAccessManipulation: Boolean,
|
||||
val ignoreOverlayPermissionManipulation: Boolean,
|
||||
val ignoreAccessibilityServiceManipulation: Boolean,
|
||||
val ignoreReboot: Boolean,
|
||||
val ignoreHadManipulation: Boolean
|
||||
): 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() {
|
||||
init {
|
||||
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() {
|
||||
init {
|
||||
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 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
|
||||
|
||||
object ApplyActionUtil {
|
||||
suspend fun applyAppLogicAction(action: AppLogicAction, appLogic: AppLogic) {
|
||||
applyAppLogicAction(action, appLogic.database, appLogic.manipulationLogic)
|
||||
suspend fun applyAppLogicAction(
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ package io.timelimit.android.sync.actions.dispatch
|
|||
|
||||
import io.timelimit.android.data.Database
|
||||
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.integration.platform.NewPermissionStatusUtil
|
||||
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 (device.currentAppVersion != action.newAppVersion) {
|
||||
device = device.copy(
|
||||
|
@ -167,6 +204,10 @@ object LocalDatabaseAppLogicActionDispatcher {
|
|||
)
|
||||
}
|
||||
|
||||
if (action.isQOrLaterNow && !device.qOrLater) {
|
||||
device = device.copy(qOrLater = true)
|
||||
}
|
||||
|
||||
database.device().updateDeviceEntry(device)
|
||||
|
||||
if (device.hasActiveManipulationWarning) {
|
||||
|
@ -186,6 +227,52 @@ object LocalDatabaseAppLogicActionDispatcher {
|
|||
|
||||
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
|
||||
}
|
||||
}.let { }
|
||||
|
|
|
@ -74,7 +74,9 @@ object LocalDatabaseParentActionDispatcher {
|
|||
blockedMinutesInWeek = ImmutableBitmask(BitSet()),
|
||||
extraTimeInMillis = 0,
|
||||
temporarilyBlocked = false,
|
||||
parentCategoryId = ""
|
||||
parentCategoryId = "",
|
||||
blockAllNotifications = false,
|
||||
timeWarnings = 0
|
||||
))
|
||||
}
|
||||
is DeleteCategoryAction -> {
|
||||
|
@ -271,6 +273,14 @@ object LocalDatabaseParentActionDispatcher {
|
|||
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) {
|
||||
deviceEntry = deviceEntry.copy(manipulationDidReboot = false)
|
||||
}
|
||||
|
@ -328,6 +338,26 @@ object LocalDatabaseParentActionDispatcher {
|
|||
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 -> {
|
||||
val deviceEntry = database.device().getDeviceByIdSync(action.deviceId)
|
||||
?: 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 { }
|
||||
|
||||
database.setTransactionSuccessful()
|
||||
|
|
|
@ -48,6 +48,8 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
|||
|
||||
private val currentNavigatorFragment = MutableLiveData<Fragment>()
|
||||
|
||||
override var ignoreStop: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
@ -104,7 +106,7 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
|||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
if (!isChangingConfigurations) {
|
||||
if ((!isChangingConfigurations) && (!ignoreStop)) {
|
||||
getActivityViewModel().logOut()
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +114,10 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
|
|||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
if ((intent?.flags ?: 0) and Intent.FLAG_ACTIVITY_REORDER_TO_FRONT == Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) {
|
||||
return
|
||||
}
|
||||
|
||||
getNavController().popBackStack(R.id.overviewFragment, true)
|
||||
getNavController().handleDeepLink(
|
||||
getNavController().createDeepLink()
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.contacts
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.databinding.AddItemViewBinding
|
||||
import io.timelimit.android.databinding.ContactsItemBinding
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class ContactsAdapter: RecyclerView.Adapter<ContactsViewHolder>() {
|
||||
companion object {
|
||||
private const val TYPE_INTRO = 1
|
||||
private const val TYPE_ITEM = 2
|
||||
private const val TYPE_ADD = 3
|
||||
}
|
||||
|
||||
var items: List<ContactsItem>? by Delegates.observable(null as List<ContactsItem>?) { _, _, _ -> notifyDataSetChanged() }
|
||||
var handlers: ContactsHandlers? = null
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items?.size ?: 0
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
val item = items!![position]
|
||||
|
||||
return when (item) {
|
||||
is IntroContactsItem -> Long.MAX_VALUE
|
||||
is AddContactsItem -> Long.MAX_VALUE - 1
|
||||
is ContactContactsItem -> item.item.id.toLong()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = when (items!![position]) {
|
||||
is IntroContactsItem -> TYPE_INTRO
|
||||
is ContactContactsItem -> TYPE_ITEM
|
||||
is AddContactsItem -> TYPE_ADD
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactsViewHolder = when (viewType) {
|
||||
TYPE_INTRO -> ContactsStaticHolder(
|
||||
LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.contacts_intro, parent, false)
|
||||
)
|
||||
TYPE_ITEM -> ContactsItemHolder(
|
||||
ContactsItemBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
)
|
||||
TYPE_ADD -> ContactsStaticHolder(
|
||||
AddItemViewBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
).let {
|
||||
it.label = parent.context.getString(R.string.contacts_add)
|
||||
|
||||
it.root.setOnClickListener {
|
||||
handlers?.onAddContactClicked()
|
||||
}
|
||||
|
||||
it.root
|
||||
}
|
||||
)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ContactsViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is ContactsStaticHolder -> {/* nothing to do */}
|
||||
is ContactsItemHolder -> {
|
||||
val item = items!![position]
|
||||
|
||||
item as ContactContactsItem
|
||||
|
||||
holder.view.title = item.item.title
|
||||
holder.view.phone = item.item.phone
|
||||
|
||||
holder.view.card.setOnClickListener { handlers?.onContactClicked(item) }
|
||||
holder.view.card.setOnLongClickListener { handlers?.onContactLongClicked(item) ?: false }
|
||||
|
||||
holder.view.executePendingBindings()
|
||||
|
||||
null
|
||||
}
|
||||
}.let {/* require handling all cases */}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ContactsViewHolder(root: View): RecyclerView.ViewHolder(root)
|
||||
class ContactsStaticHolder(root: View): ContactsViewHolder(root)
|
||||
class ContactsItemHolder(val view: ContactsItemBinding): ContactsViewHolder(view.root)
|
||||
|
||||
interface ContactsHandlers {
|
||||
fun onAddContactClicked()
|
||||
fun onContactLongClicked(item: ContactContactsItem): Boolean
|
||||
fun onContactClicked(item: ContactContactsItem)
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.contacts
|
||||
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import io.timelimit.android.R
|
||||
import android.provider.ContactsContract
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.data.model.AllowedContact
|
||||
import io.timelimit.android.databinding.ContactsFragmentBinding
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.ui.MainActivity
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||
import io.timelimit.android.util.PhoneNumberUtils
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
||||
class ContactsFragment : Fragment() {
|
||||
companion object {
|
||||
private const val LOG_TAG = "ContactsFragment"
|
||||
private const val REQ_SELECT_CONTACT = 1
|
||||
private const val REQ_CALL_PERMISSION = 2
|
||||
}
|
||||
|
||||
private val model: ContactsModel by lazy {
|
||||
ViewModelProviders.of(this).get(ContactsModel::class.java)
|
||||
}
|
||||
|
||||
private val activityModelHolder: ActivityViewModelHolder by lazy { activity as ActivityViewModelHolder }
|
||||
private val auth: ActivityViewModel by lazy { activityModelHolder.getActivityViewModel() }
|
||||
private var numberToCallWithPermission: String? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = ContactsFragmentBinding.inflate(inflater, container, false)
|
||||
val adapter = ContactsAdapter()
|
||||
|
||||
model.listItems.observe(this, Observer { adapter.items = it })
|
||||
|
||||
binding.recycler.layoutManager = LinearLayoutManager(context)
|
||||
binding.recycler.adapter = adapter
|
||||
|
||||
adapter.handlers = object: ContactsHandlers {
|
||||
override fun onAddContactClicked() {
|
||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||
activityModelHolder.ignoreStop = true
|
||||
|
||||
showContactSelection()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContactLongClicked(item: ContactContactsItem): Boolean {
|
||||
removeItem(item.item)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onContactClicked(item: ContactContactsItem) {
|
||||
startCall(item.item.phone)
|
||||
}
|
||||
}
|
||||
|
||||
ItemTouchHelper(object: ItemTouchHelper.Callback() {
|
||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||
val item = adapter.items!![viewHolder.adapterPosition]
|
||||
|
||||
if (item is ContactContactsItem && auth.isParentAuthenticated()) {
|
||||
return makeMovementFlags(0, ItemTouchHelper.START or ItemTouchHelper.END)
|
||||
} else if (item is IntroContactsItem) {
|
||||
return makeMovementFlags(0, ItemTouchHelper.START or ItemTouchHelper.END)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
||||
// ignore
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
val item = adapter.items!![viewHolder.adapterPosition]
|
||||
|
||||
if (item is ContactContactsItem) {
|
||||
removeItem(item.item)
|
||||
} else if (item is IntroContactsItem) {
|
||||
model.hideIntro()
|
||||
}
|
||||
}
|
||||
}).attachToRecyclerView(binding.recycler)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun showContactSelection() {
|
||||
startActivityForResult(
|
||||
Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
|
||||
.setType(ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE),
|
||||
REQ_SELECT_CONTACT
|
||||
)
|
||||
}
|
||||
|
||||
private fun removeItem(item: AllowedContact) {
|
||||
if (auth.isParentAuthenticated()) {
|
||||
model.removeContact(item.id)
|
||||
|
||||
Snackbar.make(view!!, getString(R.string.contacts_snackbar_removed, item.title), Snackbar.LENGTH_SHORT)
|
||||
.setAction(R.string.generic_undo) {
|
||||
model.addContact(item)
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
Snackbar.make(view!!, R.string.contacts_snackbar_remove_auth, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startCall(number: String) {
|
||||
if (ContextCompat.checkSelfPermission(context!!, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
|
||||
val logic = DefaultAppLogic.with(context!!)
|
||||
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_CALL, Uri.parse("tel:" + PhoneNumberUtils.normalizeNumber(number)))
|
||||
|
||||
logic.backgroundTaskLogic.pauseBackgroundLoop = true
|
||||
|
||||
startActivity(intent)
|
||||
|
||||
runAsync {
|
||||
delay(500)
|
||||
|
||||
startActivity(
|
||||
Intent(context!!, MainActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
)
|
||||
|
||||
delay(200)
|
||||
|
||||
logic.backgroundTaskLogic.pauseBackgroundLoop = false
|
||||
|
||||
delay(500)
|
||||
|
||||
Snackbar.make(view!!, R.string.contacts_snackbar_call_started, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "could not start call", ex)
|
||||
}
|
||||
|
||||
logic.backgroundTaskLogic.pauseBackgroundLoop = false
|
||||
|
||||
Snackbar.make(view!!, R.string.contacts_snackbar_call_failed, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
numberToCallWithPermission = number
|
||||
requestPermissions(arrayOf(Manifest.permission.CALL_PHONE), REQ_CALL_PERMISSION)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (requestCode == REQ_SELECT_CONTACT) {
|
||||
activityModelHolder.ignoreStop = false
|
||||
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
data?.data?.let { contactData ->
|
||||
val cursor = context!!.contentResolver.query(contactData, null, null, null, null)
|
||||
|
||||
cursor?.use {
|
||||
if (cursor.moveToFirst()) {
|
||||
val title = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME))
|
||||
val phoneNumber = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
|
||||
|
||||
model.addContact(title = title, phoneNumber = phoneNumber)
|
||||
|
||||
Snackbar.make(view!!, R.string.contacts_snackbar_added, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
if (requestCode == REQ_CALL_PERMISSION) {
|
||||
if (grantResults.size == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
numberToCallWithPermission?.let { number -> startCall(number) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.contacts
|
||||
|
||||
import io.timelimit.android.data.model.AllowedContact
|
||||
|
||||
sealed class ContactsItem
|
||||
object IntroContactsItem: ContactsItem()
|
||||
object AddContactsItem: ContactsItem()
|
||||
data class ContactContactsItem(val item: AllowedContact): ContactsItem()
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.contacts
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.data.model.AllowedContact
|
||||
import io.timelimit.android.data.model.HintsToShow
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.switchMap
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
|
||||
class ContactsModel(application: Application): AndroidViewModel(application) {
|
||||
private val appLogic = DefaultAppLogic.with(application)
|
||||
private val allowedContacts = appLogic.database.allowedContact().getAllowedContactsLive()
|
||||
private val didHideIntro = appLogic.database.config().wereHintsShown(HintsToShow.CONTACTS_INTRO)
|
||||
|
||||
private val convertedContactItems = allowedContacts.map { items -> items.map { ContactContactsItem(it) } }
|
||||
private val baseListItems = convertedContactItems.map { list -> list + listOf(AddContactsItem) }
|
||||
|
||||
val listItems = didHideIntro.switchMap { hideIntro ->
|
||||
baseListItems.map { baseItems ->
|
||||
if (hideIntro) {
|
||||
baseItems
|
||||
} else {
|
||||
listOf(IntroContactsItem) + baseItems
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addContact(title: String, phoneNumber: String) {
|
||||
Threads.database.submit {
|
||||
appLogic.database.allowedContact().addContactSync(AllowedContact(
|
||||
id = 0,
|
||||
phone = phoneNumber,
|
||||
title = title
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fun addContact(item: AllowedContact) {
|
||||
Threads.database.submit { appLogic.database.allowedContact().addContactSync(item) }
|
||||
}
|
||||
|
||||
fun removeContact(id: Int) {
|
||||
Threads.database.submit { appLogic.database.allowedContact().removeContactSync(id) }
|
||||
}
|
||||
|
||||
fun hideIntro() {
|
||||
Threads.database.submit { appLogic.database.config().setHintsShownSync(HintsToShow.CONTACTS_INTRO) }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,12 +32,18 @@ import io.timelimit.android.ui.main.ActivityViewModelHolder
|
|||
class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
|
||||
companion object {
|
||||
private const val EXTRA_PACKAGE_NAME = "packageName"
|
||||
private const val EXTRA_ACTIVITY_NAME = "activityName"
|
||||
private const val LOGIN_DIALOG_TAG = "loginDialog"
|
||||
|
||||
fun start(context: Context, packageName: String) {
|
||||
fun start(context: Context, packageName: String, activityName: String?) {
|
||||
context.startActivity(
|
||||
Intent(context, LockActivity::class.java)
|
||||
.putExtra(EXTRA_PACKAGE_NAME, packageName)
|
||||
.apply {
|
||||
if (activityName != null) {
|
||||
putExtra(EXTRA_ACTIVITY_NAME, activityName)
|
||||
}
|
||||
}
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||
|
@ -45,18 +51,29 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
|
|||
}
|
||||
}
|
||||
|
||||
override var ignoreStop: Boolean = false
|
||||
|
||||
val blockedPackageName: String by lazy {
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.lock_activity)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, LockFragment.newInstance(blockedPackageName))
|
||||
.replace(R.id.container, LockFragment.newInstance(blockedPackageName, blockedActivityName))
|
||||
.commitNow()
|
||||
|
||||
stopMediaPlayback()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,12 +100,12 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
|
|||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
if (!isChangingConfigurations) {
|
||||
if ((!isChangingConfigurations) && (!ignoreStop)) {
|
||||
getActivityViewModel().logOut()
|
||||
}
|
||||
}
|
||||
|
||||
fun lockTaskModeWorkaround() {
|
||||
private fun lockTaskModeWorkaround() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val platformIntegration = DefaultAppLogic.with(this).platformIntegration
|
||||
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() {
|
||||
// do nothing because going back would open the blocked app again
|
||||
// super.onBackPressed()
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package io.timelimit.android.ui.lock
|
||||
|
||||
import android.content.Intent
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
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.databinding.LockFragmentBinding
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.BlockingReason
|
||||
import io.timelimit.android.logic.BlockingReasonUtil
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.logic.*
|
||||
import io.timelimit.android.sync.actions.AddCategoryAppsAction
|
||||
import io.timelimit.android.sync.actions.IncrementCategoryExtraTimeAction
|
||||
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.advanced.managedisabletimelimits.ManageDisableTimelimitsViewHelper
|
||||
import io.timelimit.android.ui.manage.child.category.create.CreateCategoryDialogFragment
|
||||
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
||||
|
||||
class LockFragment : Fragment() {
|
||||
companion object {
|
||||
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 arguments = Bundle()
|
||||
|
||||
arguments.putString(EXTRA_PACKAGE_NAME, packageName)
|
||||
|
||||
if (activity != null) {
|
||||
arguments.putString(EXTRA_ACTIVITY, activity)
|
||||
}
|
||||
|
||||
result.arguments = arguments
|
||||
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 logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
||||
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 {
|
||||
val binding = LockFragmentBinding.inflate(layoutInflater, container, false)
|
||||
|
@ -83,8 +93,14 @@ class LockFragment : Fragment() {
|
|||
doesSupportAuth = liveDataFromValue(true)
|
||||
)
|
||||
|
||||
val enableActivityLevelBlocking = logic.deviceEntry.map { it?.enableActivityLevelBlocking ?: false }
|
||||
|
||||
binding.packageName = packageName
|
||||
|
||||
enableActivityLevelBlocking.observe(this, Observer {
|
||||
binding.activityName = if (it) activityName?.removePrefix(packageName) else null
|
||||
})
|
||||
|
||||
if (title != null) {
|
||||
binding.appTitle = title
|
||||
} else {
|
||||
|
@ -94,11 +110,16 @@ class LockFragment : Fragment() {
|
|||
binding.appIcon.setImageDrawable(logic.platformIntegration.getAppIcon(packageName))
|
||||
|
||||
blockingReason.observe(this, Observer {
|
||||
if (it == BlockingReason.None) {
|
||||
activity!!.finish()
|
||||
} else {
|
||||
binding.reason = it
|
||||
when (it) {
|
||||
is NoBlockingReason -> activity!!.finish()
|
||||
is BlockedReasonDetails -> {
|
||||
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 {
|
||||
|
@ -124,13 +145,14 @@ class LockFragment : Fragment() {
|
|||
} else {
|
||||
val (_, categoryItems) = status
|
||||
|
||||
Transformations.map(logic.database.categoryApp().getCategoryApp(
|
||||
categoryItems.map { it.id },
|
||||
packageName
|
||||
)) {
|
||||
appEntry ->
|
||||
|
||||
categoryItems.find { it.id == appEntry?.categoryId }
|
||||
blockingReason.map { reason ->
|
||||
if (reason is BlockedReasonDetails) {
|
||||
reason.categoryId
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.map { categoryId ->
|
||||
categoryItems.find { it.id == categoryId }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -196,6 +218,8 @@ class LockFragment : Fragment() {
|
|||
if (extraTimeToAdd > 0) {
|
||||
binding.extraTimeBtnOk.isEnabled = false
|
||||
|
||||
binding.extraTimeSelection.clearNumberPickerFocus()
|
||||
|
||||
val categoryId = appCategory.waitForNullableValue()?.id
|
||||
|
||||
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
|
||||
logic.deviceUserEntry.observe(this, Observer {
|
||||
child ->
|
||||
|
@ -267,9 +307,16 @@ class LockFragment : Fragment() {
|
|||
logic.platformIntegration.setSuspendedApps(listOf(packageName), false)
|
||||
|
||||
Threads.database.executeAndWait(Runnable {
|
||||
try {
|
||||
database.temporarilyAllowedApp().addTemporarilyAllowedAppSync(TemporarilyAllowedApp(
|
||||
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 val logic = DefaultAppLogic.with(application)
|
||||
val logic = DefaultAppLogic.with(application)
|
||||
private val database = logic.database
|
||||
|
||||
val shouldHighlightAuthenticationButton = MutableLiveData<Boolean>().apply { value = false }
|
||||
|
@ -115,6 +115,8 @@ class ActivityViewModel(application: Application): AndroidViewModel(application)
|
|||
authenticatedUserMetadata.value = user
|
||||
}
|
||||
|
||||
fun getAuthenticatedUser() = authenticatedUserMetadata.value
|
||||
|
||||
fun logOut() {
|
||||
authenticatedUserMetadata.value = null
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.app.Activity
|
|||
interface ActivityViewModelHolder {
|
||||
fun getActivityViewModel(): ActivityViewModel
|
||||
fun showAuthenticationScreen()
|
||||
var ignoreStop: Boolean
|
||||
}
|
||||
|
||||
fun getActivityViewModel(activity: Activity): ActivityViewModel {
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
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.AuthenticationFab
|
||||
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.*
|
||||
|
||||
class ManageCategoryFragment : Fragment(), FragmentWithCustomTitle {
|
||||
|
@ -47,7 +53,6 @@ class ManageCategoryFragment : Fragment(), FragmentWithCustomTitle {
|
|||
private val user: LiveData<User?> by lazy {
|
||||
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 var wereViewsCreated = false
|
||||
|
||||
|
@ -70,44 +75,29 @@ class ManageCategoryFragment : Fragment(), FragmentWithCustomTitle {
|
|||
|
||||
val navigation = Navigation.findNavController(view)
|
||||
|
||||
pager.adapter = adapter
|
||||
|
||||
bottom_navigation_view.setOnNavigationItemSelectedListener {
|
||||
menuItem ->
|
||||
|
||||
pager.currentItem = when(menuItem.itemId) {
|
||||
R.id.manage_category_tab_apps -> 0
|
||||
R.id.manage_category_tab_time_limit_rules -> 1
|
||||
R.id.manage_category_tab_blocked_time_areas -> 2
|
||||
R.id.manage_category_tab_usage_log -> 3
|
||||
R.id.manage_category_tab_settings -> 4
|
||||
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.manage_category_tab_apps -> CategoryAppsFragment.newInstance(params)
|
||||
R.id.manage_category_tab_time_limit_rules -> CategoryTimeLimitRulesFragment.newInstance(params)
|
||||
R.id.manage_category_tab_blocked_time_areas -> BlockedTimeAreasFragment.newInstance(params)
|
||||
R.id.manage_category_tab_usage_log -> UsageHistoryFragment.newInstance(params)
|
||||
R.id.manage_category_tab_settings -> CategorySettingsFragment.newInstance(params)
|
||||
else -> throw IllegalStateException()
|
||||
})
|
||||
.commit()
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pager.addOnPageChangeListener(object: ViewPager.OnPageChangeListener {
|
||||
override fun onPageScrollStateChanged(state: Int) {
|
||||
// ignore
|
||||
if (childFragmentManager.findFragmentById(R.id.container) == null) {
|
||||
childFragmentManager.beginTransaction()
|
||||
.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) {
|
||||
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(
|
||||
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)
|
||||
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 {
|
||||
fun onAppClicked(app: AppEntry)
|
||||
|
|
|
@ -42,7 +42,7 @@ class CategoryAppsModel(application: Application): AndroidViewModel(application)
|
|||
private val appsOfCategoryWithNames = installedApps.switchMap { allApps ->
|
||||
appsOfThisCategory.map { apps ->
|
||||
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 ->
|
||||
apps.map { (app, appEntry) ->
|
||||
if (appEntry != null) {
|
||||
AppEntry(appEntry.title, app.packageName)
|
||||
AppEntry(appEntry.title, app.packageName, app.packageNameWithoutActivityName)
|
||||
} else {
|
||||
AppEntry("app not found", app.packageName)
|
||||
AppEntry("app not found", app.packageName, app.packageNameWithoutActivityName)
|
||||
}
|
||||
}.sortedBy { it.title.toLowerCase(Locale.US) }
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import kotlin.properties.Delegates
|
|||
|
||||
class AddAppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
||||
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() }
|
||||
val selectedApps = mutableSetOf<String>()
|
||||
|
||||
|
@ -35,6 +36,8 @@ class AddAppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
|||
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onAppLongClicked(app: App) = listener?.onAppLongClicked(app) ?: false
|
||||
}
|
||||
|
||||
init {
|
||||
|
@ -86,6 +89,10 @@ class AddAppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
|||
|
||||
class ViewHolder(val binding: FragmentAddCategoryAppsItemBinding): RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
interface ItemHandlers {
|
||||
interface ItemHandlers: AddAppAdapterListener {
|
||||
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.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
|
@ -27,6 +28,7 @@ import androidx.lifecycle.Observer
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.databinding.FragmentAddCategoryAppsBinding
|
||||
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.getActivityViewModel
|
||||
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
|
||||
|
||||
class AddCategoryAppsFragment : DialogFragment() {
|
||||
|
@ -168,6 +171,26 @@ class AddCategoryAppsFragment : DialogFragment() {
|
|||
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)
|
||||
.setView(binding.root)
|
||||
.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 com.google.android.material.snackbar.Snackbar
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.databinding.FragmentCategorySettingsBinding
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
|
@ -30,6 +31,7 @@ import io.timelimit.android.sync.actions.SetCategoryExtraTimeAction
|
|||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.main.getActivityViewModel
|
||||
import io.timelimit.android.ui.manage.category.ManageCategoryFragmentArgs
|
||||
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
||||
|
||||
class CategorySettingsFragment : Fragment() {
|
||||
companion object {
|
||||
|
@ -68,6 +70,20 @@ class CategorySettingsFragment : Fragment() {
|
|||
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.editCategoryTitleGo.setOnClickListener { renameCategory() }
|
||||
|
||||
|
@ -82,6 +98,8 @@ class CategorySettingsFragment : Fragment() {
|
|||
})
|
||||
|
||||
binding.extraTimeBtnOk.setOnClickListener {
|
||||
binding.extraTimeSelection.clearNumberPickerFocus()
|
||||
|
||||
val newExtraTime = binding.extraTimeSelection.timeInMillis
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.bottomsheet.BottomSheetBehavior
|
||||
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.model.HintsToShow
|
||||
import io.timelimit.android.data.model.TimeLimitRule
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.databinding.FragmentEditTimeLimitRuleDialogBinding
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import io.timelimit.android.livedata.waitForNonNullValue
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.sync.actions.CreateTimeLimitRuleAction
|
||||
import io.timelimit.android.sync.actions.DeleteTimeLimitRuleAction
|
||||
import io.timelimit.android.sync.actions.UpdateTimeLimitRuleAction
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
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.SelectTimeSpanViewListener
|
||||
import java.nio.ByteBuffer
|
||||
|
@ -84,6 +89,23 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
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)
|
||||
?: arguments?.getParcelable<TimeLimitRule?>(PARAM_EXISTING_RULE)
|
||||
}
|
||||
|
@ -92,6 +114,7 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
|||
val view = FragmentEditTimeLimitRuleDialogBinding.inflate(layoutInflater, container, false)
|
||||
val listener = targetFragment as EditTimeLimitRuleDialogFragmentListener
|
||||
var newRule: TimeLimitRule
|
||||
val database = DefaultAppLogic.with(context!!).database
|
||||
|
||||
auth.authenticatedUser.observe(this, Observer {
|
||||
if (it == null || it.second.type != UserType.Parent) {
|
||||
|
@ -135,7 +158,7 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
|||
view.timeSpan.timeInMillis = newRule.maximumTimeInMillis.toLong()
|
||||
|
||||
val affectedDays = Math.max(0, (0..6).map { (newRule.dayMask.toInt() shr it) and 1 }.sum())
|
||||
view.timeSpan.maxDays = affectedDays - 1
|
||||
view.timeSpan.maxDays = Math.max(0, affectedDays - 1) // max prevents crash
|
||||
view.affectsMultipleDays = affectedDays >= 2
|
||||
}
|
||||
|
||||
|
@ -160,6 +183,8 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
|||
}
|
||||
|
||||
override fun onSaveRule() {
|
||||
view.timeSpan.clearNumberPickerFocus()
|
||||
|
||||
if (existingRule != null) {
|
||||
if (existingRule != newRule) {
|
||||
if (!auth.tryDispatchParentAction(
|
||||
|
@ -213,10 +238,20 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment() {
|
|||
bindRule()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setEnablePickerMode(enable: Boolean) {
|
||||
Threads.database.execute {
|
||||
database.config().setEnableAlternativeDurationSelectionSync(enable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
database.config().getEnableAlternativeDurationSelectionAsync().observe(this, Observer {
|
||||
view.timeSpan.enablePickerMode(it)
|
||||
})
|
||||
|
||||
if (existingRule != null) {
|
||||
DefaultAppLogic.with(context!!).database.timeLimitRules()
|
||||
database.timeLimitRules()
|
||||
.getTimeLimitRuleByIdLive(existingRule!!.id).observe(this, Observer {
|
||||
if (it == null) {
|
||||
// rule was deleted
|
||||
|
|
|
@ -20,6 +20,7 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
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.AuthenticationFab
|
||||
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.*
|
||||
|
||||
class ManageChildFragment : Fragment(), FragmentWithCustomTitle {
|
||||
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 child: LiveData<User?> by lazy { logic.database.user().getUserByIdLive(params.childId) }
|
||||
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||
|
@ -74,39 +77,26 @@ class ManageChildFragment : Fragment(), FragmentWithCustomTitle {
|
|||
})
|
||||
}
|
||||
|
||||
pager.adapter = adapter
|
||||
|
||||
bottom_navigation_view.setOnNavigationItemSelectedListener {
|
||||
menuItem ->
|
||||
|
||||
pager.currentItem = when (menuItem.itemId) {
|
||||
R.id.manage_child_tab_categories -> 0
|
||||
R.id.manage_child_tab_apps -> 1
|
||||
R.id.manage_child_tab_manage -> 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.manage_child_tab_categories -> ManageChildCategoriesFragment.newInstance(params)
|
||||
R.id.manage_child_tab_apps -> ChildAppsFragment.newInstance(params)
|
||||
R.id.manage_child_tab_manage -> ManageChildAdvancedFragment.newInstance(params)
|
||||
else -> throw IllegalStateException()
|
||||
})
|
||||
.commit()
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pager.addOnPageChangeListener(object: ViewPager.OnPageChangeListener {
|
||||
override fun onPageScrollStateChanged(state: Int) {
|
||||
// ignore
|
||||
if (childFragmentManager.findFragmentById(R.id.container) == null) {
|
||||
childFragmentManager.beginTransaction()
|
||||
.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 }
|
||||
|
|
|
@ -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() {
|
||||
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
|
||||
makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END)
|
||||
} 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.data.model.Device
|
||||
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.android.AdminReceiver
|
||||
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.AuthenticationFab
|
||||
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 {
|
||||
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||
|
@ -70,10 +73,6 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
activityViewModel = auth
|
||||
)
|
||||
|
||||
val userSpinnerAdapter = ArrayAdapter<String>(context!!, android.R.layout.simple_spinner_item).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
|
||||
// auth
|
||||
AuthenticationFab.manageAuthenticationFab(
|
||||
fab = binding.fab,
|
||||
|
@ -83,89 +82,41 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
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 {
|
||||
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 showUserScreen() {
|
||||
navigation.safeNavigate(
|
||||
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDeviceUserFragment(
|
||||
deviceId = args.deviceId
|
||||
),
|
||||
R.id.manageDeviceFragment
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun openNotificationAccessSettings() {
|
||||
if (binding.isThisDevice == true) {
|
||||
try {
|
||||
startActivity(
|
||||
Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
override fun showPermissionsScreen() {
|
||||
navigation.safeNavigate(
|
||||
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDevicePermissionsFragment(
|
||||
deviceId = args.deviceId
|
||||
),
|
||||
R.id.manageDeviceFragment
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.error_general,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun manageDeviceAdmin() {
|
||||
if (binding.isThisDevice == true) {
|
||||
val protectionLevel = logic.platformIntegration.getCurrentProtectionLevel()
|
||||
|
||||
if (protectionLevel == ProtectionLevel.None) {
|
||||
if (InformAboutDeviceOwnerDialogFragment.shouldShow) {
|
||||
startActivity(
|
||||
Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN)
|
||||
.putExtra(
|
||||
DevicePolicyManager.EXTRA_DEVICE_ADMIN,
|
||||
ComponentName(context!!, AdminReceiver::class.java)
|
||||
override fun showFeaturesScreen() {
|
||||
navigation.safeNavigate(
|
||||
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDeviceFeaturesFragment(
|
||||
deviceId = args.deviceId
|
||||
),
|
||||
R.id.manageDeviceFragment
|
||||
)
|
||||
)
|
||||
} else {
|
||||
InformAboutDeviceOwnerDialogFragment().show(fragmentManager!!)
|
||||
}
|
||||
} else {
|
||||
startActivity(
|
||||
Intent(Settings.ACTION_SECURITY_SETTINGS)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun editDeviceTitle() {
|
||||
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||
UpdateDeviceTitleDialogFragment.newInstance(args.deviceId).show(fragmentManager!!)
|
||||
}
|
||||
override fun showManageScreen() {
|
||||
navigation.safeNavigate(
|
||||
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDeviceAdvancedFragment(
|
||||
deviceId = args.deviceId
|
||||
),
|
||||
R.id.manageDeviceFragment
|
||||
)
|
||||
}
|
||||
|
||||
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 {
|
||||
device ->
|
||||
|
||||
|
@ -207,7 +132,6 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
} else {
|
||||
val now = logic.timeApi.getCurrentTimeInMillis()
|
||||
|
||||
binding.deviceTitle = device.name
|
||||
binding.modelString = device.model
|
||||
binding.addedAtString = getString(R.string.manage_device_added_at, DateUtils.getRelativeTimeSpanString(
|
||||
device.addedAt,
|
||||
|
@ -215,25 +139,9 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
DateUtils.HOUR_IN_MILLIS
|
||||
|
||||
))
|
||||
binding.usageStatsAccess = device.currentUsageStatsPermission
|
||||
binding.notificationAccessPermission = device.currentNotificationAccessPermission
|
||||
binding.protectionLevel = device.currentProtectionLevel
|
||||
binding.didAppDowngrade = device.currentAppVersion < device.highestAppVersion
|
||||
}
|
||||
})
|
||||
|
||||
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()
|
||||
binding.permissionCardText = ManageDevicePermissionsFragment.getPreviewText(device, context!!)
|
||||
binding.featureCardText = ManageDeviceFeaturesFragment.getPreviewText(device, context!!)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -264,35 +172,27 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
user = userEntry
|
||||
)
|
||||
|
||||
ManageDeviceTroubleshooting.bind(
|
||||
view = binding.troubleshootingView,
|
||||
userEntry = userEntry,
|
||||
lifecycleOwner = this
|
||||
ActivityLaunchPermissionRequiredAndMissing.bind(
|
||||
view = binding.activityLaunchPermissionMissing,
|
||||
lifecycleOwner = this,
|
||||
device = deviceEntry,
|
||||
user = userEntry
|
||||
)
|
||||
|
||||
ManageDeviceRebootManipulationView.bind(
|
||||
view = binding.deviceRebootManipulation,
|
||||
lifecycleOwner = this,
|
||||
deviceEntry = deviceEntry,
|
||||
auth = auth
|
||||
)
|
||||
userEntry.observe(this, Observer {
|
||||
binding.userCardText = it?.name ?: getString(R.string.manage_device_current_user_none)
|
||||
})
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
logic.backgroundTaskLogic.syncDeviceStatusAsync()
|
||||
}
|
||||
|
||||
override fun getCustomTitle() = deviceEntry.map { it?.name }
|
||||
}
|
||||
|
||||
interface ManageDeviceFragmentHandlers {
|
||||
fun openUsageStatsSettings()
|
||||
fun openNotificationAccessSettings()
|
||||
fun manageDeviceAdmin()
|
||||
fun editDeviceTitle()
|
||||
fun showUserScreen()
|
||||
fun showPermissionsScreen()
|
||||
fun showFeaturesScreen()
|
||||
fun showManageScreen()
|
||||
fun showAuthenticationScreen()
|
||||
}
|
||||
|
|
|
@ -41,6 +41,8 @@ object ManageDeviceManipulation {
|
|||
binding.hasManipulatedDeviceAdmin = device?.manipulationOfProtectionLevel ?: false
|
||||
binding.hasManipulatedUsageStatsAccess = device?.manipulationOfUsageStats ?: false
|
||||
binding.hasManipulatedNotificationAccess = device?.manipulationOfNotificationAccess ?: false
|
||||
binding.hasManipulatedOverlayPermission = device?.manipulationOfOverlayPermission ?: false
|
||||
binding.hasManipulatedAccessibilityService = device?.manipulationOfAccessibilityService ?: false
|
||||
binding.hasManipulationReboot = device?.manipulationDidReboot ?: false
|
||||
binding.hasHadManipulation = (device?.hadManipulation ?: false) and (! (device?.hasActiveManipulationWarning ?: false))
|
||||
binding.hasAnyManipulation = device?.hasAnyManipulation ?: false
|
||||
|
@ -62,6 +64,8 @@ object ManageDeviceManipulation {
|
|||
binding.deviceAdminDisabledCheckbox,
|
||||
binding.usageAccessCheckbox,
|
||||
binding.notificationAccessCheckbox,
|
||||
binding.overlayPermissionCheckbox,
|
||||
binding.accessibilityServiceCheckbox,
|
||||
binding.rebootCheckbox,
|
||||
binding.hadManipulationCheckbox
|
||||
)
|
||||
|
@ -80,6 +84,8 @@ object ManageDeviceManipulation {
|
|||
ignoreNotificationAccessManipulation = binding.notificationAccessCheckbox.isChecked && binding.hasManipulatedNotificationAccess == true,
|
||||
ignoreDeviceAdminManipulationAttempt = binding.deviceAdminDisableAttemptCheckbox.isChecked && binding.hasTriedManipulatingDeviceAdmin == 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,
|
||||
ignoreReboot = binding.rebootCheckbox.isChecked && binding.hasManipulationReboot == true,
|
||||
ignoreHadManipulation = binding.hadManipulationCheckbox.isChecked || (
|
||||
|
|
|
@ -18,20 +18,23 @@ 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.UsageStatsPermissionRequiredAndMissingBinding
|
||||
import io.timelimit.android.databinding.MissingPermissionViewBinding
|
||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||
import io.timelimit.android.livedata.mergeLiveData
|
||||
|
||||
object UsageStatsAccessRequiredAndMissing {
|
||||
fun bind(
|
||||
view: UsageStatsPermissionRequiredAndMissingBinding,
|
||||
view: MissingPermissionViewBinding,
|
||||
user: LiveData<User?>,
|
||||
device: LiveData<Device?>,
|
||||
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) ->
|
||||
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
|
||||
* 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 androidx.lifecycle.LifecycleOwner
|
|
@ -13,7 +13,7 @@
|
|||
* 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
|
||||
package io.timelimit.android.ui.manage.device.manage.advanced
|
||||
|
||||
import android.os.Bundle
|
||||
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
|
||||
* 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.LiveData
|
|
@ -13,7 +13,7 @@
|
|||
* 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
|
||||
package io.timelimit.android.ui.manage.device.manage.permission
|
||||
|
||||
import android.app.Dialog
|
||||
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)
|
||||
}
|
||||
|
||||
override var ignoreStop: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
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.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.NavController
|
||||
|
@ -35,10 +36,14 @@ import io.timelimit.android.livedata.switchMap
|
|||
import io.timelimit.android.livedata.waitForNullableValue
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.ui.contacts.ContactsFragment
|
||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||
import io.timelimit.android.ui.main.AuthenticationFab
|
||||
import io.timelimit.android.ui.overview.about.AboutFragment
|
||||
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.uninstall.UninstallFragment
|
||||
import kotlinx.android.synthetic.main.fragment_main.*
|
||||
|
||||
class MainFragment : Fragment(), OverviewFragmentParentHandlers, AboutFragmentParentHandlers {
|
||||
|
@ -79,7 +84,7 @@ class MainFragment : Fragment(), OverviewFragmentParentHandlers, AboutFragmentPa
|
|||
}
|
||||
}.observe(this, Observer { shouldShowSetup ->
|
||||
if (shouldShowSetup == true) {
|
||||
pager.post {
|
||||
fab.post {
|
||||
navigation.safeNavigate(
|
||||
MainFragmentDirections.actionOverviewFragmentToSetupTermsFragment(),
|
||||
R.id.overviewFragment
|
||||
|
@ -103,52 +108,41 @@ class MainFragment : Fragment(), OverviewFragmentParentHandlers, AboutFragmentPa
|
|||
})
|
||||
}
|
||||
|
||||
pager.adapter = adapter
|
||||
|
||||
bottom_navigation_view.setOnNavigationItemSelectedListener {
|
||||
menuItem ->
|
||||
|
||||
pager.currentItem = when(menuItem.itemId) {
|
||||
R.id.main_tab_overview -> 0
|
||||
R.id.main_tab_uninstall -> 1
|
||||
R.id.main_tab_about -> 2
|
||||
else -> 0
|
||||
fun updateShowFab(selectedItemId: Int) {
|
||||
showAuthButtonLive.value = when (selectedItemId) {
|
||||
R.id.main_tab_overview -> true
|
||||
R.id.main_tab_contacts -> true
|
||||
R.id.main_tab_uninstall -> true
|
||||
R.id.main_tab_about -> false
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fun updateShowFab(selectedPage: Int) {
|
||||
showAuthButtonLive.value = when (selectedPage) {
|
||||
0 -> true
|
||||
1 -> true
|
||||
2 -> false
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
if (childFragmentManager.findFragmentById(R.id.container) == null) {
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, OverviewFragment())
|
||||
.commit()
|
||||
}
|
||||
|
||||
updateShowFab(pager.currentItem)
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
updateShowFab(bottom_navigation_view.selectedItemId)
|
||||
}
|
||||
|
||||
override fun openAddUserScreen() {
|
||||
|
|
|
@ -80,7 +80,10 @@ class OverviewFragment : CoroutineFragment() {
|
|||
ItemTouchHelper(
|
||||
object: ItemTouchHelper.Callback() {
|
||||
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
|
||||
makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END)
|
||||
} else {
|
||||
|
|
|
@ -24,7 +24,9 @@ sealed class OverviewFragmentItem
|
|||
object OverviewFragmentHeaderUsers: OverviewFragmentItem()
|
||||
object OverviewFragmentHeaderDevices: 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()
|
||||
object OverviewFragmentActionAddUser: OverviewFragmentItem()
|
||||
|
|
|
@ -18,6 +18,7 @@ package io.timelimit.android.ui.setup
|
|||
import android.app.admin.DevicePolicyManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
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.logic.AppLogic
|
||||
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() {
|
||||
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() {
|
||||
navigation.safeNavigate(
|
||||
SetupDevicePermissionsFragmentDirections
|
||||
|
@ -113,6 +130,8 @@ class SetupDevicePermissionsFragment : Fragment() {
|
|||
binding.notificationAccessPermission = platform.getNotificationAccessPermissionStatus()
|
||||
binding.protectionLevel = platform.getCurrentProtectionLevel()
|
||||
binding.usageStatsAccess = platform.getForegroundAppPermissionStatus()
|
||||
binding.overlayPermission = platform.getOverlayPermissionStatus()
|
||||
binding.accessibilityServiceEnabled = platform.isAccessibilityServiceEnabled()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -126,5 +145,7 @@ interface SetupDevicePermissionsHandlers {
|
|||
fun manageDeviceAdmin()
|
||||
fun openUsageStatsSettings()
|
||||
fun openNotificationAccessSettings()
|
||||
fun openDrawOverOtherAppsScreen()
|
||||
fun openAccessibilitySettings()
|
||||
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))
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ package io.timelimit.android.ui.view
|
|||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.SeekBar
|
||||
import io.timelimit.android.R
|
||||
|
@ -34,14 +35,16 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet): FrameLay
|
|||
|
||||
var listener: SelectTimeSpanViewListener? = null
|
||||
|
||||
var timeInMillis: Long by Delegates.observable(0L) {
|
||||
_, _, _ ->
|
||||
var timeInMillis: Long by Delegates.observable(0L) { _, _, _ ->
|
||||
bindTime()
|
||||
listener?.onTimeSpanChanged(timeInMillis)
|
||||
}
|
||||
|
||||
var maxDays: Int by Delegates.observable(0) {
|
||||
_, _, _ -> binding.maxDays = maxDays
|
||||
var maxDays: Int by Delegates.observable(0) { _, _, _ ->
|
||||
binding.maxDays = maxDays
|
||||
|
||||
binding.dayPicker.maxValue = maxDays
|
||||
binding.dayPickerContainer.visibility = if (maxDays > 0) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
init {
|
||||
|
@ -69,6 +72,10 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet): FrameLay
|
|||
binding.daysText = TimeTextUtil.days(totalDays, context!!)
|
||||
binding.minutesText = TimeTextUtil.minutes(minutes, context!!)
|
||||
binding.hoursText = TimeTextUtil.hours(hours, context!!)
|
||||
|
||||
binding.minutePicker.value = binding.minutes ?: 0
|
||||
binding.hourPicker.value = binding.hours ?: 0
|
||||
binding.dayPicker.value = binding.days ?: 0
|
||||
}
|
||||
|
||||
private fun readStatusFromBinding() {
|
||||
|
@ -79,7 +86,43 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet): FrameLay
|
|||
timeInMillis = (((days * 24) + hours) * 60 + minutes) * 1000 * 60
|
||||
}
|
||||
|
||||
fun clearNumberPickerFocus() {
|
||||
binding.minutePicker.clearFocus()
|
||||
binding.hourPicker.clearFocus()
|
||||
binding.dayPicker.clearFocus()
|
||||
}
|
||||
|
||||
fun enablePickerMode(enable: Boolean) {
|
||||
binding.seekbarContainer.visibility = if (enable) View.GONE else View.VISIBLE
|
||||
binding.pickerContainer.visibility = if (enable) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
init {
|
||||
binding.minutePicker.minValue = 0
|
||||
binding.minutePicker.maxValue = 59
|
||||
|
||||
binding.hourPicker.minValue = 0
|
||||
binding.hourPicker.maxValue = 23
|
||||
|
||||
binding.dayPicker.minValue = 0
|
||||
binding.dayPicker.maxValue = 1
|
||||
binding.dayPickerContainer.visibility = View.GONE
|
||||
|
||||
binding.minutePicker.setOnValueChangedListener { _, _, newValue ->
|
||||
binding.minutes = newValue
|
||||
readStatusFromBinding()
|
||||
}
|
||||
|
||||
binding.hourPicker.setOnValueChangedListener { _, _, newValue ->
|
||||
binding.hours = newValue
|
||||
readStatusFromBinding()
|
||||
}
|
||||
|
||||
binding.dayPicker.setOnValueChangedListener { _, _, newValue ->
|
||||
binding.days = newValue
|
||||
readStatusFromBinding()
|
||||
}
|
||||
|
||||
binding.daysSeek.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
binding.days = progress
|
||||
|
@ -124,9 +167,15 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet): FrameLay
|
|||
// ignore
|
||||
}
|
||||
})
|
||||
|
||||
binding.pickerContainer.visibility = GONE
|
||||
|
||||
binding.switchToPickerButton.setOnClickListener { listener?.setEnablePickerMode(true) }
|
||||
binding.switchToSeekbarButton.setOnClickListener { listener?.setEnablePickerMode(false) }
|
||||
}
|
||||
}
|
||||
|
||||
interface SelectTimeSpanViewListener {
|
||||
fun onTimeSpanChanged(newTimeInMillis: Long)
|
||||
fun setEnablePickerMode(enable: Boolean)
|
||||
}
|
||||
|
|
|
@ -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