mirror of
https://codeberg.org/timelimit/opentimelimit-android.git
synced 2025-10-05 02:39:34 +02:00
Allow interpreting a device reboot as manipulation
This commit is contained in:
parent
c37b888b56
commit
212aaafd78
20 changed files with 726 additions and 21 deletions
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -161,6 +161,12 @@ object LocalDatabaseAppLogicActionDispatcher {
|
|||
}
|
||||
}
|
||||
|
||||
if (action.didReboot && device.considerRebootManipulation) {
|
||||
device = device.copy(
|
||||
manipulationDidReboot = true
|
||||
)
|
||||
}
|
||||
|
||||
database.device().updateDeviceEntry(device)
|
||||
|
||||
if (device.hasActiveManipulationWarning) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -270,6 +270,13 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
lifecycleOwner = this
|
||||
)
|
||||
|
||||
ManageDeviceRebootManipulationView.bind(
|
||||
view = binding.deviceRebootManipulation,
|
||||
lifecycleOwner = this,
|
||||
deviceEntry = deviceEntry,
|
||||
auth = auth
|
||||
)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue