Allow interpreting a device reboot as manipulation

This commit is contained in:
Jonas L 2019-02-18 13:08:13 +01:00
parent c37b888b56
commit 212aaafd78
20 changed files with 726 additions and 21 deletions

View file

@ -0,0 +1,455 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "193ef7d6dcc9fbda20e13c8d49c5295f",
"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, `consider_reboot_manipulation` 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": "considerRebootManipulation",
"columnName": "consider_reboot_manipulation",
"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, 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
}
],
"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": []
}
],
"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, \"193ef7d6dcc9fbda20e13c8d49c5295f\")"
]
}
}

View file

@ -15,4 +15,11 @@ object DatabaseMigrations {
database.execSQL("ALTER TABLE `category` ADD COLUMN `parent_category_id` TEXT NOT NULL DEFAULT \"\"") database.execSQL("ALTER TABLE `category` ADD COLUMN `parent_category_id` TEXT NOT NULL DEFAULT \"\"")
} }
} }
val MIGRATE_TO_V4 = object: Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `device` ADD COLUMN `did_reboot` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE `device` ADD COLUMN `consider_reboot_manipulation` INTEGER NOT NULL DEFAULT 0")
}
}
} }

View file

@ -31,7 +31,7 @@ import io.timelimit.android.data.model.*
TimeLimitRule::class, TimeLimitRule::class,
ConfigurationItem::class, ConfigurationItem::class,
TemporarilyAllowedApp::class TemporarilyAllowedApp::class
], version = 3) ], version = 4)
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()
@ -68,7 +68,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.addMigrations( .addMigrations(
DatabaseMigrations.MIGRATE_TO_V2, DatabaseMigrations.MIGRATE_TO_V2,
DatabaseMigrations.MIGRATE_TO_V3 DatabaseMigrations.MIGRATE_TO_V3,
DatabaseMigrations.MIGRATE_TO_V4
) )
.build() .build()
} }

View file

@ -61,8 +61,12 @@ data class Device(
val highestAppVersion: Int, val highestAppVersion: Int,
@ColumnInfo(name = "tried_disabling_device_admin") @ColumnInfo(name = "tried_disabling_device_admin")
val manipulationTriedDisablingDeviceAdmin: Boolean, val manipulationTriedDisablingDeviceAdmin: Boolean,
@ColumnInfo(name = "did_reboot")
val manipulationDidReboot: Boolean,
@ColumnInfo(name = "had_manipulation") @ColumnInfo(name = "had_manipulation")
val hadManipulation: Boolean val hadManipulation: Boolean,
@ColumnInfo(name = "consider_reboot_manipulation")
val considerRebootManipulation: Boolean
): JsonSerializable { ): JsonSerializable {
companion object { companion object {
private const val ID = "id" private const val ID = "id"
@ -79,7 +83,9 @@ data class Device(
private const val CURRENT_APP_VERSION = "ac" private const val CURRENT_APP_VERSION = "ac"
private const val HIGHEST_APP_VERSION = "am" private const val HIGHEST_APP_VERSION = "am"
private const val TRIED_DISABLING_DEVICE_ADMIN = "tdda" private const val TRIED_DISABLING_DEVICE_ADMIN = "tdda"
private const val MANIPULATION_DID_REBOOT = "mdr"
private const val HAD_MANIPULATION = "hm" private const val HAD_MANIPULATION = "hm"
private const val CONSIDER_REBOOT_A_MANIPULATION = "cram"
fun parse(reader: JsonReader): Device { fun parse(reader: JsonReader): Device {
var id: String? = null var id: String? = null
@ -96,7 +102,9 @@ data class Device(
var currentAppVersion: Int? = null var currentAppVersion: Int? = null
var highestAppVersion: Int? = null var highestAppVersion: Int? = null
var manipulationTriedDisablingDeviceAdmin: Boolean? = null var manipulationTriedDisablingDeviceAdmin: Boolean? = null
var manipulationDidReboot: Boolean = false
var hadManipulation: Boolean? = null var hadManipulation: Boolean? = null
var considerRebootManipulation = false
reader.beginObject() reader.beginObject()
@ -116,7 +124,9 @@ data class Device(
CURRENT_APP_VERSION -> currentAppVersion = reader.nextInt() CURRENT_APP_VERSION -> currentAppVersion = reader.nextInt()
HIGHEST_APP_VERSION -> highestAppVersion = reader.nextInt() HIGHEST_APP_VERSION -> highestAppVersion = reader.nextInt()
TRIED_DISABLING_DEVICE_ADMIN -> manipulationTriedDisablingDeviceAdmin = reader.nextBoolean() TRIED_DISABLING_DEVICE_ADMIN -> manipulationTriedDisablingDeviceAdmin = reader.nextBoolean()
MANIPULATION_DID_REBOOT -> manipulationDidReboot = reader.nextBoolean()
HAD_MANIPULATION -> hadManipulation = reader.nextBoolean() HAD_MANIPULATION -> hadManipulation = reader.nextBoolean()
CONSIDER_REBOOT_A_MANIPULATION -> considerRebootManipulation = reader.nextBoolean()
else -> reader.skipValue() else -> reader.skipValue()
} }
} }
@ -138,7 +148,9 @@ data class Device(
currentAppVersion = currentAppVersion!!, currentAppVersion = currentAppVersion!!,
highestAppVersion = highestAppVersion!!, highestAppVersion = highestAppVersion!!,
manipulationTriedDisablingDeviceAdmin = manipulationTriedDisablingDeviceAdmin!!, manipulationTriedDisablingDeviceAdmin = manipulationTriedDisablingDeviceAdmin!!,
hadManipulation = hadManipulation!! manipulationDidReboot = manipulationDidReboot,
hadManipulation = hadManipulation!!,
considerRebootManipulation = considerRebootManipulation
) )
} }
} }
@ -184,7 +196,9 @@ data class Device(
writer.name(CURRENT_APP_VERSION).value(currentAppVersion) writer.name(CURRENT_APP_VERSION).value(currentAppVersion)
writer.name(HIGHEST_APP_VERSION).value(highestAppVersion) writer.name(HIGHEST_APP_VERSION).value(highestAppVersion)
writer.name(TRIED_DISABLING_DEVICE_ADMIN).value(manipulationTriedDisablingDeviceAdmin) writer.name(TRIED_DISABLING_DEVICE_ADMIN).value(manipulationTriedDisablingDeviceAdmin)
writer.name(MANIPULATION_DID_REBOOT).value(manipulationDidReboot)
writer.name(HAD_MANIPULATION).value(hadManipulation) writer.name(HAD_MANIPULATION).value(hadManipulation)
writer.name(CONSIDER_REBOOT_A_MANIPULATION).value(considerRebootManipulation)
writer.endObject() writer.endObject()
} }
@ -203,7 +217,8 @@ data class Device(
manipulationOfUsageStats || manipulationOfUsageStats ||
manipulationOfNotificationAccess || manipulationOfNotificationAccess ||
manipulationOfAppVersion || manipulationOfAppVersion ||
manipulationTriedDisablingDeviceAdmin manipulationTriedDisablingDeviceAdmin ||
manipulationDidReboot
@Transient @Transient
val hasAnyManipulation = hasActiveManipulationWarning || hadManipulation val hasAnyManipulation = hasActiveManipulationWarning || hadManipulation

View file

@ -24,7 +24,7 @@ class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) { override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action == Intent.ACTION_BOOT_COMPLETED) { if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
// this starts the logic (if not yet done) // this starts the logic (if not yet done)
DefaultAppLogic.with(context) DefaultAppLogic.with(context).backgroundTaskLogic.reportDeviceReboot()
} }
} }
} }

View file

@ -82,7 +82,9 @@ class AppSetupLogic(private val appLogic: AppLogic) {
currentAppVersion = 0, currentAppVersion = 0,
highestAppVersion = 0, highestAppVersion = 0,
manipulationTriedDisablingDeviceAdmin = false, manipulationTriedDisablingDeviceAdmin = false,
hadManipulation = false manipulationDidReboot = false,
hadManipulation = false,
considerRebootManipulation = false
) )
appLogic.database.device().addDeviceSync(device) appLogic.database.device().addDeviceSync(device)

View file

@ -402,10 +402,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
if (deviceEntry != null) { if (deviceEntry != null) {
if (deviceEntry.currentAppVersion != currentAppVersion) { if (deviceEntry.currentAppVersion != currentAppVersion) {
ApplyActionUtil.applyAppLogicAction( ApplyActionUtil.applyAppLogicAction(
UpdateDeviceStatusAction( UpdateDeviceStatusAction.empty.copy(
newProtectionLevel = null,
newUsageStatsPermissionStatus = null,
newNotificationAccessPermission = null,
newAppVersion = currentAppVersion newAppVersion = currentAppVersion
), ),
appLogic appLogic
@ -432,6 +429,21 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
private val syncDeviceStatusLock = Mutex() private val syncDeviceStatusLock = Mutex()
fun reportDeviceReboot() {
runAsync {
val deviceEntry = appLogic.deviceEntry.waitForNullableValue()
if (deviceEntry?.considerRebootManipulation == true) {
ApplyActionUtil.applyAppLogicAction(
UpdateDeviceStatusAction.empty.copy(
didReboot = true
),
appLogic
)
}
}
}
private suspend fun syncDeviceStatus() { private suspend fun syncDeviceStatus() {
syncDeviceStatusLock.withLock { syncDeviceStatusLock.withLock {
val deviceEntry = appLogic.deviceEntry.waitForNullableValue() val deviceEntry = appLogic.deviceEntry.waitForNullableValue()
@ -441,14 +453,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
val usageStatsPermission = appLogic.platformIntegration.getForegroundAppPermissionStatus() val usageStatsPermission = appLogic.platformIntegration.getForegroundAppPermissionStatus()
val notificationAccess = appLogic.platformIntegration.getNotificationAccessPermissionStatus() val notificationAccess = appLogic.platformIntegration.getNotificationAccessPermissionStatus()
val emptyChanges = UpdateDeviceStatusAction( var changes = UpdateDeviceStatusAction.empty
newProtectionLevel = null,
newUsageStatsPermissionStatus = null,
newNotificationAccessPermission = null,
newAppVersion = null
)
var changes = emptyChanges
if (protectionLevel != deviceEntry.currentProtectionLevel) { if (protectionLevel != deviceEntry.currentProtectionLevel) {
changes = changes.copy( changes = changes.copy(
@ -472,7 +477,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
) )
} }
if (changes != emptyChanges) { if (changes != UpdateDeviceStatusAction.empty) {
ApplyActionUtil.applyAppLogicAction(changes, appLogic) ApplyActionUtil.applyAppLogicAction(changes, appLogic)
} }
} }

View file

@ -155,8 +155,19 @@ data class UpdateDeviceStatusAction(
val newProtectionLevel: ProtectionLevel?, val newProtectionLevel: ProtectionLevel?,
val newUsageStatsPermissionStatus: RuntimePermissionStatus?, val newUsageStatsPermissionStatus: RuntimePermissionStatus?,
val newNotificationAccessPermission: NewPermissionStatus?, val newNotificationAccessPermission: NewPermissionStatus?,
val newAppVersion: Int? val newAppVersion: Int?,
val didReboot: Boolean
): AppLogicAction() { ): AppLogicAction() {
companion object {
val empty = UpdateDeviceStatusAction(
newProtectionLevel = null,
newUsageStatsPermissionStatus = null,
newNotificationAccessPermission = null,
newAppVersion = null,
didReboot = false
)
}
init { init {
if (newAppVersion != null && newAppVersion < 0) { if (newAppVersion != null && newAppVersion < 0) {
throw IllegalArgumentException() throw IllegalArgumentException()
@ -171,6 +182,7 @@ data class IgnoreManipulationAction(
val ignoreAppDowngrade: Boolean, val ignoreAppDowngrade: Boolean,
val ignoreNotificationAccessManipulation: Boolean, val ignoreNotificationAccessManipulation: Boolean,
val ignoreUsageStatsAccessManipulation: Boolean, val ignoreUsageStatsAccessManipulation: Boolean,
val ignoreReboot: Boolean,
val ignoreHadManipulation: Boolean val ignoreHadManipulation: Boolean
): ParentAction() { ): ParentAction() {
init { init {
@ -182,6 +194,7 @@ data class IgnoreManipulationAction(
(!ignoreAppDowngrade) && (!ignoreAppDowngrade) &&
(!ignoreNotificationAccessManipulation) && (!ignoreNotificationAccessManipulation) &&
(!ignoreUsageStatsAccessManipulation) && (!ignoreUsageStatsAccessManipulation) &&
(!ignoreReboot) &&
(!ignoreHadManipulation) (!ignoreHadManipulation)
} }
@ -198,6 +211,12 @@ data class SetDeviceUserAction(val deviceId: String, val userId: String): Parent
} }
} }
data class SetConsiderRebootManipulationAction(val deviceId: String, val considerRebootManipulation: Boolean): ParentAction() {
init {
IdGenerator.assertIdValid(deviceId)
}
}
data class UpdateCategoryBlockedTimesAction(val categoryId: String, val blockedTimes: ImmutableBitmask): ParentAction() { data class UpdateCategoryBlockedTimesAction(val categoryId: String, val blockedTimes: ImmutableBitmask): ParentAction() {
init { init {
IdGenerator.assertIdValid(categoryId) IdGenerator.assertIdValid(categoryId)

View file

@ -161,6 +161,12 @@ object LocalDatabaseAppLogicActionDispatcher {
} }
} }
if (action.didReboot && device.considerRebootManipulation) {
device = device.copy(
manipulationDidReboot = true
)
}
database.device().updateDeviceEntry(device) database.device().updateDeviceEntry(device)
if (device.hasActiveManipulationWarning) { if (device.hasActiveManipulationWarning) {

View file

@ -271,6 +271,10 @@ object LocalDatabaseParentActionDispatcher {
deviceEntry = deviceEntry.copy(highestUsageStatsPermission = deviceEntry.currentUsageStatsPermission) deviceEntry = deviceEntry.copy(highestUsageStatsPermission = deviceEntry.currentUsageStatsPermission)
} }
if (action.ignoreReboot) {
deviceEntry = deviceEntry.copy(manipulationDidReboot = false)
}
if (action.ignoreHadManipulation) { if (action.ignoreHadManipulation) {
deviceEntry = deviceEntry.copy(hadManipulation = false) deviceEntry = deviceEntry.copy(hadManipulation = false)
} }
@ -324,6 +328,16 @@ object LocalDatabaseParentActionDispatcher {
timezone = action.timezone timezone = action.timezone
) )
} }
is SetConsiderRebootManipulationAction -> {
val deviceEntry = database.device().getDeviceByIdSync(action.deviceId)
?: throw IllegalArgumentException("device not found")
database.device().updateDeviceEntry(
deviceEntry.copy(
considerRebootManipulation = action.considerRebootManipulation
)
)
}
}.let { } }.let { }
database.setTransactionSuccessful() database.setTransactionSuccessful()

View file

@ -270,6 +270,13 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
lifecycleOwner = this lifecycleOwner = this
) )
ManageDeviceRebootManipulationView.bind(
view = binding.deviceRebootManipulation,
lifecycleOwner = this,
deviceEntry = deviceEntry,
auth = auth
)
return binding.root return binding.root
} }

View file

@ -41,6 +41,7 @@ object ManageDeviceManipulation {
binding.hasManipulatedDeviceAdmin = device?.manipulationOfProtectionLevel ?: false binding.hasManipulatedDeviceAdmin = device?.manipulationOfProtectionLevel ?: false
binding.hasManipulatedUsageStatsAccess = device?.manipulationOfUsageStats ?: false binding.hasManipulatedUsageStatsAccess = device?.manipulationOfUsageStats ?: false
binding.hasManipulatedNotificationAccess = device?.manipulationOfNotificationAccess ?: false binding.hasManipulatedNotificationAccess = device?.manipulationOfNotificationAccess ?: false
binding.hasManipulationReboot = device?.manipulationDidReboot ?: false
binding.hasHadManipulation = (device?.hadManipulation ?: false) and (! (device?.hasActiveManipulationWarning ?: false)) binding.hasHadManipulation = (device?.hadManipulation ?: false) and (! (device?.hasActiveManipulationWarning ?: false))
binding.hasAnyManipulation = device?.hasAnyManipulation ?: false binding.hasAnyManipulation = device?.hasAnyManipulation ?: false
}) })
@ -61,6 +62,7 @@ object ManageDeviceManipulation {
binding.deviceAdminDisabledCheckbox, binding.deviceAdminDisabledCheckbox,
binding.usageAccessCheckbox, binding.usageAccessCheckbox,
binding.notificationAccessCheckbox, binding.notificationAccessCheckbox,
binding.rebootCheckbox,
binding.hadManipulationCheckbox binding.hadManipulationCheckbox
) )
@ -79,6 +81,7 @@ object ManageDeviceManipulation {
ignoreDeviceAdminManipulationAttempt = binding.deviceAdminDisableAttemptCheckbox.isChecked && binding.hasTriedManipulatingDeviceAdmin == true, ignoreDeviceAdminManipulationAttempt = binding.deviceAdminDisableAttemptCheckbox.isChecked && binding.hasTriedManipulatingDeviceAdmin == true,
ignoreDeviceAdminManipulation = binding.deviceAdminDisabledCheckbox.isChecked && binding.hasManipulatedDeviceAdmin == true, ignoreDeviceAdminManipulation = binding.deviceAdminDisabledCheckbox.isChecked && binding.hasManipulatedDeviceAdmin == true,
ignoreAppDowngrade = binding.appVersionCheckbox.isChecked && binding.hasManipulatedAppVersion == true, ignoreAppDowngrade = binding.appVersionCheckbox.isChecked && binding.hasManipulatedAppVersion == true,
ignoreReboot = binding.rebootCheckbox.isChecked && binding.hasManipulationReboot == true,
ignoreHadManipulation = binding.hadManipulationCheckbox.isChecked || ( ignoreHadManipulation = binding.hadManipulationCheckbox.isChecked || (
device.hadManipulation and device.hasActiveManipulationWarning device.hadManipulation and device.hasActiveManipulationWarning
), ),

View file

@ -0,0 +1,55 @@
/*
* 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.device.manage
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import io.timelimit.android.data.model.Device
import io.timelimit.android.databinding.ManageDeviceRebootManipulationViewBinding
import io.timelimit.android.sync.actions.SetConsiderRebootManipulationAction
import io.timelimit.android.ui.main.ActivityViewModel
object ManageDeviceRebootManipulationView {
fun bind(
view: ManageDeviceRebootManipulationViewBinding,
deviceEntry: LiveData<Device?>,
lifecycleOwner: LifecycleOwner,
auth: ActivityViewModel
) {
deviceEntry.observe(lifecycleOwner, Observer { device ->
val checked = device?.considerRebootManipulation ?: false
view.checkbox.setOnCheckedChangeListener { _, _ -> }
view.checkbox.isChecked = checked
view.checkbox.setOnCheckedChangeListener { _, isChecked ->
if (isChecked != checked) {
if (
device != null &&
!auth.tryDispatchParentAction(
SetConsiderRebootManipulationAction(
deviceId = device.id,
considerRebootManipulation = isChecked
)
)
) {
view.checkbox.isChecked = checked
}
}
}
})
}
}

View file

@ -379,6 +379,9 @@
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
<include android:id="@+id/device_reboot_manipulation"
layout="@layout/manage_device_reboot_manipulation_view" />
<include android:id="@+id/troubleshooting_view" <include android:id="@+id/troubleshooting_view"
layout="@layout/manage_device_troubleshooting_view" /> layout="@layout/manage_device_troubleshooting_view" />

View file

@ -38,6 +38,10 @@
name="hasManipulatedAppVersion" name="hasManipulatedAppVersion"
type="Boolean" /> type="Boolean" />
<variable
name="hasManipulationReboot"
type="Boolean" />
<variable <variable
name="hasHadManipulation" name="hasHadManipulation"
type="Boolean" /> type="Boolean" />
@ -111,6 +115,13 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<CheckBox
android:visibility="@{safeUnbox(hasManipulationReboot) ? View.VISIBLE : View.GONE}"
android:id="@+id/reboot_checkbox"
android:text="@string/manage_device_manipulation_reboot"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<CheckBox <CheckBox
android:visibility="@{safeUnbox(hasHadManipulation) ? View.VISIBLE : View.GONE}" android:visibility="@{safeUnbox(hasHadManipulation) ? View.VISIBLE : View.GONE}"
android:id="@+id/had_manipulation_checkbox" android:id="@+id/had_manipulation_checkbox"

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.cardview.widget.CardView
app:cardUseCompatPadding="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="vertical"
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:textAppearance="?android:textAppearanceLarge"
android:text="@string/manage_device_reboot_manipulation_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceMedium"
android:text="@string/manage_device_reboot_manipulation_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<CheckBox
android:id="@+id/checkbox"
android:text="@string/manage_device_reboot_manipulation_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</layout>

View file

@ -24,6 +24,7 @@
<string name="manage_device_manipulation_usage_stats_access">Nutzungsdatenzugriff</string> <string name="manage_device_manipulation_usage_stats_access">Nutzungsdatenzugriff</string>
<string name="manage_device_manipulation_notification_access">Benachrichtigungszugriff</string> <string name="manage_device_manipulation_notification_access">Benachrichtigungszugriff</string>
<string name="manage_device_manipulation_app_version">ältere App-Version installiert</string> <string name="manage_device_manipulation_app_version">ältere App-Version installiert</string>
<string name="manage_device_manipulation_reboot">Gerät wurde neu gestartet</string>
<string name="manage_device_manipulation_existed">Es gab eine Manipulation, die wieder beendet wurde</string> <string name="manage_device_manipulation_existed">Es gab eine Manipulation, die wieder beendet wurde</string>
<string name="manage_device_manipulation_btn_ignore">Warnungen ignorieren</string> <string name="manage_device_manipulation_btn_ignore">Warnungen ignorieren</string>
<string name="manage_device_manipulation_toast_nothing_selected">Sie müssen zuerst Warnungen wählen, die Sie ignorieren möchten</string> <string name="manage_device_manipulation_toast_nothing_selected">Sie müssen zuerst Warnungen wählen, die Sie ignorieren möchten</string>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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/>.
-->
<resources>
<string name="manage_device_reboot_manipulation_title">Neustart als Manipulation einstufen</string>
<string name="manage_device_reboot_manipulation_text">
Wenn TimeLimit nach einem Neustart nicht schnell genug startet,
dann können Sie hier einstellen,
dass das als Manipulation gewertet werden soll.
</string>
<string name="manage_device_reboot_manipulation_checkbox">Neustart als Manipulation einstufen</string>
</resources>

View file

@ -24,6 +24,7 @@
<string name="manage_device_manipulation_usage_stats_access">usage stats access</string> <string name="manage_device_manipulation_usage_stats_access">usage stats access</string>
<string name="manage_device_manipulation_notification_access">notification access</string> <string name="manage_device_manipulation_notification_access">notification access</string>
<string name="manage_device_manipulation_app_version">older App version installed</string> <string name="manage_device_manipulation_app_version">older App version installed</string>
<string name="manage_device_manipulation_reboot">device was rebooted</string>
<string name="manage_device_manipulation_existed">there was a manipulation which was stopped again</string> <string name="manage_device_manipulation_existed">there was a manipulation which was stopped again</string>
<string name="manage_device_manipulation_btn_ignore">ignore warnings</string> <string name="manage_device_manipulation_btn_ignore">ignore warnings</string>
<string name="manage_device_manipulation_toast_nothing_selected">You have to select warnings to ignore first</string> <string name="manage_device_manipulation_toast_nothing_selected">You have to select warnings to ignore first</string>

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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/>.
-->
<resources>
<string name="manage_device_reboot_manipulation_title">Consider reboot a manipulation</string>
<string name="manage_device_reboot_manipulation_text">
If TimeLimit does not start fast enough after a reboot, then you can
enable considering that as a manipulation.
</string>
<string name="manage_device_reboot_manipulation_checkbox">consider reboot a manipulation</string>
</resources>