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

@ -15,4 +15,11 @@ object DatabaseMigrations {
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,
ConfigurationItem::class,
TemporarilyAllowedApp::class
], version = 3)
], version = 4)
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
companion object {
private val lock = Object()
@ -68,7 +68,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
.fallbackToDestructiveMigration()
.addMigrations(
DatabaseMigrations.MIGRATE_TO_V2,
DatabaseMigrations.MIGRATE_TO_V3
DatabaseMigrations.MIGRATE_TO_V3,
DatabaseMigrations.MIGRATE_TO_V4
)
.build()
}

View file

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

View file

@ -24,7 +24,7 @@ class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
// 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,
highestAppVersion = 0,
manipulationTriedDisablingDeviceAdmin = false,
hadManipulation = false
manipulationDidReboot = false,
hadManipulation = false,
considerRebootManipulation = false
)
appLogic.database.device().addDeviceSync(device)

View file

@ -402,10 +402,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
if (deviceEntry != null) {
if (deviceEntry.currentAppVersion != currentAppVersion) {
ApplyActionUtil.applyAppLogicAction(
UpdateDeviceStatusAction(
newProtectionLevel = null,
newUsageStatsPermissionStatus = null,
newNotificationAccessPermission = null,
UpdateDeviceStatusAction.empty.copy(
newAppVersion = currentAppVersion
),
appLogic
@ -432,6 +429,21 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
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() {
syncDeviceStatusLock.withLock {
val deviceEntry = appLogic.deviceEntry.waitForNullableValue()
@ -441,14 +453,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
val usageStatsPermission = appLogic.platformIntegration.getForegroundAppPermissionStatus()
val notificationAccess = appLogic.platformIntegration.getNotificationAccessPermissionStatus()
val emptyChanges = UpdateDeviceStatusAction(
newProtectionLevel = null,
newUsageStatsPermissionStatus = null,
newNotificationAccessPermission = null,
newAppVersion = null
)
var changes = emptyChanges
var changes = UpdateDeviceStatusAction.empty
if (protectionLevel != deviceEntry.currentProtectionLevel) {
changes = changes.copy(
@ -472,7 +477,7 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
)
}
if (changes != emptyChanges) {
if (changes != UpdateDeviceStatusAction.empty) {
ApplyActionUtil.applyAppLogicAction(changes, appLogic)
}
}

View file

@ -155,8 +155,19 @@ data class UpdateDeviceStatusAction(
val newProtectionLevel: ProtectionLevel?,
val newUsageStatsPermissionStatus: RuntimePermissionStatus?,
val newNotificationAccessPermission: NewPermissionStatus?,
val newAppVersion: Int?
val newAppVersion: Int?,
val didReboot: Boolean
): AppLogicAction() {
companion object {
val empty = UpdateDeviceStatusAction(
newProtectionLevel = null,
newUsageStatsPermissionStatus = null,
newNotificationAccessPermission = null,
newAppVersion = null,
didReboot = false
)
}
init {
if (newAppVersion != null && newAppVersion < 0) {
throw IllegalArgumentException()
@ -171,6 +182,7 @@ data class IgnoreManipulationAction(
val ignoreAppDowngrade: Boolean,
val ignoreNotificationAccessManipulation: Boolean,
val ignoreUsageStatsAccessManipulation: Boolean,
val ignoreReboot: Boolean,
val ignoreHadManipulation: Boolean
): ParentAction() {
init {
@ -182,6 +194,7 @@ data class IgnoreManipulationAction(
(!ignoreAppDowngrade) &&
(!ignoreNotificationAccessManipulation) &&
(!ignoreUsageStatsAccessManipulation) &&
(!ignoreReboot) &&
(!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() {
init {
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)
if (device.hasActiveManipulationWarning) {

View file

@ -271,6 +271,10 @@ object LocalDatabaseParentActionDispatcher {
deviceEntry = deviceEntry.copy(highestUsageStatsPermission = deviceEntry.currentUsageStatsPermission)
}
if (action.ignoreReboot) {
deviceEntry = deviceEntry.copy(manipulationDidReboot = false)
}
if (action.ignoreHadManipulation) {
deviceEntry = deviceEntry.copy(hadManipulation = false)
}
@ -324,6 +328,16 @@ object LocalDatabaseParentActionDispatcher {
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 { }
database.setTransactionSuccessful()

View file

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

View file

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