mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 01:39:22 +02:00
Adjust for GPlay policies related to the list of installed Apps
This commit is contained in:
parent
d16117f885
commit
fd5ffe73ce
21 changed files with 1911 additions and 261 deletions
1264
app/schemas/io.timelimit.android.data.RoomDatabase/41.json
Normal file
1264
app/schemas/io.timelimit.android.data.RoomDatabase/41.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -21,37 +21,37 @@ import io.timelimit.android.data.model.TimeLimitRule
|
|||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
|
||||
object DatabaseMigrations {
|
||||
val MIGRATE_TO_V2 = object: Migration(1, 2) {
|
||||
private val MIGRATE_TO_V2 = object: Migration(1, 2) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE device ADD COLUMN did_report_uninstall INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V3 = object: Migration(2, 3) {
|
||||
private val MIGRATE_TO_V3 = object: Migration(2, 3) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE device ADD COLUMN is_user_kept_signed_in INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V4 = object: Migration(3, 4) {
|
||||
private val MIGRATE_TO_V4 = object: Migration(3, 4) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `user` ADD COLUMN `category_for_not_assigned_apps` TEXT NOT NULL DEFAULT \"\"")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V5 = object: Migration(4, 5) {
|
||||
private val MIGRATE_TO_V5 = object: Migration(4, 5) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `category` ADD COLUMN `parent_category_id` TEXT NOT NULL DEFAULT \"\"")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V6 = object: Migration(5, 6) {
|
||||
private val MIGRATE_TO_V6 = object: Migration(5, 6) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `device` ADD COLUMN `show_device_connected` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V7 = object: Migration(6, 7) {
|
||||
private val MIGRATE_TO_V7 = object: Migration(6, 7) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `device` ADD COLUMN `default_user` TEXT NOT NULL DEFAULT \"\"")
|
||||
database.execSQL("ALTER TABLE `device` ADD COLUMN `default_user_timeout` INTEGER NOT NULL DEFAULT 0")
|
||||
|
@ -59,7 +59,7 @@ object DatabaseMigrations {
|
|||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V8 = object: Migration(7, 8) {
|
||||
private val MIGRATE_TO_V8 = object: Migration(7, 8) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// this is empty
|
||||
//
|
||||
|
@ -67,14 +67,14 @@ object DatabaseMigrations {
|
|||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V9 = object: Migration(8, 9) {
|
||||
private val MIGRATE_TO_V9 = object: Migration(8, 9) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V10 = object: Migration(9, 10) {
|
||||
private val MIGRATE_TO_V10 = object: Migration(9, 10) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// this is empty
|
||||
//
|
||||
|
@ -82,40 +82,40 @@ object DatabaseMigrations {
|
|||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V11 = object: Migration(10, 11) {
|
||||
private val MIGRATE_TO_V11 = object: Migration(10, 11) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `user` ADD COLUMN `mail_notification_flags` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V12 = object: Migration(11, 12) {
|
||||
private val MIGRATE_TO_V12 = object: Migration(11, 12) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `category` ADD COLUMN `block_all_notifications` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V13 = object: Migration(12, 13) {
|
||||
private val MIGRATE_TO_V13 = object: Migration(12, 13) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `device` ADD COLUMN `current_overlay_permission` TEXT NOT NULL DEFAULT \"not granted\"")
|
||||
database.execSQL("ALTER TABLE `device` ADD COLUMN `highest_overlay_permission` TEXT NOT NULL DEFAULT \"not granted\"")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V14 = object: Migration(13, 14) {
|
||||
private val MIGRATE_TO_V14 = object: Migration(13, 14) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `device` ADD COLUMN `current_accessibility_service_permission` INTEGER NOT NULL DEFAULT 0")
|
||||
database.execSQL("ALTER TABLE `device` ADD COLUMN `was_accessibility_service_permission` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V15 = object: Migration(14, 15) {
|
||||
private val MIGRATE_TO_V15 = object: Migration(14, 15) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE `app_activity` (`device_id` TEXT NOT NULL, `app_package_name` TEXT NOT NULL, `activity_class_name` TEXT NOT NULL, `activity_title` TEXT NOT NULL, PRIMARY KEY(`device_id`, `app_package_name`, `activity_class_name`))")
|
||||
database.execSQL("ALTER TABLE `device` ADD COLUMN `enable_activity_level_blocking` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V16 = object: Migration(15, 16) {
|
||||
private val MIGRATE_TO_V16 = object: Migration(15, 16) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// this is empty
|
||||
//
|
||||
|
@ -123,20 +123,20 @@ object DatabaseMigrations {
|
|||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V17 = object: Migration(16, 17) {
|
||||
private val MIGRATE_TO_V17 = object: Migration(16, 17) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `notification` (`type` INTEGER NOT NULL, `id` TEXT NOT NULL, `first_notify_time` INTEGER NOT NULL, `dismissed` INTEGER NOT NULL, PRIMARY KEY(`type`, `id`))")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V18 = object: Migration(17, 18) {
|
||||
private val MIGRATE_TO_V18 = object: Migration(17, 18) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `device` ADD COLUMN `q_or_later` INTEGER NOT NULL DEFAULT 0")
|
||||
database.execSQL("ALTER TABLE `category` ADD COLUMN `time_warnings` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V19 = object: Migration(18, 19) {
|
||||
private val MIGRATE_TO_V19 = object: Migration(18, 19) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// this is empty
|
||||
//
|
||||
|
@ -144,44 +144,44 @@ object DatabaseMigrations {
|
|||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V20 = object: Migration(19, 20) {
|
||||
private 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)")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V21 = object: Migration(20, 21) {
|
||||
private val MIGRATE_TO_V21 = object: Migration(20, 21) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `device` ADD COLUMN `had_manipulation_flags` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V22 = object: Migration(21, 22) {
|
||||
private val MIGRATE_TO_V22 = object: Migration(21, 22) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `user` ADD COLUMN `blocked_times` TEXT NOT NULL DEFAULT \"\"")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V23 = object: Migration(22, 23) {
|
||||
private val MIGRATE_TO_V23 = object: Migration(22, 23) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `category` ADD COLUMN `min_battery_charging` INTEGER NOT NULL DEFAULT 0")
|
||||
database.execSQL("ALTER TABLE `category` ADD COLUMN `min_battery_mobile` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V24 = object: Migration(23, 24) {
|
||||
private val MIGRATE_TO_V24 = object: Migration(23, 24) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `category` ADD COLUMN `temporarily_blocked_end_time` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V25 = object: Migration(24, 25) {
|
||||
private val MIGRATE_TO_V25 = object: Migration(24, 25) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `category` ADD COLUMN `sort` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V26 = object: Migration(25, 26) {
|
||||
private val MIGRATE_TO_V26 = object: Migration(25, 26) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// this is empty
|
||||
//
|
||||
|
@ -189,20 +189,20 @@ object DatabaseMigrations {
|
|||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V27 = object: Migration(26, 27) {
|
||||
private val MIGRATE_TO_V27 = object: Migration(26, 27) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `category` ADD COLUMN `extra_time_day` INTEGER NOT NULL DEFAULT -1")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V28 = object: Migration(27, 28) {
|
||||
private val MIGRATE_TO_V28 = object: Migration(27, 28) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `user_key` (`user_id` TEXT NOT NULL, `key` BLOB NOT NULL, `last_use` INTEGER NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
|
||||
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_user_key_key` ON `user_key` (`key`)")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V29 = object: Migration(28, 29) {
|
||||
private val MIGRATE_TO_V29 = object: Migration(28, 29) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `start_minute_of_day` INTEGER NOT NULL DEFAULT ${TimeLimitRule.MIN_START_MINUTE}")
|
||||
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `end_minute_of_day` INTEGER NOT NULL DEFAULT ${TimeLimitRule.MAX_END_MINUTE}")
|
||||
|
@ -219,71 +219,120 @@ object DatabaseMigrations {
|
|||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V30 = object: Migration(29, 30) {
|
||||
private val MIGRATE_TO_V30 = object: Migration(29, 30) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `user` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V31 = object: Migration(30, 31) {
|
||||
private val MIGRATE_TO_V31 = object: Migration(30, 31) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `user_limit_login_category` (`user_id` TEXT NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `user`(`id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS `user_limit_login_category_index_category_id` ON `user_limit_login_category` (`category_id`)")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V32 = object: Migration(31, 32) {
|
||||
private val MIGRATE_TO_V32 = object: Migration(31, 32) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `category_network_id` (`category_id` TEXT NOT NULL, `network_item_id` TEXT NOT NULL, `hashed_network_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `network_item_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V33 = object: Migration(32, 33) {
|
||||
private val MIGRATE_TO_V33 = object: Migration(32, 33) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `category` ADD COLUMN `disable_limits_until` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V34 = object: Migration(33, 34) {
|
||||
private val MIGRATE_TO_V34 = object: Migration(33, 34) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `child_task` (`task_id` TEXT NOT NULL, `category_id` TEXT NOT NULL, `task_title` TEXT NOT NULL, `extra_time_duration` INTEGER NOT NULL, `pending_request` INTEGER NOT NULL, `last_grant_timestamp` INTEGER NOT NULL, PRIMARY KEY(`task_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
|
||||
database.execSQL("ALTER TABLE `category` ADD COLUMN `tasks_version` TEXT NOT NULL DEFAULT ''")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V35 = object: Migration(34, 35) {
|
||||
private val MIGRATE_TO_V35 = object: Migration(34, 35) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `time_limit_rule` ADD COLUMN `per_day` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V36 = object: Migration(35, 36) {
|
||||
private val MIGRATE_TO_V36 = object: Migration(35, 36) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `user_limit_login_category` ADD COLUMN pre_block_duration INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V37 = object: Migration(36, 37) {
|
||||
private val MIGRATE_TO_V37 = object: Migration(36, 37) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `category` ADD COLUMN `flags` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V38 = object: Migration(37, 38) {
|
||||
private val MIGRATE_TO_V38 = object: Migration(37, 38) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE category ADD COLUMN block_notification_delay INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V39 = object: Migration(38, 39) {
|
||||
private val MIGRATE_TO_V39 = object: Migration(38, 39) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// nothing to do, there was just a new config item type added
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V40 = object: Migration(39, 40) {
|
||||
private val MIGRATE_TO_V40 = object: Migration(39, 40) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `category_time_warning` (`category_id` TEXT NOT NULL, `minutes` INTEGER NOT NULL, PRIMARY KEY(`category_id`, `minutes`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATE_TO_V41 = object: Migration(40, 41) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// nothing to do, there was just a new config item type added
|
||||
}
|
||||
}
|
||||
|
||||
val ALL = arrayOf(
|
||||
MIGRATE_TO_V2,
|
||||
MIGRATE_TO_V3,
|
||||
MIGRATE_TO_V4,
|
||||
MIGRATE_TO_V5,
|
||||
MIGRATE_TO_V6,
|
||||
MIGRATE_TO_V7,
|
||||
MIGRATE_TO_V8,
|
||||
MIGRATE_TO_V9,
|
||||
MIGRATE_TO_V10,
|
||||
MIGRATE_TO_V11,
|
||||
MIGRATE_TO_V12,
|
||||
MIGRATE_TO_V13,
|
||||
MIGRATE_TO_V14,
|
||||
MIGRATE_TO_V15,
|
||||
MIGRATE_TO_V16,
|
||||
MIGRATE_TO_V17,
|
||||
MIGRATE_TO_V18,
|
||||
MIGRATE_TO_V19,
|
||||
MIGRATE_TO_V20,
|
||||
MIGRATE_TO_V21,
|
||||
MIGRATE_TO_V22,
|
||||
MIGRATE_TO_V23,
|
||||
MIGRATE_TO_V24,
|
||||
MIGRATE_TO_V25,
|
||||
MIGRATE_TO_V26,
|
||||
MIGRATE_TO_V27,
|
||||
MIGRATE_TO_V28,
|
||||
MIGRATE_TO_V29,
|
||||
MIGRATE_TO_V30,
|
||||
MIGRATE_TO_V31,
|
||||
MIGRATE_TO_V32,
|
||||
MIGRATE_TO_V33,
|
||||
MIGRATE_TO_V34,
|
||||
MIGRATE_TO_V35,
|
||||
MIGRATE_TO_V36,
|
||||
MIGRATE_TO_V37,
|
||||
MIGRATE_TO_V38,
|
||||
MIGRATE_TO_V39,
|
||||
MIGRATE_TO_V40,
|
||||
MIGRATE_TO_V41
|
||||
)
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import androidx.room.Database
|
|||
import androidx.room.InvalidationTracker
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.data.dao.DerivedDataDao
|
||||
|
@ -52,7 +53,7 @@ import java.util.concurrent.TimeUnit
|
|||
CategoryNetworkId::class,
|
||||
ChildTask::class,
|
||||
CategoryTimeWarning::class
|
||||
], version = 40)
|
||||
], version = 41)
|
||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||
companion object {
|
||||
private val lock = Object()
|
||||
|
@ -87,47 +88,7 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
|
|||
)
|
||||
.setJournalMode(JournalMode.TRUNCATE)
|
||||
.fallbackToDestructiveMigrationOnDowngrade()
|
||||
.addMigrations(
|
||||
DatabaseMigrations.MIGRATE_TO_V2,
|
||||
DatabaseMigrations.MIGRATE_TO_V3,
|
||||
DatabaseMigrations.MIGRATE_TO_V4,
|
||||
DatabaseMigrations.MIGRATE_TO_V5,
|
||||
DatabaseMigrations.MIGRATE_TO_V6,
|
||||
DatabaseMigrations.MIGRATE_TO_V7,
|
||||
DatabaseMigrations.MIGRATE_TO_V8,
|
||||
DatabaseMigrations.MIGRATE_TO_V9,
|
||||
DatabaseMigrations.MIGRATE_TO_V10,
|
||||
DatabaseMigrations.MIGRATE_TO_V11,
|
||||
DatabaseMigrations.MIGRATE_TO_V12,
|
||||
DatabaseMigrations.MIGRATE_TO_V13,
|
||||
DatabaseMigrations.MIGRATE_TO_V14,
|
||||
DatabaseMigrations.MIGRATE_TO_V15,
|
||||
DatabaseMigrations.MIGRATE_TO_V16,
|
||||
DatabaseMigrations.MIGRATE_TO_V17,
|
||||
DatabaseMigrations.MIGRATE_TO_V18,
|
||||
DatabaseMigrations.MIGRATE_TO_V19,
|
||||
DatabaseMigrations.MIGRATE_TO_V20,
|
||||
DatabaseMigrations.MIGRATE_TO_V21,
|
||||
DatabaseMigrations.MIGRATE_TO_V22,
|
||||
DatabaseMigrations.MIGRATE_TO_V23,
|
||||
DatabaseMigrations.MIGRATE_TO_V24,
|
||||
DatabaseMigrations.MIGRATE_TO_V25,
|
||||
DatabaseMigrations.MIGRATE_TO_V26,
|
||||
DatabaseMigrations.MIGRATE_TO_V27,
|
||||
DatabaseMigrations.MIGRATE_TO_V28,
|
||||
DatabaseMigrations.MIGRATE_TO_V29,
|
||||
DatabaseMigrations.MIGRATE_TO_V30,
|
||||
DatabaseMigrations.MIGRATE_TO_V31,
|
||||
DatabaseMigrations.MIGRATE_TO_V32,
|
||||
DatabaseMigrations.MIGRATE_TO_V33,
|
||||
DatabaseMigrations.MIGRATE_TO_V34,
|
||||
DatabaseMigrations.MIGRATE_TO_V35,
|
||||
DatabaseMigrations.MIGRATE_TO_V36,
|
||||
DatabaseMigrations.MIGRATE_TO_V37,
|
||||
DatabaseMigrations.MIGRATE_TO_V38,
|
||||
DatabaseMigrations.MIGRATE_TO_V39,
|
||||
DatabaseMigrations.MIGRATE_TO_V40
|
||||
)
|
||||
.addMigrations(*DatabaseMigrations.ALL)
|
||||
.setQueryExecutor(Threads.database)
|
||||
.addCallback(object: Callback() {
|
||||
override fun onOpen(db: SupportSQLiteDatabase) {
|
||||
|
|
|
@ -330,4 +330,28 @@ abstract class ConfigDao {
|
|||
|
||||
fun getAnnoyManualUnblockCounter() = getValueOfKeySync(ConfigurationItemType.AnnoyManualUnblockCounter).let { it?.toInt() ?: 0 }
|
||||
fun setAnoyManualUnblockCounterSync(counter: Int) { updateValueSync(ConfigurationItemType.AnnoyManualUnblockCounter, counter.toString()) }
|
||||
|
||||
private val consentFlags: LiveData<Long> by lazy {
|
||||
getValueOfKeyAsync(ConfigurationItemType.ConsentFlags).map {
|
||||
it?.toLong(16) ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
fun getConsentFlagsSync(): Long = getValueOfKeySync(ConfigurationItemType.ConsentFlags).let {
|
||||
it?.toLong(16) ?: 0
|
||||
}
|
||||
|
||||
fun isConsentFlagSetAsync(flags: Long) = consentFlags.map {
|
||||
(it and flags) == flags
|
||||
}.ignoreUnchanged()
|
||||
|
||||
fun setConsentFlagSync(flags: Long, enable: Boolean) {
|
||||
updateValueSync(
|
||||
ConfigurationItemType.ConsentFlags,
|
||||
if (enable)
|
||||
(getConsentFlagsSync() or flags).toString(16)
|
||||
else
|
||||
(getConsentFlagsSync() and (flags.inv())).toString(16)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,6 +101,7 @@ enum class ConfigurationItemType {
|
|||
CustomOrganizationName,
|
||||
ServerApiLevel,
|
||||
AnnoyManualUnblockCounter,
|
||||
ConsentFlags,
|
||||
}
|
||||
|
||||
object ConfigurationItemTypeUtil {
|
||||
|
@ -128,6 +129,7 @@ object ConfigurationItemTypeUtil {
|
|||
private const val CUSTOM_ORGANIZATION_NAME = 23
|
||||
private const val SERVER_API_LEVEL = 24
|
||||
private const val ANNOY_MANUAL_UNBLOCK_COUNTER = 25
|
||||
private const val CONSENT_FLAGS = 26
|
||||
|
||||
val TYPES = listOf(
|
||||
ConfigurationItemType.OwnDeviceId,
|
||||
|
@ -153,7 +155,8 @@ object ConfigurationItemTypeUtil {
|
|||
ConfigurationItemType.UpdateStatus,
|
||||
ConfigurationItemType.CustomOrganizationName,
|
||||
ConfigurationItemType.ServerApiLevel,
|
||||
ConfigurationItemType.AnnoyManualUnblockCounter
|
||||
ConfigurationItemType.AnnoyManualUnblockCounter,
|
||||
ConfigurationItemType.ConsentFlags
|
||||
)
|
||||
|
||||
fun serialize(value: ConfigurationItemType) = when(value) {
|
||||
|
@ -181,6 +184,7 @@ object ConfigurationItemTypeUtil {
|
|||
ConfigurationItemType.CustomOrganizationName -> CUSTOM_ORGANIZATION_NAME
|
||||
ConfigurationItemType.ServerApiLevel -> SERVER_API_LEVEL
|
||||
ConfigurationItemType.AnnoyManualUnblockCounter -> ANNOY_MANUAL_UNBLOCK_COUNTER
|
||||
ConfigurationItemType.ConsentFlags -> CONSENT_FLAGS
|
||||
}
|
||||
|
||||
fun parse(value: Int) = when(value) {
|
||||
|
@ -208,6 +212,7 @@ object ConfigurationItemTypeUtil {
|
|||
CUSTOM_ORGANIZATION_NAME -> ConfigurationItemType.CustomOrganizationName
|
||||
SERVER_API_LEVEL -> ConfigurationItemType.ServerApiLevel
|
||||
ANNOY_MANUAL_UNBLOCK_COUNTER -> ConfigurationItemType.AnnoyManualUnblockCounter
|
||||
CONSENT_FLAGS -> ConfigurationItemType.ConsentFlags
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
@ -251,4 +256,8 @@ object ExperimentalFlags {
|
|||
// const val INSTANCE_ID_FG_APP_DETECTION = 65536L
|
||||
// private const val OBSOLETE_DISABLE_FG_APP_DETECTION_FALLBACK = 131072L
|
||||
const val STRICT_OVERLAY_CHECKING = 0x40000L
|
||||
}
|
||||
|
||||
object ConsentFlags {
|
||||
const val APP_LIST_SYNC = 1L
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -28,7 +28,8 @@ data class DeviceRelatedData (
|
|||
val isLocalMode: Boolean,
|
||||
val hasValidDefaultUser: Boolean,
|
||||
val temporarilyAllowedApps: Set<String>,
|
||||
val experimentalFlags: Long
|
||||
val experimentalFlags: Long,
|
||||
val consentFlags: Long
|
||||
): Observer {
|
||||
companion object {
|
||||
private val relatedTables = arrayOf(Table.ConfigurationItem, Table.Device, Table.User, Table.TemporarilyAllowedApp)
|
||||
|
@ -41,6 +42,7 @@ data class DeviceRelatedData (
|
|||
val hasValidDefaultUser = database.user().getUserByIdSync(deviceEntry.defaultUser) != null
|
||||
val temporarilyAllowedApps = database.temporarilyAllowedApp().getTemporarilyAllowedAppsSync().toSet()
|
||||
val experimentalFlags = database.config().getExperimentalFlagsSync()
|
||||
val consentFlags = database.config().getConsentFlagsSync()
|
||||
|
||||
DeviceRelatedData(
|
||||
deviceEntry = deviceEntry,
|
||||
|
@ -48,7 +50,8 @@ data class DeviceRelatedData (
|
|||
isLocalMode = isLocalMode,
|
||||
hasValidDefaultUser = hasValidDefaultUser,
|
||||
temporarilyAllowedApps = temporarilyAllowedApps,
|
||||
experimentalFlags = experimentalFlags
|
||||
experimentalFlags = experimentalFlags,
|
||||
consentFlags = consentFlags
|
||||
).also {
|
||||
database.registerWeakObserver(relatedTables, WeakReference(it))
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -236,5 +236,50 @@ fun <T1, T2, T3, T4> mergeLiveDataWaitForValues(d1: LiveData<T1>, d2: LiveData<T
|
|||
update()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun <T1, T2, T3, T4, T5> mergeLiveDataWaitForValues(d1: LiveData<T1>, d2: LiveData<T2>, d3: LiveData<T3>, d4: LiveData<T4>, d5: LiveData<T5>): LiveData<FiveTuple<T1, T2, T3, T4, T5>> {
|
||||
val result = MediatorLiveData<FiveTuple<T1, T2, T3, T4, T5>>()
|
||||
var state = FiveTuple<Option<T1>, Option<T2>, Option<T3>, Option<T4>, Option<T5>>(Option.None(), Option.None(), Option.None(), Option.None(), Option.None())
|
||||
|
||||
fun update() {
|
||||
val (a, b, c, d, e) = state
|
||||
|
||||
if (a is Option.Some && b is Option.Some && c is Option.Some && d is Option.Some && e is Option.Some) {
|
||||
result.value = FiveTuple(a.value, b.value, c.value, d.value, e.value)
|
||||
}
|
||||
}
|
||||
|
||||
result.addSource(d1) {
|
||||
state = state.copy(first = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
result.addSource(d2) {
|
||||
state = state.copy(second = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
result.addSource(d3) {
|
||||
state = state.copy(third = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
result.addSource(d4) {
|
||||
state = state.copy(forth = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
result.addSource(d5) {
|
||||
state = state.copy(fifth = Option.Some(it))
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
|
@ -54,7 +54,7 @@ class AppLogic(
|
|||
}
|
||||
}.ignoreUnchanged()
|
||||
|
||||
val deviceEntryIfEnabled = enable.switchMap {
|
||||
val deviceEntryIfEnabled: LiveData<Device?> = enable.switchMap {
|
||||
if (it == null || it == false) {
|
||||
liveDataFromNullableValue(null as Device?)
|
||||
} else {
|
||||
|
@ -95,8 +95,9 @@ class AppLogic(
|
|||
websocketClientCreator = websocketClientCreator
|
||||
)
|
||||
|
||||
val syncAppsLogic = SyncInstalledAppsLogic(this)
|
||||
|
||||
init {
|
||||
SyncInstalledAppsLogic(this)
|
||||
WatchdogLogic(this)
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import io.timelimit.android.coroutines.executeAndWait
|
|||
import io.timelimit.android.coroutines.runAsyncExpectForever
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.data.model.AppActivity
|
||||
import io.timelimit.android.data.model.ConsentFlags
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||
import io.timelimit.android.livedata.*
|
||||
|
@ -45,17 +46,53 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
|
|||
requestSync.value = true
|
||||
}
|
||||
|
||||
private val deviceStateLive = mergeLiveDataWaitForValues(
|
||||
appLogic.deviceEntryIfEnabled,
|
||||
appLogic.database.config().isConsentFlagSetAsync(ConsentFlags.APP_LIST_SYNC),
|
||||
appLogic.database.config().getDeviceAuthTokenAsync().map { it.isEmpty() },
|
||||
appLogic.deviceUserEntry,
|
||||
appLogic.deviceEntryIfEnabled.switchMap { deviceEntry ->
|
||||
val defaultUser = deviceEntry?.defaultUser
|
||||
|
||||
if (defaultUser.isNullOrEmpty()) liveDataFromNullableValue(null)
|
||||
else appLogic.database.user().getUserByIdLive(defaultUser)
|
||||
}
|
||||
).map { (deviceEntry, hasSyncConsent, isLocalMode, deviceUser, deviceDefaultUser) ->
|
||||
deviceEntry?.let { device ->
|
||||
DeviceState(
|
||||
id = device.id,
|
||||
isCurrentUserChild = deviceUser?.type == UserType.Child,
|
||||
isDefaultUserChild = deviceDefaultUser?.type == UserType.Child,
|
||||
enableActivityLevelBlocking = device.enableActivityLevelBlocking,
|
||||
isDeviceOwner = device.currentProtectionLevel == ProtectionLevel.DeviceOwner,
|
||||
hasSyncConsent = hasSyncConsent,
|
||||
isLocalMode = isLocalMode
|
||||
)
|
||||
}
|
||||
}.ignoreUnchanged()
|
||||
|
||||
val shouldAskForConsent = deviceStateLive.map { it?.shouldAskForConsent ?: false }.ignoreUnchanged()
|
||||
|
||||
private fun getDeviceStateSync(): DeviceState? {
|
||||
val userAndDeviceData = appLogic.database.derivedDataDao().getUserAndDeviceRelatedDataSync() ?: return null
|
||||
val deviceRelatedData = userAndDeviceData.deviceRelatedData
|
||||
val device = deviceRelatedData.deviceEntry
|
||||
val defaultUser = if (device.defaultUser.isNotEmpty()) appLogic.database.user().getUserByIdSync(device.defaultUser) else null
|
||||
|
||||
return DeviceState(
|
||||
id = device.id,
|
||||
isCurrentUserChild = userAndDeviceData.userRelatedData?.user?.type == UserType.Child,
|
||||
isDefaultUserChild = defaultUser?.type == UserType.Child,
|
||||
enableActivityLevelBlocking = device.enableActivityLevelBlocking,
|
||||
isDeviceOwner = device.currentProtectionLevel == ProtectionLevel.DeviceOwner,
|
||||
hasSyncConsent = deviceRelatedData.consentFlags and ConsentFlags.APP_LIST_SYNC == ConsentFlags.APP_LIST_SYNC,
|
||||
isLocalMode = deviceRelatedData.isLocalMode
|
||||
)
|
||||
}
|
||||
|
||||
init {
|
||||
appLogic.platformIntegration.installedAppsChangeListener = Runnable { requestSync() }
|
||||
appLogic.deviceEntryIfEnabled.map { device ->
|
||||
device?.let { DeviceState(
|
||||
id = device.id,
|
||||
currentUserId = device.currentUserId,
|
||||
defaultUser = device.defaultUser,
|
||||
enableActivityLevelBlocking = device.enableActivityLevelBlocking,
|
||||
isDeviceOwner = device.currentProtectionLevel == ProtectionLevel.DeviceOwner
|
||||
) }
|
||||
}.ignoreUnchanged().observeForever { requestSync() }
|
||||
deviceStateLive.observeForever { requestSync() }
|
||||
|
||||
runAsyncExpectForever { syncLoop() }
|
||||
}
|
||||
|
@ -88,26 +125,17 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
|
|||
|
||||
private suspend fun doSyncNow() {
|
||||
doSyncLock.withLock {
|
||||
val deviceEntry = appLogic.deviceEntryIfEnabled.waitForNullableValue()
|
||||
val deviceState = Threads.database.executeAndWait { getDeviceStateSync() } ?: return
|
||||
|
||||
if (deviceEntry == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (appLogic.database.config().getDeviceAuthTokenAsync().waitForNullableValue().isNullOrEmpty()) {
|
||||
if (deviceState.isLocalMode) {
|
||||
// local mode -> sync always
|
||||
} else {
|
||||
// connected mode -> don't sync always
|
||||
|
||||
val userEntry = appLogic.deviceUserEntry.waitForNullableValue()
|
||||
val defaultUserEntry = appLogic.database.user().getUserByIdLive(deviceEntry.defaultUser).waitForNullableValue()
|
||||
|
||||
if (userEntry?.type != UserType.Child && defaultUserEntry?.type != UserType.Child) {
|
||||
return@withLock
|
||||
}
|
||||
if (!deviceState.hasSyncConsent) return@withLock
|
||||
if (!deviceState.hasAnyChildUser) return@withLock
|
||||
}
|
||||
|
||||
val deviceId = deviceEntry.id
|
||||
val deviceId = deviceState.id
|
||||
|
||||
val currentlyInstalledApps = getCurrentApps(deviceId)
|
||||
|
||||
|
@ -153,7 +181,7 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
|
|||
run {
|
||||
fun buildKey(activity: AppActivity) = "${activity.appPackageName}:${activity.activityClassName}"
|
||||
|
||||
val currentlyInstalled = if (deviceEntry.enableActivityLevelBlocking)
|
||||
val currentlyInstalled = if (deviceState.enableActivityLevelBlocking)
|
||||
Threads.backgroundOSInteraction.executeAndWait {
|
||||
val realActivities = appLogic.platformIntegration.getLocalAppActivities(deviceId = deviceId)
|
||||
val dummyActivities = currentlyInstalledApps.keys.map { packageName ->
|
||||
|
@ -220,9 +248,14 @@ class SyncInstalledAppsLogic(val appLogic: AppLogic) {
|
|||
|
||||
internal data class DeviceState(
|
||||
val id: String,
|
||||
val currentUserId: String,
|
||||
val defaultUser: String,
|
||||
val isCurrentUserChild: Boolean,
|
||||
val isDefaultUserChild: Boolean,
|
||||
val enableActivityLevelBlocking: Boolean,
|
||||
val isDeviceOwner: Boolean
|
||||
)
|
||||
val isDeviceOwner: Boolean,
|
||||
val hasSyncConsent: Boolean,
|
||||
val isLocalMode: Boolean
|
||||
) {
|
||||
val hasAnyChildUser = isCurrentUserChild || isDefaultUserChild
|
||||
val shouldAskForConsent = hasAnyChildUser && !isLocalMode && !hasSyncConsent
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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.consent
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.data.model.ConsentFlags
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
|
||||
class SyncAppListConsentDialogFragment: DialogFragment() {
|
||||
companion object {
|
||||
private const val DIALOG_TAG = "SyncAppListConsentDialogFragment"
|
||||
private const val LOG_TAG = "SyncAppListConsent"
|
||||
|
||||
fun newInstance() = SyncAppListConsentDialogFragment()
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = AlertDialog.Builder(requireContext(), theme)
|
||||
.setTitle(R.string.consent_app_list_sync_dialog_title)
|
||||
.setMessage(R.string.consent_app_list_sync_dialog_text)
|
||||
.setNegativeButton(R.string.generic_reject, null)
|
||||
.setPositiveButton(R.string.generic_accept) { _, _ ->
|
||||
val database = DefaultAppLogic.with(requireContext()).database
|
||||
|
||||
Threads.database.execute {
|
||||
try {
|
||||
database.config().setConsentFlagSync(ConsentFlags.APP_LIST_SYNC, true)
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w(LOG_TAG, "Could not save consent", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.create()
|
||||
|
||||
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -23,6 +23,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.model.Category
|
||||
import io.timelimit.android.databinding.AddItemViewBinding
|
||||
import io.timelimit.android.databinding.AppListSyncPermissionRequestCardBinding
|
||||
import io.timelimit.android.databinding.CategoryRichCardBinding
|
||||
import io.timelimit.android.databinding.IntroCardBinding
|
||||
import io.timelimit.android.ui.util.DateUtil
|
||||
|
@ -35,6 +36,7 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
|
|||
private const val TYPE_ADD = 1
|
||||
private const val TYPE_INTRO = 2
|
||||
private const val TYPE_MANIPULATION_WARNING = 3
|
||||
private const val TYPE_APP_LIST_BANNER = 4
|
||||
}
|
||||
|
||||
var categories: List<ManageChildCategoriesListItem>? by Delegates.observable(null as List<ManageChildCategoriesListItem>?) { _, _, _ -> notifyDataSetChanged() }
|
||||
|
@ -53,6 +55,7 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
|
|||
CreateCategoryItem -> item.hashCode()
|
||||
CategoriesIntroductionHeader -> item.hashCode()
|
||||
ManipulationWarningCategoryItem -> item.hashCode()
|
||||
ManageChildCategoriesListItem.SyncAppListBanner -> item.hashCode()
|
||||
}.toLong()
|
||||
}
|
||||
|
||||
|
@ -61,6 +64,7 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
|
|||
CreateCategoryItem -> TYPE_ADD
|
||||
CategoriesIntroductionHeader -> TYPE_INTRO
|
||||
ManipulationWarningCategoryItem -> TYPE_MANIPULATION_WARNING
|
||||
ManageChildCategoriesListItem.SyncAppListBanner -> TYPE_APP_LIST_BANNER
|
||||
}
|
||||
|
||||
override fun getItemCount() = categories?.size ?: 0
|
||||
|
@ -104,6 +108,13 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
|
|||
.inflate(R.layout.manage_child_manipulation_warning, parent, false)
|
||||
)
|
||||
|
||||
TYPE_APP_LIST_BANNER ->
|
||||
SyncAppListViewHolder(
|
||||
AppListSyncPermissionRequestCardBinding.inflate(LayoutInflater.from(parent.context), parent, false).also {
|
||||
it.detailButton.setOnClickListener { handlers?.onRequestAppListSyncConsentClicked() }
|
||||
}.root
|
||||
)
|
||||
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
|
||||
|
@ -163,6 +174,9 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
|
|||
ManipulationWarningCategoryItem -> {
|
||||
// nothing to do
|
||||
}
|
||||
ManageChildCategoriesListItem.SyncAppListBanner -> {
|
||||
// nothing to do
|
||||
}
|
||||
}.let { }
|
||||
}
|
||||
}
|
||||
|
@ -171,10 +185,12 @@ sealed class ViewHolder(view: View): RecyclerView.ViewHolder(view)
|
|||
class AddViewHolder(view: View): ViewHolder(view)
|
||||
class IntroViewHolder(view: View): ViewHolder(view)
|
||||
class ManipulationWarningViewHolder(view: View): ViewHolder(view)
|
||||
class SyncAppListViewHolder(view: View): ViewHolder(view)
|
||||
class ItemViewHolder(val binding: CategoryRichCardBinding): ViewHolder(binding.root)
|
||||
|
||||
interface Handlers {
|
||||
fun onCategoryClicked(category: Category)
|
||||
fun onCreateCategoryClicked()
|
||||
fun onCategorySwitched(category: CategoryItem, isChecked: Boolean): Boolean
|
||||
fun onRequestAppListSyncConsentClicked()
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -17,7 +17,10 @@ package io.timelimit.android.ui.manage.child.category
|
|||
|
||||
import io.timelimit.android.data.model.Category
|
||||
|
||||
sealed class ManageChildCategoriesListItem
|
||||
sealed class ManageChildCategoriesListItem {
|
||||
object SyncAppListBanner: ManageChildCategoriesListItem()
|
||||
}
|
||||
|
||||
object CategoriesIntroductionHeader: ManageChildCategoriesListItem()
|
||||
object CreateCategoryItem: ManageChildCategoriesListItem()
|
||||
object ManipulationWarningCategoryItem: ManageChildCategoriesListItem()
|
||||
|
|
|
@ -37,6 +37,7 @@ import io.timelimit.android.logic.DefaultAppLogic
|
|||
import io.timelimit.android.sync.actions.UpdateCategoryDisableLimitsAction
|
||||
import io.timelimit.android.sync.actions.UpdateCategorySortingAction
|
||||
import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction
|
||||
import io.timelimit.android.ui.consent.SyncAppListConsentDialogFragment
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.main.getActivityViewModel
|
||||
import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs
|
||||
|
@ -58,7 +59,7 @@ class ManageChildCategoriesFragment : Fragment() {
|
|||
private val model: ManageChildCategoriesModel by viewModels()
|
||||
private lateinit var binding: RecyclerFragmentBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = RecyclerFragmentBinding.inflate(inflater, container, false)
|
||||
|
||||
return binding.root
|
||||
|
@ -136,6 +137,10 @@ class ManageChildCategoriesFragment : Fragment() {
|
|||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestAppListSyncConsentClicked() {
|
||||
SyncAppListConsentDialogFragment.newInstance().show(parentFragmentManager)
|
||||
}
|
||||
}
|
||||
|
||||
binding.recycler.adapter = adapter
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -144,23 +144,25 @@ class ManageChildCategoriesModel(application: Application): AndroidViewModel(app
|
|||
|
||||
private val hasShownHint = logic.database.config().wereHintsShown(HintsToShow.CATEGORIES_INTRODUCTION)
|
||||
|
||||
private val listContentStep1 = hasShownHint.switchMap { hasShownHint ->
|
||||
categoryItems.map { categoryItems ->
|
||||
if (hasShownHint) {
|
||||
categoryItems + listOf(CreateCategoryItem)
|
||||
} else {
|
||||
listOf(CategoriesIntroductionHeader) + categoryItems + listOf(CreateCategoryItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val showSyncConsentBanner = logic.syncAppsLogic.shouldAskForConsent
|
||||
|
||||
val listContent = hasNotSuppressedChildDeviceManipulation.switchMap { hasChildDevicesWithManipulation ->
|
||||
listContentStep1.map { listContent ->
|
||||
if (hasChildDevicesWithManipulation) {
|
||||
listOf(ManipulationWarningCategoryItem) + listContent
|
||||
} else {
|
||||
listContent
|
||||
}
|
||||
}
|
||||
val listContent = mergeLiveDataWaitForValues(
|
||||
categoryItems,
|
||||
hasShownHint,
|
||||
showSyncConsentBanner,
|
||||
hasNotSuppressedChildDeviceManipulation
|
||||
).map { (categoryItems, hasShownHint, showSyncConsentBanner, hasChildDevicesWithManipulation) ->
|
||||
val headers1 = emptyList<ManageChildCategoriesListItem>()
|
||||
|
||||
val headers2 = if (hasShownHint) headers1
|
||||
else headers1 + listOf(CategoriesIntroductionHeader)
|
||||
|
||||
val headers3 = if (showSyncConsentBanner) headers2 + listOf(ManageChildCategoriesListItem.SyncAppListBanner)
|
||||
else headers2
|
||||
|
||||
val headers4 = if (hasChildDevicesWithManipulation) headers3 + listOf(ManipulationWarningCategoryItem)
|
||||
else headers3
|
||||
|
||||
headers4 + categoryItems + listOf(CreateCategoryItem)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -24,10 +24,10 @@ import android.view.ViewGroup
|
|||
import android.widget.CheckBox
|
||||
import androidx.appcompat.widget.AppCompatRadioButton
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
|
@ -60,6 +60,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
private const val STATUS_ALLOWED_APPS_CATEGORY = "c"
|
||||
}
|
||||
|
||||
private val model: SetupDeviceModel by viewModels()
|
||||
private val selectedUser = MutableLiveData<String>()
|
||||
private val selectedAppsToNotWhitelist = mutableSetOf<String>()
|
||||
private var allowedAppsCategory = ""
|
||||
|
@ -89,11 +90,10 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
outState.putString(STATUS_ALLOWED_APPS_CATEGORY, allowedAppsCategory)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val binding = FragmentSetupDeviceBinding.inflate(inflater, container, false)
|
||||
val logic = DefaultAppLogic.with(requireContext())
|
||||
val activity = activity as ActivityViewModelHolder
|
||||
val model = ViewModelProviders.of(this).get(SetupDeviceModel::class.java)
|
||||
val navigation = Navigation.findNavController(container!!)
|
||||
|
||||
binding.needsParent.authBtn.setOnClickListener {
|
||||
|
@ -136,7 +136,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
}
|
||||
})
|
||||
|
||||
logic.database.user().getAllUsersLive().observe(this, Observer { users ->
|
||||
logic.database.user().getAllUsersLive().observe(viewLifecycleOwner) { users ->
|
||||
// ID to label
|
||||
val items = mutableListOf<Pair<String, String>>()
|
||||
|
||||
|
@ -150,7 +150,9 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
|
||||
// select the first item if nothing is selected currently
|
||||
if (items.find { (id) -> id == selectedUser.value } == null) {
|
||||
selectedUser.value = items.first().first
|
||||
items.firstOrNull()?.first?.let {
|
||||
selectedUser.value = it
|
||||
}
|
||||
}
|
||||
|
||||
// build the views
|
||||
|
@ -166,7 +168,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
binding.selectUserRadioGroup.removeAllViews()
|
||||
views.forEach { view -> binding.selectUserRadioGroup.addView(view) }
|
||||
views.find { it.tag == selectedUser.value }?.isChecked = true
|
||||
})
|
||||
}
|
||||
|
||||
val isNewUser = selectedUser.map { NEW_USER.contains(it) }
|
||||
val isParentUser = selectedUser.switchMap {
|
||||
|
@ -181,8 +183,8 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
}
|
||||
}
|
||||
|
||||
isNewUser.observe(this, Observer { binding.isAddingNewUser = it })
|
||||
isParentUser.observe(this, Observer { binding.isAddingChild = !it })
|
||||
isNewUser.observe(viewLifecycleOwner) { binding.isAddingNewUser = it }
|
||||
isParentUser.observe(viewLifecycleOwner) { binding.isAddingChild = !it }
|
||||
|
||||
val categoriesOfTheSelectedUser = selectedUser.switchMap { user ->
|
||||
if (NEW_USER.contains(user)) {
|
||||
|
@ -204,7 +206,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
recommendWhitelistLocalApps.filterNot { app -> assignedApps.contains(app.packageName) }
|
||||
}
|
||||
|
||||
appsToWhitelist.observe(this, Observer { apps ->
|
||||
appsToWhitelist.observe(viewLifecycleOwner) { apps ->
|
||||
binding.areThereAnyApps = apps.isNotEmpty()
|
||||
|
||||
binding.suggestedAllowedApps.removeAllViews()
|
||||
|
@ -225,9 +227,9 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
categoriesOfTheSelectedUser.observe(this, Observer { categories ->
|
||||
categoriesOfTheSelectedUser.observe(viewLifecycleOwner) { categories ->
|
||||
// id to title
|
||||
val items = mutableListOf<Pair<String, String>>()
|
||||
|
||||
|
@ -241,7 +243,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
} else {
|
||||
if (items.find { (id) -> id == allowedAppsCategory } == null) {
|
||||
// use the one with the lowest blocked times
|
||||
allowedAppsCategory = categories.sortedBy { it.blockedMinutesInWeek.dataNotToModify.cardinality() }.first().id
|
||||
allowedAppsCategory = categories.minByOrNull { it.blockedMinutesInWeek.dataNotToModify.cardinality() }!!.id
|
||||
}
|
||||
|
||||
binding.areThereAnyCategories = true
|
||||
|
@ -258,7 +260,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
views.forEach { view -> binding.allowedAppsCategory.addView(view) }
|
||||
views.find { it.tag == allowedAppsCategory }?.isChecked = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
val selectedName = MutableLiveData<String>().apply { value = binding.newUserName.text.toString() }
|
||||
binding.newUserName.addTextChangedListener(object: TextWatcher {
|
||||
|
@ -281,9 +283,9 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
)
|
||||
val validationOfAll = (validationOfName.and(validationOfPassword)).or(isNewUser.invert())
|
||||
|
||||
validationOfAll.observe(this, Observer { binding.confirmBtn.isEnabled = it })
|
||||
validationOfAll.observe(viewLifecycleOwner) { binding.confirmBtn.isEnabled = it }
|
||||
|
||||
isPasswordRequired.observe(this, Observer { binding.setPasswordView.allowNoPassword.value = !it })
|
||||
isPasswordRequired.observe(viewLifecycleOwner) { binding.setPasswordView.allowNoPassword.value = !it }
|
||||
|
||||
ManageDeviceBackgroundSync.bind(
|
||||
view = binding.backgroundSync,
|
||||
|
@ -302,7 +304,8 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
appsToNotWhitelist = selectedAppsToNotWhitelist,
|
||||
model = activity.getActivityViewModel(),
|
||||
networkTime = SetupNetworkTimeVerification.readSelection(binding.networkTimeVerification),
|
||||
enableUpdateChecks = binding.update.enableSwitch.isChecked
|
||||
enableUpdateChecks = binding.update.enableSwitch.isChecked,
|
||||
enableAppListSync = binding.appListSync.enableSwitch.isChecked && (isParentUser.value == false)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -16,13 +16,18 @@
|
|||
package io.timelimit.android.ui.setup.device
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.model.AppRecommendation
|
||||
import io.timelimit.android.data.model.ConsentFlags
|
||||
import io.timelimit.android.data.model.NetworkTime
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.livedata.castDown
|
||||
|
@ -35,6 +40,10 @@ import io.timelimit.android.ui.user.create.DefaultCategories
|
|||
import io.timelimit.android.update.UpdateUtil
|
||||
|
||||
class SetupDeviceModel(application: Application): AndroidViewModel(application) {
|
||||
companion object {
|
||||
private const val LOG_TAG = "SetupDeviceModel"
|
||||
}
|
||||
|
||||
private val logic = DefaultAppLogic.with(application)
|
||||
private val statusInternal = MutableLiveData<SetupDeviceModelStatus>().apply { value = SetupDeviceModelStatus.Ready }
|
||||
|
||||
|
@ -48,7 +57,8 @@ class SetupDeviceModel(application: Application): AndroidViewModel(application)
|
|||
appsToNotWhitelist: Set<String>,
|
||||
model: ActivityViewModel,
|
||||
networkTime: NetworkTime,
|
||||
enableUpdateChecks: Boolean
|
||||
enableUpdateChecks: Boolean,
|
||||
enableAppListSync: Boolean
|
||||
) {
|
||||
if (statusInternal.value != SetupDeviceModelStatus.Ready) {
|
||||
return
|
||||
|
@ -57,123 +67,158 @@ class SetupDeviceModel(application: Application): AndroidViewModel(application)
|
|||
statusInternal.value = SetupDeviceModelStatus.Working
|
||||
|
||||
runAsync {
|
||||
val actions = mutableListOf<ParentAction>()
|
||||
var realUserId = userId
|
||||
var realAllowedAppsCategory = allowedAppsCategory
|
||||
val defaultCategories = DefaultCategories.with(getApplication())
|
||||
try {
|
||||
val actions = mutableListOf<ParentAction>()
|
||||
var realUserId = userId
|
||||
var realAllowedAppsCategory = allowedAppsCategory
|
||||
val defaultCategories = DefaultCategories.with(getApplication())
|
||||
|
||||
val isUserAnChild = when (userId) {
|
||||
SetupDeviceFragment.NEW_PARENT -> {
|
||||
// generate user id
|
||||
realUserId = IdGenerator.generateId()
|
||||
val isUserAnChild = when (userId) {
|
||||
SetupDeviceFragment.NEW_PARENT -> {
|
||||
// generate user id
|
||||
realUserId = IdGenerator.generateId()
|
||||
|
||||
// create parent
|
||||
actions.add(AddUserAction(
|
||||
userId = realUserId,
|
||||
name = username,
|
||||
timeZone = logic.timeApi.getSystemTimeZone().id,
|
||||
userType = UserType.Parent,
|
||||
password = ParentPassword.createCoroutine(password)
|
||||
))
|
||||
// create parent
|
||||
actions.add(
|
||||
AddUserAction(
|
||||
userId = realUserId,
|
||||
name = username,
|
||||
timeZone = logic.timeApi.getSystemTimeZone().id,
|
||||
userType = UserType.Parent,
|
||||
password = ParentPassword.createCoroutine(password)
|
||||
)
|
||||
)
|
||||
|
||||
false
|
||||
false
|
||||
}
|
||||
SetupDeviceFragment.NEW_CHILD -> {
|
||||
// generate user id
|
||||
realUserId = IdGenerator.generateId()
|
||||
|
||||
// create child
|
||||
actions.add(
|
||||
AddUserAction(
|
||||
userId = realUserId,
|
||||
name = username,
|
||||
timeZone = logic.timeApi.getSystemTimeZone().id,
|
||||
userType = UserType.Child,
|
||||
password = if (password.isEmpty()) null else ParentPassword.createCoroutine(
|
||||
password
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// create default categories
|
||||
realAllowedAppsCategory = IdGenerator.generateId()
|
||||
val allowedGamesCategory = IdGenerator.generateId()
|
||||
|
||||
actions.add(
|
||||
CreateCategoryAction(
|
||||
childId = realUserId,
|
||||
categoryId = realAllowedAppsCategory,
|
||||
title = defaultCategories.allowedAppsTitle
|
||||
)
|
||||
)
|
||||
|
||||
actions.add(
|
||||
CreateCategoryAction(
|
||||
childId = realUserId,
|
||||
categoryId = allowedGamesCategory,
|
||||
title = defaultCategories.allowedGamesTitle
|
||||
)
|
||||
)
|
||||
|
||||
defaultCategories.generateGamesTimeLimitRules(allowedGamesCategory)
|
||||
.forEach { rule ->
|
||||
actions.add(CreateTimeLimitRuleAction(rule))
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
else -> {
|
||||
logic.database.user().getUserByIdLive(userId)
|
||||
.waitForNullableValue()!!.type == UserType.Child
|
||||
}
|
||||
}
|
||||
SetupDeviceFragment.NEW_CHILD -> {
|
||||
// generate user id
|
||||
realUserId = IdGenerator.generateId()
|
||||
|
||||
// create child
|
||||
actions.add(AddUserAction(
|
||||
userId = realUserId,
|
||||
name = username,
|
||||
timeZone = logic.timeApi.getSystemTimeZone().id,
|
||||
userType = UserType.Child,
|
||||
password = if (password.isEmpty()) null else ParentPassword.createCoroutine(password)
|
||||
))
|
||||
if (isUserAnChild) {
|
||||
if (realAllowedAppsCategory == "") {
|
||||
// create allowed apps category if none was specified and overwrite its id
|
||||
realAllowedAppsCategory = IdGenerator.generateId()
|
||||
|
||||
// create default categories
|
||||
realAllowedAppsCategory = IdGenerator.generateId()
|
||||
val allowedGamesCategory = IdGenerator.generateId()
|
||||
|
||||
actions.add(CreateCategoryAction(
|
||||
childId = realUserId,
|
||||
categoryId = realAllowedAppsCategory,
|
||||
title = defaultCategories.allowedAppsTitle
|
||||
))
|
||||
|
||||
actions.add(CreateCategoryAction(
|
||||
childId = realUserId,
|
||||
categoryId = allowedGamesCategory,
|
||||
title = defaultCategories.allowedGamesTitle
|
||||
))
|
||||
|
||||
defaultCategories.generateGamesTimeLimitRules(allowedGamesCategory).forEach { rule ->
|
||||
actions.add(CreateTimeLimitRuleAction(rule))
|
||||
actions.add(
|
||||
CreateCategoryAction(
|
||||
childId = realUserId,
|
||||
categoryId = realAllowedAppsCategory,
|
||||
title = defaultCategories.allowedAppsTitle
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
else -> {
|
||||
logic.database.user().getUserByIdLive(userId).waitForNullableValue()!!.type == UserType.Child
|
||||
}
|
||||
}
|
||||
val alreadyAssignedApps = Threads.database.executeAndWait {
|
||||
logic.database.categoryApp().getCategoryAppsByUserIdSync(realUserId)
|
||||
.filter { it.appSpecifier.deviceId == null }
|
||||
.map { it.appSpecifier.packageName }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
if (isUserAnChild) {
|
||||
if (realAllowedAppsCategory == "") {
|
||||
// create allowed apps category if none was specified and overwrite its id
|
||||
realAllowedAppsCategory = IdGenerator.generateId()
|
||||
// add allowed apps
|
||||
val allowedAppsPackages =
|
||||
logic.platformIntegration.getLocalApps(IdGenerator.generateId())
|
||||
.filter { app -> app.recommendation == AppRecommendation.Whitelist }
|
||||
.map { app -> app.packageName }
|
||||
.toMutableSet().apply {
|
||||
removeAll(appsToNotWhitelist)
|
||||
removeAll(alreadyAssignedApps)
|
||||
}.toList()
|
||||
|
||||
actions.add(CreateCategoryAction(
|
||||
childId = realUserId,
|
||||
categoryId = realAllowedAppsCategory,
|
||||
title = defaultCategories.allowedAppsTitle
|
||||
))
|
||||
if (allowedAppsPackages.isNotEmpty()) {
|
||||
actions.add(
|
||||
AddCategoryAppsAction(
|
||||
categoryId = realAllowedAppsCategory,
|
||||
packageNames = allowedAppsPackages
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val alreadyAssignedApps = Threads.database.executeAndWait {
|
||||
logic.database.categoryApp().getCategoryAppsByUserIdSync(realUserId)
|
||||
.filter { it.appSpecifier.deviceId == null }
|
||||
.map { it.appSpecifier.packageName }
|
||||
.toSet()
|
||||
// apply the network time mode
|
||||
val deviceId = logic.deviceId.waitForNullableValue()!!
|
||||
|
||||
actions.add(
|
||||
UpdateNetworkTimeVerificationAction(
|
||||
deviceId = deviceId,
|
||||
mode = networkTime
|
||||
)
|
||||
)
|
||||
|
||||
// assign user to this device
|
||||
actions.add(
|
||||
SetDeviceUserAction(
|
||||
deviceId = deviceId,
|
||||
userId = realUserId
|
||||
)
|
||||
)
|
||||
|
||||
// configure update check
|
||||
UpdateUtil.setEnableChecks(getApplication(), enableUpdateChecks)
|
||||
|
||||
Threads.database.executeAndWait {
|
||||
DefaultAppLogic.with(getApplication()).database.config().setConsentFlagSync(ConsentFlags.APP_LIST_SYNC, enableAppListSync)
|
||||
}
|
||||
|
||||
// add allowed apps
|
||||
val allowedAppsPackages = logic.platformIntegration.getLocalApps(IdGenerator.generateId())
|
||||
.filter { app -> app.recommendation == AppRecommendation.Whitelist }
|
||||
.map { app -> app.packageName }
|
||||
.toMutableSet().apply {
|
||||
removeAll(appsToNotWhitelist)
|
||||
removeAll(alreadyAssignedApps)
|
||||
}.toList()
|
||||
|
||||
if (allowedAppsPackages.isNotEmpty()) {
|
||||
actions.add(AddCategoryAppsAction(
|
||||
categoryId = realAllowedAppsCategory,
|
||||
packageNames = allowedAppsPackages
|
||||
))
|
||||
if (model.tryDispatchParentActions(actions)) {
|
||||
statusInternal.value = SetupDeviceModelStatus.Done
|
||||
} else {
|
||||
statusInternal.value = SetupDeviceModelStatus.Ready
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "could not setup device", ex)
|
||||
}
|
||||
}
|
||||
|
||||
// apply the network time mode
|
||||
val deviceId = logic.deviceId.waitForNullableValue()!!
|
||||
Toast.makeText(getApplication(), R.string.error_general, Toast.LENGTH_SHORT).show()
|
||||
|
||||
actions.add(UpdateNetworkTimeVerificationAction(
|
||||
deviceId = deviceId,
|
||||
mode = networkTime
|
||||
))
|
||||
|
||||
// assign user to this device
|
||||
actions.add(SetDeviceUserAction(
|
||||
deviceId = deviceId,
|
||||
userId = realUserId
|
||||
))
|
||||
|
||||
// configure update check
|
||||
UpdateUtil.setEnableChecks(getApplication(), enableUpdateChecks)
|
||||
|
||||
if (model.tryDispatchParentActions(actions)) {
|
||||
statusInternal.value = SetupDeviceModelStatus.Done
|
||||
} else {
|
||||
statusInternal.value = SetupDeviceModelStatus.Ready
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2022 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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardUseCompatPadding="true">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
android:text="@string/consent_app_list_sync_dialog_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@string/consent_app_list_sync_card_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:layout_marginTop="16dp"
|
||||
android:id="@+id/detail_button"
|
||||
android:layout_gravity="end"
|
||||
android:text="@string/generic_show_details"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</layout>
|
|
@ -1,5 +1,5 @@
|
|||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
TimeLimit Copyright <C> 2019 - 2022 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.
|
||||
|
@ -203,6 +203,10 @@
|
|||
<include android:id="@+id/network_time_verification"
|
||||
layout="@layout/setup_network_time_verification" />
|
||||
|
||||
<include android:id="@+id/app_list_sync"
|
||||
android:visibility="@{isAddingChild ? View.VISIBLE : View.GONE}"
|
||||
layout="@layout/setup_device_app_list_sync" />
|
||||
|
||||
<include android:id="@+id/background_sync"
|
||||
layout="@layout/manage_device_background_sync_view" />
|
||||
|
||||
|
|
48
app/src/main/res/layout/setup_device_app_list_sync.xml
Normal file
48
app/src/main/res/layout/setup_device_app_list_sync.xml
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2022 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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardUseCompatPadding="true">
|
||||
<LinearLayout
|
||||
android:padding="8dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
android:text="@string/consent_app_list_sync_dialog_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@string/consent_app_list_sync_dialog_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:text="@string/consent_app_list_sync_switch_label"
|
||||
android:id="@+id/enable_switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</layout>
|
|
@ -30,6 +30,8 @@
|
|||
<string name="generic_yes">Ja</string>
|
||||
<string name="generic_skip">Überspringen</string>
|
||||
<string name="generic_show_details">Details anzeigen</string>
|
||||
<string name="generic_accept">Akzeptieren</string>
|
||||
<string name="generic_reject">Ablehnen</string>
|
||||
|
||||
<string name="generic_swipe_to_dismiss">Sie können diesen Hinweis entfernen, indem Sie ihn zur Seite wischen</string>
|
||||
<string name="generic_runtime_permission_rejected">Berechtigung abgelehnt; Sie können die Berechtigungen in den Systemeinstellungen verwalten</string>
|
||||
|
@ -275,7 +277,8 @@
|
|||
und daher auch keine Apps auf Geräten, die dem Benutzer zugeordnet wurden
|
||||
</string>
|
||||
<string name="category_apps_add_empty_child_devices_no_apps">Es gibt mindestens ein Gerät für diesen Benutzer,
|
||||
aber es sind keine Apps von diesem Gerät bekannt. Ist die Benutzerzuordnung am entsprechenden Gerät angekommen?
|
||||
aber es sind keine Apps von diesem Gerät bekannt. Ist die Benutzerzuordnung am entsprechenden Gerät angekommen
|
||||
und wurde dort die Synchronisation der App-Liste aktiviert?
|
||||
</string>
|
||||
<string name="category_apps_add_empty_all_devices_no_apps_no_childs">Es gibt kein Gerät, das einem Kind zugeordnet wurde.
|
||||
Daher wurden keine Apps erfasst und dadurch sind jetzt keine Apps bekannt, die zugerodnet werden können.
|
||||
|
@ -1391,7 +1394,7 @@
|
|||
<string name="setup_privacy_connected_title">Vernetzung und Datenschutz</string>
|
||||
<string name="setup_privacy_connected_text_general_intro">Für den vernetzten Modus
|
||||
gibt es eine Zentrale - den Server. Auf diesen werden die Einstellungen,
|
||||
die Nutzungsdauern und die Gerätezustände (erteilte Berechtigungen, installierte Apps und die App- und System-Version)
|
||||
die Nutzungsdauern und die Gerätezustände (erteilte Berechtigungen und die TimeLimit- und System-Version)
|
||||
gespeichert.
|
||||
</string>
|
||||
<string name="setup_privacy_connected_text_default_server">Sie verwenden - da Sie Nichts anderes gewählt haben -
|
||||
|
@ -1404,6 +1407,17 @@
|
|||
verhindern, indem Sie den lokalen Modus verwenden.
|
||||
</string>
|
||||
|
||||
<string name="consent_app_list_sync_dialog_title">App-Listen-Synchronisation</string>
|
||||
<string name="consent_app_list_sync_dialog_text">TimeLimit möchte die Liste
|
||||
der installierten Apps synchronisieren, um das Freigeben von Apps mittels Fernsteuerung von anderen
|
||||
verknüpften Geräten aus zu ermöglichen. Dafür wird die Liste auf dem TimeLimit-Server
|
||||
gespeichert. Diese Daten werden für keine anderen Zwecke verwendet.
|
||||
Alternativ können Sie auch den lokalen Modus verwenden.
|
||||
</string>
|
||||
<string name="consent_app_list_sync_switch_label">Synchronisation aktivieren</string>
|
||||
<string name="consent_app_list_sync_card_text">TimeLimit möchte die Liste der installierten Apps
|
||||
für die Fernsteuerungsfunktion synchronisieren.</string>
|
||||
|
||||
<string name="setup_remote_child_code_invalid">Dieser Code ist ungültig</string>
|
||||
<string name="setup_remote_child_title">Dieses Gerät verknüpfen</string>
|
||||
<string name="setup_remote_child_text">
|
||||
|
|
|
@ -33,6 +33,8 @@
|
|||
<string name="generic_yes">Yes</string>
|
||||
<string name="generic_skip">Skip</string>
|
||||
<string name="generic_show_details">Show details</string>
|
||||
<string name="generic_accept">Accept</string>
|
||||
<string name="generic_reject">Reject</string>
|
||||
|
||||
<string name="generic_swipe_to_dismiss">Swipe to the side to remove this message</string>
|
||||
<string name="generic_runtime_permission_rejected">Permission rejected; You can manage permissions in the system settings</string>
|
||||
|
@ -315,7 +317,8 @@
|
|||
</string>
|
||||
<string name="category_apps_add_empty_child_devices_no_apps">
|
||||
There is a device assigned to this user, but there are no know Apps.
|
||||
Did the device receive the user assignment?
|
||||
Did the device receive the user assignment and was the App List
|
||||
Synchronisation enabled at this device?
|
||||
</string>
|
||||
<string name="category_apps_add_empty_all_devices_no_apps_no_childs">There is no device with any child assigned.
|
||||
Due to that, no Apps were recorded and thus no assignable Apps are known.
|
||||
|
@ -1427,7 +1430,7 @@
|
|||
<string name="setup_privacy_connected_title">Networking and Privacy</string>
|
||||
<string name="setup_privacy_connected_text_general_intro">For the connected mode,
|
||||
there is a central unit - the server. This server saves the settings, the usage durations
|
||||
and the status of the devices (granted permissions, installed Apps, App and OS version).
|
||||
and the status of the devices (granted permissions, TimeLimit version and OS version).
|
||||
</string>
|
||||
<string name="setup_privacy_connected_text_default_server">Because you have not selected anything different,
|
||||
you are using the default server which is provided by the developer of the App.
|
||||
|
@ -1440,6 +1443,15 @@
|
|||
the data transmission by using the local mode.
|
||||
</string>
|
||||
|
||||
<string name="consent_app_list_sync_dialog_title">App List Synchronization</string>
|
||||
<string name="consent_app_list_sync_dialog_text">TimeLimit would like to sync the
|
||||
list of the installed Apps to make it possible to allow Apps remotely. For this,
|
||||
the list is saved at the TimeLimit server. This data is not used for any other purposes.
|
||||
Alternatively, you can use the local mode.
|
||||
</string>
|
||||
<string name="consent_app_list_sync_switch_label">Enable Synchronization</string>
|
||||
<string name="consent_app_list_sync_card_text">TimeLimit would like to sync the list of installed Apps for the remote control.</string>
|
||||
|
||||
<string name="setup_remote_child_code_invalid">This code is invalid</string>
|
||||
<string name="setup_remote_child_title">Link this device</string>
|
||||
<string name="setup_remote_child_text">
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue