Add contact whitelist

This commit is contained in:
Jonas L 2019-07-01 00:00:00 +00:00
parent 5e694f0c1c
commit dc3c2a0023
29 changed files with 1707 additions and 7 deletions

View file

@ -0,0 +1,791 @@
{
"formatVersion": 1,
"database": {
"version": 20,
"identityHash": "a59be3b5567854fe9546f64b96f2480e",
"entities": [
{
"tableName": "user",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `password` TEXT NOT NULL, `second_password_salt` TEXT NOT NULL, `type` TEXT NOT NULL, `timezone` TEXT NOT NULL, `disable_limits_until` INTEGER NOT NULL, `mail` TEXT NOT NULL, `current_device` TEXT NOT NULL, `category_for_not_assigned_apps` TEXT NOT NULL, `relax_primary_device` INTEGER NOT NULL, `mail_notification_flags` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "secondPasswordSalt",
"columnName": "second_password_salt",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timeZone",
"columnName": "timezone",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "disableLimitsUntil",
"columnName": "disable_limits_until",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mail",
"columnName": "mail",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "currentDevice",
"columnName": "current_device",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "categoryForNotAssignedApps",
"columnName": "category_for_not_assigned_apps",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "relaxPrimaryDevice",
"columnName": "relax_primary_device",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mailNotificationFlags",
"columnName": "mail_notification_flags",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "device",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `model` TEXT NOT NULL, `added_at` INTEGER NOT NULL, `current_user_id` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `network_time` TEXT NOT NULL, `current_protection_level` TEXT NOT NULL, `highest_permission_level` TEXT NOT NULL, `current_usage_stats_permission` TEXT NOT NULL, `highest_usage_stats_permission` TEXT NOT NULL, `current_notification_access_permission` TEXT NOT NULL, `highest_notification_access_permission` TEXT NOT NULL, `current_app_version` INTEGER NOT NULL, `highest_app_version` INTEGER NOT NULL, `tried_disabling_device_admin` INTEGER NOT NULL, `did_reboot` INTEGER NOT NULL, `had_manipulation` INTEGER NOT NULL, `did_report_uninstall` INTEGER NOT NULL, `is_user_kept_signed_in` INTEGER NOT NULL, `show_device_connected` INTEGER NOT NULL, `default_user` TEXT NOT NULL, `default_user_timeout` INTEGER NOT NULL, `consider_reboot_manipulation` INTEGER NOT NULL, `current_overlay_permission` TEXT NOT NULL, `highest_overlay_permission` TEXT NOT NULL, `current_accessibility_service_permission` INTEGER NOT NULL, `was_accessibility_service_permission` INTEGER NOT NULL, `enable_activity_level_blocking` INTEGER NOT NULL, `q_or_later` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "model",
"columnName": "model",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "addedAt",
"columnName": "added_at",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "currentUserId",
"columnName": "current_user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "installedAppsVersion",
"columnName": "apps_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "networkTime",
"columnName": "network_time",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "currentProtectionLevel",
"columnName": "current_protection_level",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "highestProtectionLevel",
"columnName": "highest_permission_level",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "currentUsageStatsPermission",
"columnName": "current_usage_stats_permission",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "highestUsageStatsPermission",
"columnName": "highest_usage_stats_permission",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "currentNotificationAccessPermission",
"columnName": "current_notification_access_permission",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "highestNotificationAccessPermission",
"columnName": "highest_notification_access_permission",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "currentAppVersion",
"columnName": "current_app_version",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "highestAppVersion",
"columnName": "highest_app_version",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "manipulationTriedDisablingDeviceAdmin",
"columnName": "tried_disabling_device_admin",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "manipulationDidReboot",
"columnName": "did_reboot",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hadManipulation",
"columnName": "had_manipulation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "didReportUninstall",
"columnName": "did_report_uninstall",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isUserKeptSignedIn",
"columnName": "is_user_kept_signed_in",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "showDeviceConnected",
"columnName": "show_device_connected",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultUser",
"columnName": "default_user",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "defaultUserTimeout",
"columnName": "default_user_timeout",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "considerRebootManipulation",
"columnName": "consider_reboot_manipulation",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "currentOverlayPermission",
"columnName": "current_overlay_permission",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "highestOverlayPermission",
"columnName": "highest_overlay_permission",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accessibilityServiceEnabled",
"columnName": "current_accessibility_service_permission",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "wasAccessibilityServiceEnabled",
"columnName": "was_accessibility_service_permission",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enableActivityLevelBlocking",
"columnName": "enable_activity_level_blocking",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "qOrLater",
"columnName": "q_or_later",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "app",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, `title` TEXT NOT NULL, `launchable` INTEGER NOT NULL, `recommendation` TEXT NOT NULL, PRIMARY KEY(`device_id`, `package_name`))",
"fields": [
{
"fieldPath": "deviceId",
"columnName": "device_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isLaunchable",
"columnName": "launchable",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "recommendation",
"columnName": "recommendation",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"device_id",
"package_name"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_app_device_id",
"unique": false,
"columnNames": [
"device_id"
],
"createSql": "CREATE INDEX `index_app_device_id` ON `${TABLE_NAME}` (`device_id`)"
},
{
"name": "index_app_package_name",
"unique": false,
"columnNames": [
"package_name"
],
"createSql": "CREATE INDEX `index_app_package_name` ON `${TABLE_NAME}` (`package_name`)"
}
],
"foreignKeys": []
},
{
"tableName": "category_app",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`category_id`, `package_name`))",
"fields": [
{
"fieldPath": "categoryId",
"columnName": "category_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"category_id",
"package_name"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_category_app_category_id",
"unique": false,
"columnNames": [
"category_id"
],
"createSql": "CREATE INDEX `index_category_app_category_id` ON `${TABLE_NAME}` (`category_id`)"
},
{
"name": "index_category_app_package_name",
"unique": false,
"columnNames": [
"package_name"
],
"createSql": "CREATE INDEX `index_category_app_package_name` ON `${TABLE_NAME}` (`package_name`)"
}
],
"foreignKeys": []
},
{
"tableName": "category",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `child_id` TEXT NOT NULL, `title` TEXT NOT NULL, `blocked_times` TEXT NOT NULL, `extra_time` INTEGER NOT NULL, `temporarily_blocked` INTEGER NOT NULL, `base_version` TEXT NOT NULL, `apps_version` TEXT NOT NULL, `rules_version` TEXT NOT NULL, `usedtimes_version` TEXT NOT NULL, `parent_category_id` TEXT NOT NULL, `block_all_notifications` INTEGER NOT NULL, `time_warnings` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "childId",
"columnName": "child_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "blockedMinutesInWeek",
"columnName": "blocked_times",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "extraTimeInMillis",
"columnName": "extra_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "temporarilyBlocked",
"columnName": "temporarily_blocked",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "baseVersion",
"columnName": "base_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "assignedAppsVersion",
"columnName": "apps_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timeLimitRulesVersion",
"columnName": "rules_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "usedTimesVersion",
"columnName": "usedtimes_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parentCategoryId",
"columnName": "parent_category_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "blockAllNotifications",
"columnName": "block_all_notifications",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "timeWarnings",
"columnName": "time_warnings",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "used_time",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`day_of_epoch` INTEGER NOT NULL, `used_time` INTEGER NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `day_of_epoch`))",
"fields": [
{
"fieldPath": "dayOfEpoch",
"columnName": "day_of_epoch",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "usedMillis",
"columnName": "used_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "categoryId",
"columnName": "category_id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"category_id",
"day_of_epoch"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "time_limit_rule",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `category_id` TEXT NOT NULL, `apply_to_extra_time_usage` INTEGER NOT NULL, `day_mask` INTEGER NOT NULL, `max_time` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "categoryId",
"columnName": "category_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "applyToExtraTimeUsage",
"columnName": "apply_to_extra_time_usage",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "dayMask",
"columnName": "day_mask",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "maximumTimeInMillis",
"columnName": "max_time",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "key",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "temporarily_allowed_app",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`device_id`, `package_name`))",
"fields": [
{
"fieldPath": "deviceId",
"columnName": "device_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"device_id",
"package_name"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "pending_sync_action",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sequence_number` INTEGER NOT NULL, `action` TEXT NOT NULL, `integrity` TEXT NOT NULL, `scheduled_for_upload` INTEGER NOT NULL, `type` TEXT NOT NULL, `user_id` TEXT NOT NULL, PRIMARY KEY(`sequence_number`))",
"fields": [
{
"fieldPath": "sequenceNumber",
"columnName": "sequence_number",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "encodedAction",
"columnName": "action",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "integrity",
"columnName": "integrity",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "scheduledForUpload",
"columnName": "scheduled_for_upload",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"sequence_number"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_pending_sync_action_scheduled_for_upload",
"unique": false,
"columnNames": [
"scheduled_for_upload"
],
"createSql": "CREATE INDEX `index_pending_sync_action_scheduled_for_upload` ON `${TABLE_NAME}` (`scheduled_for_upload`)"
}
],
"foreignKeys": []
},
{
"tableName": "app_activity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`device_id` TEXT NOT NULL, `app_package_name` TEXT NOT NULL, `activity_class_name` TEXT NOT NULL, `activity_title` TEXT NOT NULL, PRIMARY KEY(`device_id`, `app_package_name`, `activity_class_name`))",
"fields": [
{
"fieldPath": "deviceId",
"columnName": "device_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "appPackageName",
"columnName": "app_package_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activityClassName",
"columnName": "activity_class_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "activity_title",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"device_id",
"app_package_name",
"activity_class_name"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "notification",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL, `id` TEXT NOT NULL, `first_notify_time` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, PRIMARY KEY(`type`, `id`))",
"fields": [
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "firstNotifyTime",
"columnName": "first_notify_time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isDismissed",
"columnName": "dismissed",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"type",
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "allowed_contact",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `phone` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "phone",
"columnName": "phone",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"a59be3b5567854fe9546f64b96f2480e\")"
]
}
}

View file

@ -31,6 +31,8 @@
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-feature android:name="android.hardware.telephony" android:required="false" />
<application <application
android:name=".Application" android:name=".Application"

View file

@ -31,6 +31,7 @@ interface Database {
fun pendingSyncAction(): PendingSyncActionDao fun pendingSyncAction(): PendingSyncActionDao
fun appActivity(): AppActivityDao fun appActivity(): AppActivityDao
fun notification(): NotificationDao fun notification(): NotificationDao
fun allowedContact(): AllowedContactDao
fun beginTransaction() fun beginTransaction()
fun setTransactionSuccessful() fun setTransactionSuccessful()

View file

@ -141,4 +141,10 @@ object DatabaseMigrations {
// a new possible enum value was added, the version upgrade enables the downgrade mechanism // a new possible enum value was added, the version upgrade enables the downgrade mechanism
} }
} }
val MIGRATE_TO_V20 = object: Migration(19, 20) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `allowed_contact` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `phone` TEXT NOT NULL)")
}
}
} }

View file

@ -33,8 +33,9 @@ import io.timelimit.android.data.model.*
TemporarilyAllowedApp::class, TemporarilyAllowedApp::class,
PendingSyncAction::class, PendingSyncAction::class,
AppActivity::class, AppActivity::class,
Notification::class Notification::class,
], version = 19) AllowedContact::class
], version = 20)
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database { abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
companion object { companion object {
private val lock = Object() private val lock = Object()
@ -87,7 +88,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
DatabaseMigrations.MIGRATE_TO_V16, DatabaseMigrations.MIGRATE_TO_V16,
DatabaseMigrations.MIGRATE_TO_V17, DatabaseMigrations.MIGRATE_TO_V17,
DatabaseMigrations.MIGRATE_TO_V18, DatabaseMigrations.MIGRATE_TO_V18,
DatabaseMigrations.MIGRATE_TO_V19 DatabaseMigrations.MIGRATE_TO_V19,
DatabaseMigrations.MIGRATE_TO_V20
) )
.build() .build()
} }

View file

@ -40,6 +40,7 @@ object DatabaseBackupLowlevel {
private const val USER = "user" private const val USER = "user"
private const val APP_ACTIVITY = "appActivity" private const val APP_ACTIVITY = "appActivity"
private const val NOTIFICATION = "notification" private const val NOTIFICATION = "notification"
private const val ALLOWED_CONTACT = "allowedContact"
fun outputAsBackupJson(database: Database, outputStream: OutputStream) { fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8)) val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
@ -83,6 +84,7 @@ object DatabaseBackupLowlevel {
handleCollection(USER) { offset, pageSize -> database.user().getUserPageSync(offset, pageSize) } handleCollection(USER) { offset, pageSize -> database.user().getUserPageSync(offset, pageSize) }
handleCollection(APP_ACTIVITY) { offset, pageSize -> database.appActivity().getAppActivityPageSync(offset, pageSize) } handleCollection(APP_ACTIVITY) { offset, pageSize -> database.appActivity().getAppActivityPageSync(offset, pageSize) }
handleCollection(NOTIFICATION) { offset, pageSize -> database.notification().getNotificationPageSync(offset, pageSize) } handleCollection(NOTIFICATION) { offset, pageSize -> database.notification().getNotificationPageSync(offset, pageSize) }
handleCollection(ALLOWED_CONTACT) { offset, pageSize -> database.allowedContact().getAllowedContactPageSync(offset, pageSize) }
writer.endObject().flush() writer.endObject().flush()
} }
@ -201,6 +203,18 @@ object DatabaseBackupLowlevel {
reader.endArray() reader.endArray()
} }
ALLOWED_CONTACT -> {
reader.beginArray()
while (reader.hasNext()) {
database.allowedContact().addContactSync(
// this will use an unused id
AllowedContact.parse(reader).copy(id = 0)
)
}
reader.endArray()
}
else -> reader.skipValue() else -> reader.skipValue()
} }
} }

View file

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

View file

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

View file

@ -180,4 +180,5 @@ object HintsToShow {
const val DEVICE_SCREEN_INTRODUCTION = 2L const val DEVICE_SCREEN_INTRODUCTION = 2L
const val CATEGORIES_INTRODUCTION = 4L const val CATEGORIES_INTRODUCTION = 4L
const val TIME_LIMIT_RULE_INTRODUCTION = 8L const val TIME_LIMIT_RULE_INTRODUCTION = 8L
const val CONTACTS_INTRO = 16L
} }

View file

@ -18,7 +18,6 @@ package io.timelimit.android.logic
import android.util.Log import android.util.Log
import android.util.SparseArray import android.util.SparseArray
import android.util.SparseLongArray import android.util.SparseLongArray
import android.widget.Toast
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import io.timelimit.android.BuildConfig import io.timelimit.android.BuildConfig
import io.timelimit.android.R import io.timelimit.android.R
@ -47,6 +46,8 @@ import kotlinx.coroutines.sync.withLock
import java.util.* import java.util.*
class BackgroundTaskLogic(val appLogic: AppLogic) { class BackgroundTaskLogic(val appLogic: AppLogic) {
var pauseBackgroundLoop = false
companion object { companion object {
private const val CHECK_PERMISSION_INTERVAL = 10 * 1000L // all 10 seconds private const val CHECK_PERMISSION_INTERVAL = 10 * 1000L // all 10 seconds
private const val BACKGROUND_SERVICE_INTERVAL = 100L // all 100 ms private const val BACKGROUND_SERVICE_INTERVAL = 100L // all 100 ms
@ -280,7 +281,14 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
// the following is not executed if the permission is missing // the following is not executed if the permission is missing
if ( if (pauseBackgroundLoop) {
usedTimeUpdateHelper?.commit(appLogic)
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
title = appLogic.context.getString(R.string.background_logic_paused_title),
text = appLogic.context.getString(R.string.background_logic_paused_text)
))
appLogic.platformIntegration.setShowBlockingOverlay(false)
} else if (
(foregroundAppPackageName == BuildConfig.APPLICATION_ID) || (foregroundAppPackageName == BuildConfig.APPLICATION_ID) ||
(foregroundAppPackageName != null && AndroidIntegrationApps.ignoredApps[foregroundAppPackageName].let { (foregroundAppPackageName != null && AndroidIntegrationApps.ignoredApps[foregroundAppPackageName].let {
when (it) { when (it) {

View file

@ -63,6 +63,7 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
setActivityCheckout(checkout) setActivityCheckout(checkout)
} }
} }
override var ignoreStop: Boolean = false
val googleSignInUtil = GoogleSignInUtil(this) val googleSignInUtil = GoogleSignInUtil(this)
@ -134,7 +135,7 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
if (!isChangingConfigurations) { if ((!isChangingConfigurations) && (!ignoreStop)) {
getActivityViewModel().logOut() getActivityViewModel().logOut()
} }
@ -150,6 +151,10 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder {
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent) super.onNewIntent(intent)
if ((intent?.flags ?: 0) and Intent.FLAG_ACTIVITY_REORDER_TO_FRONT == Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) {
return
}
getNavController().popBackStack(R.id.overviewFragment, true) getNavController().popBackStack(R.id.overviewFragment, true)
getNavController().handleDeepLink( getNavController().handleDeepLink(
getNavController().createDeepLink() getNavController().createDeepLink()

View file

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

View file

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

View file

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

View file

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

View file

@ -54,6 +54,8 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
} }
} }
override var ignoreStop: Boolean = false
private val blockedPackageName: String by lazy { private val blockedPackageName: String by lazy {
intent.getStringExtra(EXTRA_PACKAGE_NAME) intent.getStringExtra(EXTRA_PACKAGE_NAME)
} }
@ -115,7 +117,7 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
if (!isChangingConfigurations) { if ((!isChangingConfigurations) && (!ignoreStop)) {
getActivityViewModel().logOut() getActivityViewModel().logOut()
} }

View file

@ -20,6 +20,7 @@ import android.app.Activity
interface ActivityViewModelHolder { interface ActivityViewModelHolder {
fun getActivityViewModel(): ActivityViewModel fun getActivityViewModel(): ActivityViewModel
fun showAuthenticationScreen() fun showAuthenticationScreen()
var ignoreStop: Boolean
} }
fun getActivityViewModel(activity: Activity): ActivityViewModel { fun getActivityViewModel(activity: Activity): ActivityViewModel {

View file

@ -36,6 +36,8 @@ class UnlockAfterManipulationActivity : AppCompatActivity(), ActivityViewModelHo
ViewModelProviders.of(this).get(ActivityViewModel::class.java) ViewModelProviders.of(this).get(ActivityViewModel::class.java)
} }
override var ignoreStop: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_unlock_after_manipulation) setContentView(R.layout.activity_unlock_after_manipulation)

View file

@ -36,6 +36,7 @@ import io.timelimit.android.livedata.switchMap
import io.timelimit.android.livedata.waitForNullableValue import io.timelimit.android.livedata.waitForNullableValue
import io.timelimit.android.logic.AppLogic import io.timelimit.android.logic.AppLogic
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.contacts.ContactsFragment
import io.timelimit.android.ui.main.ActivityViewModelHolder import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.AuthenticationFab import io.timelimit.android.ui.main.AuthenticationFab
import io.timelimit.android.ui.manage.device.add.AddDeviceFragment import io.timelimit.android.ui.manage.device.add.AddDeviceFragment
@ -113,6 +114,7 @@ class MainFragment : Fragment(), OverviewFragmentParentHandlers, AboutFragmentPa
fun updateShowFab(selectedItemId: Int) { fun updateShowFab(selectedItemId: Int) {
showAuthButtonLive.value = when (selectedItemId) { showAuthButtonLive.value = when (selectedItemId) {
R.id.main_tab_overview -> true R.id.main_tab_overview -> true
R.id.main_tab_contacts -> true
R.id.main_tab_uninstall -> !BuildConfig.storeCompilant R.id.main_tab_uninstall -> !BuildConfig.storeCompilant
R.id.main_tab_about -> false R.id.main_tab_about -> false
else -> throw IllegalStateException() else -> throw IllegalStateException()
@ -125,6 +127,7 @@ class MainFragment : Fragment(), OverviewFragmentParentHandlers, AboutFragmentPa
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.replace(R.id.container, when(menuItem.itemId) { .replace(R.id.container, when(menuItem.itemId) {
R.id.main_tab_overview -> OverviewFragment() R.id.main_tab_overview -> OverviewFragment()
R.id.main_tab_contacts -> ContactsFragment()
R.id.main_tab_uninstall -> UninstallFragment() R.id.main_tab_uninstall -> UninstallFragment()
R.id.main_tab_about -> AboutFragment() R.id.main_tab_about -> AboutFragment()
else -> throw IllegalStateException() else -> throw IllegalStateException()

View file

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

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,3h-1L18,1h-2v2L8,3L8,1L6,1v2L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,6c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM18,18L6,18v-1c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1v1z"/>
</vector>

View file

@ -0,0 +1,24 @@
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:context="io.timelimit.android.ui.contacts.ContactsFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</layout>

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardUseCompatPadding="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:padding="8dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:text="@string/contacts_title_long"
android:textAppearance="?android:textAppearanceLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/contacts_description"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceSmall"
android:text="@string/contacts_intro_swipe"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="title"
type="String" />
<variable
name="phone"
type="String" />
</data>
<androidx.cardview.widget.CardView
android:id="@+id/card"
android:foreground="?selectableItemBackground"
app:cardUseCompatPadding="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="vertical"
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
tools:text="Max Mustermann"
android:textAppearance="?android:textAppearanceLarge"
android:text="@{title}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
tools:text="+49 1234 567890123"
android:text="@{phone}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</layout>

View file

@ -19,6 +19,11 @@
android:title="@string/main_tab_overview" android:title="@string/main_tab_overview"
android:id="@+id/main_tab_overview" /> android:id="@+id/main_tab_overview" />
<item
android:icon="@drawable/ic_perm_contact_calendar_black_24dp"
android:title="@string/contacts_title"
android:id="@+id/main_tab_contacts" />
<item <item
android:icon="@drawable/ic_delete_black_24dp" android:icon="@drawable/ic_delete_black_24dp"
android:title="@string/main_tab_uninstall" android:title="@string/main_tab_uninstall"

View file

@ -31,4 +31,7 @@
<string name="background_logic_timeout_title">automatische Abmeldung aktiviert</string> <string name="background_logic_timeout_title">automatische Abmeldung aktiviert</string>
<string name="background_logic_timeout_text">TimeLimit läuft noch, um das zu ermöglichen</string> <string name="background_logic_timeout_text">TimeLimit läuft noch, um das zu ermöglichen</string>
<string name="background_logic_paused_title">Vorübergehend deaktiviert</string>
<string name="background_logic_paused_text">Keine Einschränkungen</string>
</resources> </resources>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
<string name="contacts_title">Kontakte</string>
<string name="contacts_snackbar_added">Kontakt wurde hinzugefügt</string>
<string name="contacts_title_long">Erlaubte Kontakte</string>
<string name="contacts_description">Das ist eine Liste von Telefonnummern.
Eltern können Einträge von der Systemkontaktliste hinzufügen.
Jeder Nutzer diese Gerätes kann über diese Ansicht einen Anruf zu diesen Nummern starten,
auch wenn die Telefon-App gesperrt ist.
Die Telefonnummern werden nur auf diesem Gerät gespeichert.
</string>
<string name="contacts_intro_swipe">Se können diesen Hinweis entfernen, indem Sie ihn zur Seite wischen</string>
<string name="contacts_add">Kontakt hinzufügen</string>
<string name="contacts_snackbar_remove_auth">Sie müssen sich anmelden um Kontakte zu entfernen</string>
<string name="contacts_snackbar_removed">%s wurde entfernt</string>
<string name="contacts_snackbar_call_failed">Anruf fehlgeschlagen</string>
<string name="contacts_snackbar_call_started">Anruf gestartet; Kann per Benachrichtigungsbereich beendet werden</string>
</resources>

View file

@ -31,4 +31,7 @@
<string name="background_logic_timeout_title">auto logout enabled</string> <string name="background_logic_timeout_title">auto logout enabled</string>
<string name="background_logic_timeout_text">TimeLimit still runs to do that</string> <string name="background_logic_timeout_text">TimeLimit still runs to do that</string>
<string name="background_logic_paused_title">Temporarily disabled</string>
<string name="background_logic_paused_text">No limitations apply</string>
</resources> </resources>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 Jonas Lochmann
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
<string name="contacts_title">Contacts</string>
<string name="contacts_snackbar_added">Contact was added</string>
<string name="contacts_title_long">Allowed contacts</string>
<string name="contacts_description">This is a list of phone numbers.
Parents can add items from the contact list of the device.
Every user of this device can start a call to one of these numbers using this screen.
This works if the phone app is blocked.
The phone numbers are only saved at this device and are never transmitted.
</string>
<string name="contacts_intro_swipe">Swipe to the side to remove this message</string>
<string name="contacts_add">Add contact</string>
<string name="contacts_snackbar_remove_auth">You must sign in to remove contacts</string>
<string name="contacts_snackbar_removed">%s was removed</string>
<string name="contacts_snackbar_call_failed">Could not start call</string>
<string name="contacts_snackbar_call_started">Call started; You can hang up using the notification area</string>
</resources>