mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +02:00
Add option to limit categories depending on the current network
This commit is contained in:
parent
1387ef86e2
commit
20ca6bcc91
49 changed files with 2265 additions and 92 deletions
1115
app/schemas/io.timelimit.android.data.RoomDatabase/32.json
Normal file
1115
app/schemas/io.timelimit.android.data.RoomDatabase/32.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -33,6 +33,9 @@
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||||
|
<!-- for the categories which are limited to a wifi network -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
<uses-permission-sdk-23 android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.telephony" android:required="false" />
|
<uses-feature android:name="android.hardware.telephony" android:required="false" />
|
||||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -24,5 +24,5 @@ object Sha512 {
|
||||||
return HexString.toHex(hashSync(data.toByteArray(charset("UTF-8"))))
|
return HexString.toHex(hashSync(data.toByteArray(charset("UTF-8"))))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hashSync(data: ByteArray) = messageDigest.digest(data)
|
fun hashSync(data: ByteArray): ByteArray = messageDigest.digest(data)
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ interface Database {
|
||||||
fun sessionDuration(): SessionDurationDao
|
fun sessionDuration(): SessionDurationDao
|
||||||
fun derivedDataDao(): DerivedDataDao
|
fun derivedDataDao(): DerivedDataDao
|
||||||
fun userLimitLoginCategoryDao(): UserLimitLoginCategoryDao
|
fun userLimitLoginCategoryDao(): UserLimitLoginCategoryDao
|
||||||
|
fun categoryNetworkId(): CategoryNetworkIdDao
|
||||||
|
|
||||||
fun <T> runInTransaction(block: () -> T): T
|
fun <T> runInTransaction(block: () -> T): T
|
||||||
fun <T> runInUnobservedTransaction(block: () -> T): T
|
fun <T> runInUnobservedTransaction(block: () -> T): T
|
||||||
|
|
|
@ -231,4 +231,10 @@ object DatabaseMigrations {
|
||||||
database.execSQL("CREATE INDEX IF NOT EXISTS `user_limit_login_category_index_category_id` ON `user_limit_login_category` (`category_id`)")
|
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) {
|
||||||
|
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 )")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,8 +47,9 @@ import java.util.concurrent.TimeUnit
|
||||||
AllowedContact::class,
|
AllowedContact::class,
|
||||||
UserKey::class,
|
UserKey::class,
|
||||||
SessionDuration::class,
|
SessionDuration::class,
|
||||||
UserLimitLoginCategory::class
|
UserLimitLoginCategory::class,
|
||||||
], version = 31)
|
CategoryNetworkId::class
|
||||||
|
], version = 32)
|
||||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||||
companion object {
|
companion object {
|
||||||
private val lock = Object()
|
private val lock = Object()
|
||||||
|
@ -113,7 +114,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
|
||||||
DatabaseMigrations.MIGRATE_TO_V28,
|
DatabaseMigrations.MIGRATE_TO_V28,
|
||||||
DatabaseMigrations.MIGRATE_TO_V29,
|
DatabaseMigrations.MIGRATE_TO_V29,
|
||||||
DatabaseMigrations.MIGRATE_TO_V30,
|
DatabaseMigrations.MIGRATE_TO_V30,
|
||||||
DatabaseMigrations.MIGRATE_TO_V31
|
DatabaseMigrations.MIGRATE_TO_V31,
|
||||||
|
DatabaseMigrations.MIGRATE_TO_V32
|
||||||
)
|
)
|
||||||
.setQueryExecutor(Threads.database)
|
.setQueryExecutor(Threads.database)
|
||||||
.build()
|
.build()
|
||||||
|
|
|
@ -43,6 +43,7 @@ object DatabaseBackupLowlevel {
|
||||||
private const val USER_KEY = "userKey"
|
private const val USER_KEY = "userKey"
|
||||||
private const val SESSION_DURATION = "sessionDuration"
|
private const val SESSION_DURATION = "sessionDuration"
|
||||||
private const val USER_LIMIT_LOGIN_CATEGORY = "userLimitLoginCategory"
|
private const val USER_LIMIT_LOGIN_CATEGORY = "userLimitLoginCategory"
|
||||||
|
private const val CATEGORY_NETWORK_ID = "categoryNetworkId"
|
||||||
|
|
||||||
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
||||||
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
|
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
|
||||||
|
@ -90,6 +91,7 @@ object DatabaseBackupLowlevel {
|
||||||
handleCollection(USER_KEY) { offset, pageSize -> database.userKey().getUserKeyPageSync(offset, pageSize) }
|
handleCollection(USER_KEY) { offset, pageSize -> database.userKey().getUserKeyPageSync(offset, pageSize) }
|
||||||
handleCollection(SESSION_DURATION) { offset, pageSize -> database.sessionDuration().getSessionDurationPageSync(offset, pageSize) }
|
handleCollection(SESSION_DURATION) { offset, pageSize -> database.sessionDuration().getSessionDurationPageSync(offset, pageSize) }
|
||||||
handleCollection(USER_LIMIT_LOGIN_CATEGORY) { offset, pageSize -> database.userLimitLoginCategoryDao().getAllowedContactPageSync(offset, pageSize) }
|
handleCollection(USER_LIMIT_LOGIN_CATEGORY) { offset, pageSize -> database.userLimitLoginCategoryDao().getAllowedContactPageSync(offset, pageSize) }
|
||||||
|
handleCollection(CATEGORY_NETWORK_ID) { offset, pageSize -> database.categoryNetworkId().getPageSync(offset, pageSize) }
|
||||||
|
|
||||||
writer.endObject().flush()
|
writer.endObject().flush()
|
||||||
}
|
}
|
||||||
|
@ -98,6 +100,7 @@ object DatabaseBackupLowlevel {
|
||||||
val reader = JsonReader(InputStreamReader(inputStream, Charsets.UTF_8))
|
val reader = JsonReader(InputStreamReader(inputStream, Charsets.UTF_8))
|
||||||
|
|
||||||
var userLoginLimitCategories = emptyList<UserLimitLoginCategory>()
|
var userLoginLimitCategories = emptyList<UserLimitLoginCategory>()
|
||||||
|
var categoryNetworkId = emptyList<CategoryNetworkId>()
|
||||||
|
|
||||||
database.runInTransaction {
|
database.runInTransaction {
|
||||||
database.deleteAllData()
|
database.deleteAllData()
|
||||||
|
@ -251,12 +254,26 @@ object DatabaseBackupLowlevel {
|
||||||
|
|
||||||
reader.endArray()
|
reader.endArray()
|
||||||
}
|
}
|
||||||
|
CATEGORY_NETWORK_ID -> {
|
||||||
|
reader.beginArray()
|
||||||
|
|
||||||
|
mutableListOf<CategoryNetworkId>().let { list ->
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
list.add(CategoryNetworkId.parse(reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryNetworkId = list
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endArray()
|
||||||
|
}
|
||||||
else -> reader.skipValue()
|
else -> reader.skipValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.endObject()
|
reader.endObject()
|
||||||
|
|
||||||
database.userLimitLoginCategoryDao().addItemsSync(userLoginLimitCategories)
|
if (userLoginLimitCategories.isNotEmpty()) { database.userLimitLoginCategoryDao().addItemsSync(userLoginLimitCategories) }
|
||||||
|
if (categoryNetworkId.isNotEmpty()) { database.categoryNetworkId().insertItemsSync(categoryNetworkId) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.dao
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Query
|
||||||
|
import io.timelimit.android.data.model.CategoryNetworkId
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface CategoryNetworkIdDao {
|
||||||
|
@Query("SELECT * FROM category_network_id LIMIT :pageSize OFFSET :offset")
|
||||||
|
fun getPageSync(offset: Int, pageSize: Int): List<CategoryNetworkId>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM category_network_id WHERE category_id = :categoryId")
|
||||||
|
fun getByCategoryIdLive(categoryId: String): LiveData<List<CategoryNetworkId>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM category_network_id WHERE category_id = :categoryId")
|
||||||
|
fun getByCategoryIdSync(categoryId: String): List<CategoryNetworkId>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM category_network_id WHERE category_id = :categoryId AND network_item_id = :itemId")
|
||||||
|
fun getByCategoryIdAndItemIdSync(categoryId: String, itemId: String): CategoryNetworkId?
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM category_network_id WHERE category_id = :categoryId")
|
||||||
|
fun countByCategoryIdSync(categoryId: String): Long
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun insertItemsSync(items: List<CategoryNetworkId>)
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun insertItemSync(item: CategoryNetworkId)
|
||||||
|
|
||||||
|
@Query("DELETE FROM category_network_id WHERE category_id = :categoryId")
|
||||||
|
fun deleteByCategoryId(categoryId: String)
|
||||||
|
}
|
|
@ -32,7 +32,8 @@ enum class Table {
|
||||||
UsedTimeItem,
|
UsedTimeItem,
|
||||||
User,
|
User,
|
||||||
UserKey,
|
UserKey,
|
||||||
UserLimitLoginCategory
|
UserLimitLoginCategory,
|
||||||
|
CategoryNetworkId
|
||||||
}
|
}
|
||||||
|
|
||||||
object TableNames {
|
object TableNames {
|
||||||
|
@ -52,6 +53,7 @@ object TableNames {
|
||||||
const val USER = "user"
|
const val USER = "user"
|
||||||
const val USER_KEY = "user_key"
|
const val USER_KEY = "user_key"
|
||||||
const val USER_LIMIT_LOGIN_CATEGORY = "user_limit_login_category"
|
const val USER_LIMIT_LOGIN_CATEGORY = "user_limit_login_category"
|
||||||
|
const val CATEGORY_NETWORK_ID = "category_network_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
object TableUtil {
|
object TableUtil {
|
||||||
|
@ -72,6 +74,7 @@ object TableUtil {
|
||||||
Table.User -> TableNames.USER
|
Table.User -> TableNames.USER
|
||||||
Table.UserKey -> TableNames.USER_KEY
|
Table.UserKey -> TableNames.USER_KEY
|
||||||
Table.UserLimitLoginCategory -> TableNames.USER_LIMIT_LOGIN_CATEGORY
|
Table.UserLimitLoginCategory -> TableNames.USER_LIMIT_LOGIN_CATEGORY
|
||||||
|
Table.CategoryNetworkId -> TableNames.CATEGORY_NETWORK_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toEnum(value: String): Table = when (value) {
|
fun toEnum(value: String): Table = when (value) {
|
||||||
|
@ -91,6 +94,7 @@ object TableUtil {
|
||||||
TableNames.USER -> Table.User
|
TableNames.USER -> Table.User
|
||||||
TableNames.USER_KEY -> Table.UserKey
|
TableNames.USER_KEY -> Table.UserKey
|
||||||
TableNames.USER_LIMIT_LOGIN_CATEGORY -> Table.UserLimitLoginCategory
|
TableNames.USER_LIMIT_LOGIN_CATEGORY -> Table.UserLimitLoginCategory
|
||||||
|
TableNames.CATEGORY_NETWORK_ID -> Table.CategoryNetworkId
|
||||||
else -> throw IllegalArgumentException()
|
else -> throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation version 3 of the License.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package io.timelimit.android.data.model
|
||||||
|
|
||||||
|
import android.util.JsonReader
|
||||||
|
import android.util.JsonWriter
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import io.timelimit.android.crypto.HexString
|
||||||
|
import io.timelimit.android.crypto.Sha512
|
||||||
|
import io.timelimit.android.data.IdGenerator
|
||||||
|
import io.timelimit.android.data.JsonSerializable
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "category_network_id",
|
||||||
|
primaryKeys = ["category_id", "network_item_id"],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = Category::class,
|
||||||
|
parentColumns = ["id"],
|
||||||
|
childColumns = ["category_id"],
|
||||||
|
onUpdate = ForeignKey.CASCADE,
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class CategoryNetworkId(
|
||||||
|
@ColumnInfo(name = "category_id")
|
||||||
|
val categoryId: String,
|
||||||
|
@ColumnInfo(name = "network_item_id")
|
||||||
|
val networkItemId: String,
|
||||||
|
@ColumnInfo(name = "hashed_network_id")
|
||||||
|
val hashedNetworkId: String
|
||||||
|
): JsonSerializable {
|
||||||
|
companion object {
|
||||||
|
private const val CATEGORY_ID = "categoryId"
|
||||||
|
private const val NETWORK_ITEM_ID = "networkItemId"
|
||||||
|
private const val HASHED_NETWORK_ID = "hashedNetworkId"
|
||||||
|
const val ANONYMIZED_NETWORK_ID_LENGTH = 8
|
||||||
|
const val MAX_ITEMS = 8
|
||||||
|
|
||||||
|
fun anonymizeNetworkId(itemId: String, networkId: String) = Sha512.hashSync(itemId + networkId).substring(0, ANONYMIZED_NETWORK_ID_LENGTH)
|
||||||
|
|
||||||
|
fun parse(reader: JsonReader): CategoryNetworkId {
|
||||||
|
var categoryId: String? = null
|
||||||
|
var networkItemId: String? = null
|
||||||
|
var hashedNetworkId: String? = null
|
||||||
|
|
||||||
|
reader.beginObject()
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
when (reader.nextName()) {
|
||||||
|
CATEGORY_ID -> categoryId = reader.nextString()
|
||||||
|
NETWORK_ITEM_ID -> networkItemId = reader.nextString()
|
||||||
|
HASHED_NETWORK_ID -> hashedNetworkId = reader.nextString()
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
return CategoryNetworkId(
|
||||||
|
categoryId = categoryId!!,
|
||||||
|
networkItemId = networkItemId!!,
|
||||||
|
hashedNetworkId = hashedNetworkId!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
IdGenerator.assertIdValid(networkItemId)
|
||||||
|
if (hashedNetworkId.length != ANONYMIZED_NETWORK_ID_LENGTH) throw IllegalArgumentException()
|
||||||
|
HexString.assertIsHexString(hashedNetworkId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(CATEGORY_ID).value(categoryId)
|
||||||
|
writer.name(NETWORK_ITEM_ID).value(networkItemId)
|
||||||
|
writer.name(HASHED_NETWORK_ID).value(hashedNetworkId)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,28 +17,28 @@
|
||||||
package io.timelimit.android.data.model.derived
|
package io.timelimit.android.data.model.derived
|
||||||
|
|
||||||
import io.timelimit.android.data.Database
|
import io.timelimit.android.data.Database
|
||||||
import io.timelimit.android.data.model.Category
|
import io.timelimit.android.data.model.*
|
||||||
import io.timelimit.android.data.model.SessionDuration
|
|
||||||
import io.timelimit.android.data.model.TimeLimitRule
|
|
||||||
import io.timelimit.android.data.model.UsedTimeItem
|
|
||||||
|
|
||||||
data class CategoryRelatedData(
|
data class CategoryRelatedData(
|
||||||
val category: Category,
|
val category: Category,
|
||||||
val rules: List<TimeLimitRule>,
|
val rules: List<TimeLimitRule>,
|
||||||
val usedTimes: List<UsedTimeItem>,
|
val usedTimes: List<UsedTimeItem>,
|
||||||
val durations: List<SessionDuration>
|
val durations: List<SessionDuration>,
|
||||||
|
val networks: List<CategoryNetworkId>
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun load(category: Category, database: Database): CategoryRelatedData = database.runInUnobservedTransaction {
|
fun load(category: Category, database: Database): CategoryRelatedData = database.runInUnobservedTransaction {
|
||||||
val rules = database.timeLimitRules().getTimeLimitRulesByCategorySync(category.id)
|
val rules = database.timeLimitRules().getTimeLimitRulesByCategorySync(category.id)
|
||||||
val usedTimes = database.usedTimes().getUsedTimeItemsByCategoryId(category.id)
|
val usedTimes = database.usedTimes().getUsedTimeItemsByCategoryId(category.id)
|
||||||
val durations = database.sessionDuration().getSessionDurationItemsByCategoryIdSync(category.id)
|
val durations = database.sessionDuration().getSessionDurationItemsByCategoryIdSync(category.id)
|
||||||
|
val networks = database.categoryNetworkId().getByCategoryIdSync(category.id)
|
||||||
|
|
||||||
CategoryRelatedData(
|
CategoryRelatedData(
|
||||||
category = category,
|
category = category,
|
||||||
rules = rules,
|
rules = rules,
|
||||||
usedTimes = usedTimes,
|
usedTimes = usedTimes,
|
||||||
durations = durations
|
durations = durations,
|
||||||
|
networks = networks
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ data class CategoryRelatedData(
|
||||||
updateRules: Boolean,
|
updateRules: Boolean,
|
||||||
updateTimes: Boolean,
|
updateTimes: Boolean,
|
||||||
updateDurations: Boolean,
|
updateDurations: Boolean,
|
||||||
|
updateNetworks: Boolean,
|
||||||
database: Database
|
database: Database
|
||||||
): CategoryRelatedData = database.runInUnobservedTransaction {
|
): CategoryRelatedData = database.runInUnobservedTransaction {
|
||||||
if (category.id != this.category.id) {
|
if (category.id != this.category.id) {
|
||||||
|
@ -57,15 +58,17 @@ data class CategoryRelatedData(
|
||||||
val rules = if (updateRules) database.timeLimitRules().getTimeLimitRulesByCategorySync(category.id) else rules
|
val rules = if (updateRules) database.timeLimitRules().getTimeLimitRulesByCategorySync(category.id) else rules
|
||||||
val usedTimes = if (updateTimes) database.usedTimes().getUsedTimeItemsByCategoryId(category.id) else usedTimes
|
val usedTimes = if (updateTimes) database.usedTimes().getUsedTimeItemsByCategoryId(category.id) else usedTimes
|
||||||
val durations = if (updateDurations) database.sessionDuration().getSessionDurationItemsByCategoryIdSync(category.id) else durations
|
val durations = if (updateDurations) database.sessionDuration().getSessionDurationItemsByCategoryIdSync(category.id) else durations
|
||||||
|
val networks = if (updateNetworks) database.categoryNetworkId().getByCategoryIdSync(category.id) else networks
|
||||||
|
|
||||||
if (category == this.category && rules == this.rules && usedTimes == this.usedTimes && durations == this.durations) {
|
if (category == this.category && rules == this.rules && usedTimes == this.usedTimes && durations == this.durations && networks == this.networks) {
|
||||||
this
|
this
|
||||||
} else {
|
} else {
|
||||||
CategoryRelatedData(
|
CategoryRelatedData(
|
||||||
category = category,
|
category = category,
|
||||||
rules = rules,
|
rules = rules,
|
||||||
usedTimes = usedTimes,
|
usedTimes = usedTimes,
|
||||||
durations = durations
|
durations = durations,
|
||||||
|
networks = networks
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,8 @@ data class UserRelatedData(
|
||||||
|
|
||||||
private val relatedTables = arrayOf(
|
private val relatedTables = arrayOf(
|
||||||
Table.User, Table.Category, Table.TimeLimitRule,
|
Table.User, Table.Category, Table.TimeLimitRule,
|
||||||
Table.UsedTimeItem, Table.SessionDuration, Table.CategoryApp
|
Table.UsedTimeItem, Table.SessionDuration, Table.CategoryApp,
|
||||||
|
Table.CategoryNetworkId
|
||||||
)
|
)
|
||||||
|
|
||||||
fun load(user: User, database: Database): UserRelatedData = database.runInUnobservedTransaction {
|
fun load(user: User, database: Database): UserRelatedData = database.runInUnobservedTransaction {
|
||||||
|
@ -82,9 +83,10 @@ data class UserRelatedData(
|
||||||
private var usedTimesInvalidated = false
|
private var usedTimesInvalidated = false
|
||||||
private var sessionDurationsInvalidated = false
|
private var sessionDurationsInvalidated = false
|
||||||
private var categoryAppsInvalidated = false
|
private var categoryAppsInvalidated = false
|
||||||
|
private var categoryNetworksInvalidated = false
|
||||||
|
|
||||||
private val invalidated
|
private val invalidated
|
||||||
get() = userInvalidated || categoriesInvalidated || rulesInvalidated || usedTimesInvalidated || sessionDurationsInvalidated || categoryAppsInvalidated
|
get() = userInvalidated || categoriesInvalidated || rulesInvalidated || usedTimesInvalidated || sessionDurationsInvalidated || categoryAppsInvalidated || categoryNetworksInvalidated
|
||||||
|
|
||||||
override fun onInvalidated(tables: Set<Table>) {
|
override fun onInvalidated(tables: Set<Table>) {
|
||||||
tables.forEach {
|
tables.forEach {
|
||||||
|
@ -95,6 +97,7 @@ data class UserRelatedData(
|
||||||
Table.UsedTimeItem -> usedTimesInvalidated = true
|
Table.UsedTimeItem -> usedTimesInvalidated = true
|
||||||
Table.SessionDuration -> sessionDurationsInvalidated = true
|
Table.SessionDuration -> sessionDurationsInvalidated = true
|
||||||
Table.CategoryApp -> categoryAppsInvalidated = true
|
Table.CategoryApp -> categoryAppsInvalidated = true
|
||||||
|
Table.CategoryNetworkId -> categoryNetworksInvalidated = true
|
||||||
else -> {/* do nothing */}
|
else -> {/* do nothing */}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,20 +120,22 @@ data class UserRelatedData(
|
||||||
database = database,
|
database = database,
|
||||||
updateDurations = sessionDurationsInvalidated,
|
updateDurations = sessionDurationsInvalidated,
|
||||||
updateRules = rulesInvalidated,
|
updateRules = rulesInvalidated,
|
||||||
updateTimes = usedTimesInvalidated
|
updateTimes = usedTimesInvalidated,
|
||||||
|
updateNetworks = categoryNetworksInvalidated
|
||||||
) ?: CategoryRelatedData.load(
|
) ?: CategoryRelatedData.load(
|
||||||
category = category,
|
category = category,
|
||||||
database = database
|
database = database
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (sessionDurationsInvalidated || rulesInvalidated || usedTimesInvalidated) {
|
} else if (sessionDurationsInvalidated || rulesInvalidated || usedTimesInvalidated || categoryNetworksInvalidated) {
|
||||||
categories.map {
|
categories.map {
|
||||||
it.update(
|
it.update(
|
||||||
category = it.category,
|
category = it.category,
|
||||||
database = database,
|
database = database,
|
||||||
updateDurations = sessionDurationsInvalidated,
|
updateDurations = sessionDurationsInvalidated,
|
||||||
updateRules = rulesInvalidated,
|
updateRules = rulesInvalidated,
|
||||||
updateTimes = usedTimesInvalidated
|
updateTimes = usedTimesInvalidated,
|
||||||
|
updateNetworks = categoryNetworksInvalidated
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -74,6 +74,8 @@ abstract class PlatformIntegration(
|
||||||
|
|
||||||
abstract fun restartApp()
|
abstract fun restartApp()
|
||||||
|
|
||||||
|
abstract fun getCurrentNetworkId(): NetworkId
|
||||||
|
|
||||||
var installedAppsChangeListener: Runnable? = null
|
var installedAppsChangeListener: Runnable? = null
|
||||||
var systemClockChangeListener: Runnable? = null
|
var systemClockChangeListener: Runnable? = null
|
||||||
}
|
}
|
||||||
|
@ -216,4 +218,12 @@ data class BatteryStatus(
|
||||||
throw IllegalArgumentException()
|
throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class NetworkId {
|
||||||
|
object NoNetworkConnected : NetworkId()
|
||||||
|
object MissingPermission: NetworkId()
|
||||||
|
data class Network(val id: String): NetworkId()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NetworkId.getNetworkIdOrNull(): String? = if (this is NetworkId.Network) this.id else null
|
|
@ -83,6 +83,7 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
||||||
private val deviceAdmin = ComponentName(context.applicationContext, AdminReceiver::class.java)
|
private val deviceAdmin = ComponentName(context.applicationContext, AdminReceiver::class.java)
|
||||||
private val overlay = OverlayUtil(context as Application)
|
private val overlay = OverlayUtil(context as Application)
|
||||||
private val battery = BatteryStatusUtil(context)
|
private val battery = BatteryStatusUtil(context)
|
||||||
|
private val connectedNetwork = ConnectedNetworkUtil(context)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
AppsChangeListener.registerBroadcastReceiver(this.context, object : BroadcastReceiver() {
|
AppsChangeListener.registerBroadcastReceiver(this.context, object : BroadcastReceiver() {
|
||||||
|
@ -520,4 +521,6 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getCurrentNetworkId(): NetworkId = connectedNetwork.getNetworkId()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2020 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.integration.platform.android
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.wifi.WifiInfo
|
||||||
|
import android.net.wifi.WifiManager
|
||||||
|
import io.timelimit.android.crypto.Sha512
|
||||||
|
import io.timelimit.android.integration.platform.NetworkId
|
||||||
|
|
||||||
|
class ConnectedNetworkUtil (context: Context) {
|
||||||
|
private val workContext = context.applicationContext
|
||||||
|
private val wifiManager = workContext.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||||
|
|
||||||
|
fun getNetworkId(): NetworkId {
|
||||||
|
val info: WifiInfo? = wifiManager.connectionInfo
|
||||||
|
|
||||||
|
info ?: return NetworkId.NoNetworkConnected
|
||||||
|
|
||||||
|
val ssid: String? = info.ssid
|
||||||
|
val bssid: String? = info.bssid
|
||||||
|
|
||||||
|
if (ssid == null || bssid == null) return NetworkId.NoNetworkConnected
|
||||||
|
if (ssid == WifiManager.UNKNOWN_SSID) return NetworkId.MissingPermission
|
||||||
|
|
||||||
|
return NetworkId.Network(Sha512.hashSync(ssid + bssid).substring(0, 16))
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,8 @@ import io.timelimit.android.async.Threads
|
||||||
import io.timelimit.android.coroutines.executeAndWait
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
import io.timelimit.android.coroutines.runAsync
|
import io.timelimit.android.coroutines.runAsync
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
|
import io.timelimit.android.integration.platform.getNetworkIdOrNull
|
||||||
|
import io.timelimit.android.livedata.waitForNonNullValue
|
||||||
import io.timelimit.android.logic.*
|
import io.timelimit.android.logic.*
|
||||||
import io.timelimit.android.logic.blockingreason.AppBaseHandling
|
import io.timelimit.android.logic.blockingreason.AppBaseHandling
|
||||||
import io.timelimit.android.logic.blockingreason.CategoryItselfHandling
|
import io.timelimit.android.logic.blockingreason.CategoryItselfHandling
|
||||||
|
@ -117,6 +119,7 @@ class NotificationListener: NotificationListenerService() {
|
||||||
BlockingReason.NotificationsAreBlocked -> getString(R.string.lock_reason_short_notification_blocking)
|
BlockingReason.NotificationsAreBlocked -> getString(R.string.lock_reason_short_notification_blocking)
|
||||||
BlockingReason.BatteryLimit -> getString(R.string.lock_reason_short_battery_limit)
|
BlockingReason.BatteryLimit -> getString(R.string.lock_reason_short_battery_limit)
|
||||||
BlockingReason.SessionDurationLimit -> getString(R.string.lock_reason_short_session_duration)
|
BlockingReason.SessionDurationLimit -> getString(R.string.lock_reason_short_session_duration)
|
||||||
|
BlockingReason.MissingRequiredNetwork -> getString(R.string.lock_reason_short_missing_required_network)
|
||||||
BlockingReason.None -> throw IllegalStateException()
|
BlockingReason.None -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -165,12 +168,16 @@ class NotificationListener: NotificationListenerService() {
|
||||||
val time = RealTime.newInstance()
|
val time = RealTime.newInstance()
|
||||||
val battery = appLogic.platformIntegration.getBatteryStatus()
|
val battery = appLogic.platformIntegration.getBatteryStatus()
|
||||||
val allowNotificationFilter = deviceAndUserRelatedData.deviceRelatedData.isConnectedAndHasPremium || deviceAndUserRelatedData.deviceRelatedData.isLocalMode
|
val allowNotificationFilter = deviceAndUserRelatedData.deviceRelatedData.isConnectedAndHasPremium || deviceAndUserRelatedData.deviceRelatedData.isLocalMode
|
||||||
|
val networkId = if (appHandling.needsNetworkId) appLogic.platformIntegration.getCurrentNetworkId().getNetworkIdOrNull() else null
|
||||||
|
val hasPremiumOrLocalMode = appLogic.fullVersion.shouldProvideFullVersionFunctions.waitForNonNullValue()
|
||||||
|
|
||||||
appLogic.realTimeLogic.getRealTime(time)
|
appLogic.realTimeLogic.getRealTime(time)
|
||||||
|
|
||||||
val categoryHandlings = appHandling.categoryIds.map { categoryId ->
|
val categoryHandlings = appHandling.categoryIds.map { categoryId ->
|
||||||
|
val categoryRelatedData = deviceAndUserRelatedData.userRelatedData.categoryById[categoryId]!!
|
||||||
|
|
||||||
CategoryItselfHandling.calculate(
|
CategoryItselfHandling.calculate(
|
||||||
categoryRelatedData = deviceAndUserRelatedData.userRelatedData.categoryById[categoryId]!!,
|
categoryRelatedData = categoryRelatedData,
|
||||||
user = deviceAndUserRelatedData.userRelatedData,
|
user = deviceAndUserRelatedData.userRelatedData,
|
||||||
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(
|
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(
|
||||||
device = deviceAndUserRelatedData.deviceRelatedData,
|
device = deviceAndUserRelatedData.deviceRelatedData,
|
||||||
|
@ -178,7 +185,9 @@ class NotificationListener: NotificationListenerService() {
|
||||||
),
|
),
|
||||||
batteryStatus = battery,
|
batteryStatus = battery,
|
||||||
shouldTrustTimeTemporarily = time.shouldTrustTimeTemporarily,
|
shouldTrustTimeTemporarily = time.shouldTrustTimeTemporarily,
|
||||||
timeInMillis = time.timeInMillis
|
timeInMillis = time.timeInMillis,
|
||||||
|
currentNetworkId = networkId,
|
||||||
|
hasPremiumOrLocalMode = hasPremiumOrLocalMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -175,4 +175,6 @@ class DummyIntegration(
|
||||||
override fun setForceNetworkTime(enable: Boolean) = Unit
|
override fun setForceNetworkTime(enable: Boolean) = Unit
|
||||||
|
|
||||||
override fun restartApp() = Unit
|
override fun restartApp() = Unit
|
||||||
|
|
||||||
|
override fun getCurrentNetworkId(): NetworkId = NetworkId.NoNetworkConnected
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,9 +31,11 @@ import io.timelimit.android.date.DateInTimezone
|
||||||
import io.timelimit.android.integration.platform.AppStatusMessage
|
import io.timelimit.android.integration.platform.AppStatusMessage
|
||||||
import io.timelimit.android.integration.platform.ProtectionLevel
|
import io.timelimit.android.integration.platform.ProtectionLevel
|
||||||
import io.timelimit.android.integration.platform.android.AccessibilityService
|
import io.timelimit.android.integration.platform.android.AccessibilityService
|
||||||
|
import io.timelimit.android.integration.platform.getNetworkIdOrNull
|
||||||
import io.timelimit.android.livedata.*
|
import io.timelimit.android.livedata.*
|
||||||
import io.timelimit.android.logic.blockingreason.AppBaseHandling
|
import io.timelimit.android.logic.blockingreason.AppBaseHandling
|
||||||
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
|
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
|
||||||
|
import io.timelimit.android.logic.blockingreason.needsNetworkId
|
||||||
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
|
import io.timelimit.android.sync.actions.UpdateDeviceStatusAction
|
||||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
import io.timelimit.android.ui.IsAppInForeground
|
import io.timelimit.android.ui.IsAppInForeground
|
||||||
|
@ -266,16 +268,6 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reportStatusToCategoryHandlingCache(userRelatedData: UserRelatedData) {
|
|
||||||
categoryHandlingCache.reportStatus(
|
|
||||||
user = userRelatedData,
|
|
||||||
timeInMillis = nowTimestamp,
|
|
||||||
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
|
|
||||||
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(deviceRelatedData, userRelatedData),
|
|
||||||
batteryStatus = batteryStatus
|
|
||||||
)
|
|
||||||
}; reportStatusToCategoryHandlingCache(userRelatedData)
|
|
||||||
|
|
||||||
val foregroundApps = appLogic.platformIntegration.getForegroundApps(
|
val foregroundApps = appLogic.platformIntegration.getForegroundApps(
|
||||||
appLogic.getForegroundAppQueryInterval(),
|
appLogic.getForegroundAppQueryInterval(),
|
||||||
appLogic.getEnableMultiAppDetection()
|
appLogic.getEnableMultiAppDetection()
|
||||||
|
@ -304,6 +296,21 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
||||||
pauseCounting = false
|
pauseCounting = false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val needsNetworkId = foregroundAppWithBaseHandlings.find { it.second.needsNetworkId() } != null || backgroundAppBaseHandling.needsNetworkId()
|
||||||
|
val networkId: String? = if (needsNetworkId) appLogic.platformIntegration.getCurrentNetworkId().getNetworkIdOrNull() else null
|
||||||
|
|
||||||
|
fun reportStatusToCategoryHandlingCache(userRelatedData: UserRelatedData) {
|
||||||
|
categoryHandlingCache.reportStatus(
|
||||||
|
user = userRelatedData,
|
||||||
|
timeInMillis = nowTimestamp,
|
||||||
|
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
|
||||||
|
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(deviceRelatedData, userRelatedData),
|
||||||
|
batteryStatus = batteryStatus,
|
||||||
|
currentNetworkId = networkId,
|
||||||
|
hasPremiumOrLocalMode = deviceRelatedData.isLocalMode || deviceRelatedData.isConnectedAndHasPremium
|
||||||
|
)
|
||||||
|
}; reportStatusToCategoryHandlingCache(userRelatedData)
|
||||||
|
|
||||||
// check if should be blocked
|
// check if should be blocked
|
||||||
val blockedForegroundApp = foregroundAppWithBaseHandlings.find { (_, foregroundAppBaseHandling) ->
|
val blockedForegroundApp = foregroundAppWithBaseHandlings.find { (_, foregroundAppBaseHandling) ->
|
||||||
foregroundAppBaseHandling is AppBaseHandling.BlockDueToNoCategory ||
|
foregroundAppBaseHandling is AppBaseHandling.BlockDueToNoCategory ||
|
||||||
|
|
|
@ -32,7 +32,8 @@ enum class BlockingReason {
|
||||||
RequiresCurrentDevice,
|
RequiresCurrentDevice,
|
||||||
NotificationsAreBlocked,
|
NotificationsAreBlocked,
|
||||||
BatteryLimit,
|
BatteryLimit,
|
||||||
SessionDurationLimit
|
SessionDurationLimit,
|
||||||
|
MissingRequiredNetwork
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class BlockingLevel {
|
enum class BlockingLevel {
|
||||||
|
|
|
@ -112,7 +112,9 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
|
||||||
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(
|
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(
|
||||||
device = userAndDeviceRelatedData.deviceRelatedData,
|
device = userAndDeviceRelatedData.deviceRelatedData,
|
||||||
user = userRelatedData
|
user = userRelatedData
|
||||||
)
|
),
|
||||||
|
currentNetworkId = null, // not relevant/ not suspending Apps if there is no matching network
|
||||||
|
hasPremiumOrLocalMode = userAndDeviceRelatedData.deviceRelatedData.isLocalMode || userAndDeviceRelatedData.deviceRelatedData.isConnectedAndHasPremium
|
||||||
)
|
)
|
||||||
|
|
||||||
val defaultCategory = userRelatedData.user.categoryForNotAssignedApps
|
val defaultCategory = userRelatedData.user.categoryForNotAssignedApps
|
||||||
|
|
|
@ -32,7 +32,8 @@ sealed class AppBaseHandling {
|
||||||
data class UseCategories(
|
data class UseCategories(
|
||||||
val categoryIds: Set<String>,
|
val categoryIds: Set<String>,
|
||||||
val shouldCount: Boolean,
|
val shouldCount: Boolean,
|
||||||
val level: BlockingLevel
|
val level: BlockingLevel,
|
||||||
|
val needsNetworkId: Boolean
|
||||||
): AppBaseHandling() {
|
): AppBaseHandling() {
|
||||||
init {
|
init {
|
||||||
if (categoryIds.isEmpty()) {
|
if (categoryIds.isEmpty()) {
|
||||||
|
@ -85,14 +86,19 @@ sealed class AppBaseHandling {
|
||||||
if (startCategory == null) {
|
if (startCategory == null) {
|
||||||
return BlockDueToNoCategory
|
return BlockDueToNoCategory
|
||||||
} else {
|
} else {
|
||||||
|
val categoryIds = userRelatedData.getCategoryWithParentCategories(startCategoryId = startCategory.category.id)
|
||||||
|
|
||||||
return UseCategories(
|
return UseCategories(
|
||||||
categoryIds = userRelatedData.getCategoryWithParentCategories(startCategoryId = startCategory.category.id),
|
categoryIds = categoryIds,
|
||||||
shouldCount = !pauseCounting,
|
shouldCount = !pauseCounting,
|
||||||
level = when (appCategory?.specifiesActivity) {
|
level = when (appCategory?.specifiesActivity) {
|
||||||
null -> BlockingLevel.Activity // occurs when using a default category
|
null -> BlockingLevel.Activity // occurs when using a default category
|
||||||
true -> BlockingLevel.Activity
|
true -> BlockingLevel.Activity
|
||||||
false -> BlockingLevel.App
|
false -> BlockingLevel.App
|
||||||
}
|
},
|
||||||
|
needsNetworkId = categoryIds.find { categoryId ->
|
||||||
|
userRelatedData.categoryById[categoryId]!!.networks.isNotEmpty()
|
||||||
|
} != null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -112,4 +118,6 @@ sealed class AppBaseHandling {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun AppBaseHandling.needsNetworkId(): Boolean = if (this is AppBaseHandling.UseCategories) this.needsNetworkId else false
|
|
@ -27,19 +27,25 @@ class CategoryHandlingCache {
|
||||||
private var shouldTrustTimeTemporarily: Boolean = false
|
private var shouldTrustTimeTemporarily: Boolean = false
|
||||||
private var timeInMillis: Long = 0
|
private var timeInMillis: Long = 0
|
||||||
private var assumeCurrentDevice: Boolean = false
|
private var assumeCurrentDevice: Boolean = false
|
||||||
|
private var currentNetworkId: String? = null
|
||||||
|
private var hasPremiumOrLocalMode: Boolean = false
|
||||||
|
|
||||||
fun reportStatus(
|
fun reportStatus(
|
||||||
user: UserRelatedData,
|
user: UserRelatedData,
|
||||||
batteryStatus: BatteryStatus,
|
batteryStatus: BatteryStatus,
|
||||||
shouldTrustTimeTemporarily: Boolean,
|
shouldTrustTimeTemporarily: Boolean,
|
||||||
timeInMillis: Long,
|
timeInMillis: Long,
|
||||||
assumeCurrentDevice: Boolean
|
assumeCurrentDevice: Boolean,
|
||||||
|
currentNetworkId: String?,
|
||||||
|
hasPremiumOrLocalMode: Boolean
|
||||||
) {
|
) {
|
||||||
this.user = user
|
this.user = user
|
||||||
this.batteryStatus = batteryStatus
|
this.batteryStatus = batteryStatus
|
||||||
this.shouldTrustTimeTemporarily = shouldTrustTimeTemporarily
|
this.shouldTrustTimeTemporarily = shouldTrustTimeTemporarily
|
||||||
this.timeInMillis = timeInMillis
|
this.timeInMillis = timeInMillis
|
||||||
this.assumeCurrentDevice = assumeCurrentDevice
|
this.assumeCurrentDevice = assumeCurrentDevice
|
||||||
|
this.currentNetworkId = currentNetworkId
|
||||||
|
this.hasPremiumOrLocalMode = hasPremiumOrLocalMode
|
||||||
|
|
||||||
val iterator = cachedItems.iterator()
|
val iterator = cachedItems.iterator()
|
||||||
|
|
||||||
|
@ -54,7 +60,9 @@ class CategoryHandlingCache {
|
||||||
batteryStatus = batteryStatus,
|
batteryStatus = batteryStatus,
|
||||||
assumeCurrentDevice = assumeCurrentDevice,
|
assumeCurrentDevice = assumeCurrentDevice,
|
||||||
shouldTrustTimeTemporarily = shouldTrustTimeTemporarily,
|
shouldTrustTimeTemporarily = shouldTrustTimeTemporarily,
|
||||||
timeInMillis = timeInMillis
|
timeInMillis = timeInMillis,
|
||||||
|
currentNetworkId = currentNetworkId,
|
||||||
|
hasPremiumOrLocalMode = hasPremiumOrLocalMode
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
iterator.remove()
|
iterator.remove()
|
||||||
|
@ -76,6 +84,8 @@ class CategoryHandlingCache {
|
||||||
batteryStatus = batteryStatus,
|
batteryStatus = batteryStatus,
|
||||||
assumeCurrentDevice = assumeCurrentDevice,
|
assumeCurrentDevice = assumeCurrentDevice,
|
||||||
shouldTrustTimeTemporarily = shouldTrustTimeTemporarily,
|
shouldTrustTimeTemporarily = shouldTrustTimeTemporarily,
|
||||||
timeInMillis = timeInMillis
|
timeInMillis = timeInMillis,
|
||||||
|
currentNetworkId = currentNetworkId,
|
||||||
|
hasPremiumOrLocalMode = hasPremiumOrLocalMode
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.logic.blockingreason
|
package io.timelimit.android.logic.blockingreason
|
||||||
|
|
||||||
|
import io.timelimit.android.data.model.CategoryNetworkId
|
||||||
import io.timelimit.android.data.model.derived.CategoryRelatedData
|
import io.timelimit.android.data.model.derived.CategoryRelatedData
|
||||||
import io.timelimit.android.data.model.derived.UserRelatedData
|
import io.timelimit.android.data.model.derived.UserRelatedData
|
||||||
import io.timelimit.android.date.DateInTimezone
|
import io.timelimit.android.date.DateInTimezone
|
||||||
|
@ -37,6 +38,7 @@ data class CategoryItselfHandling (
|
||||||
val areLimitsTemporarilyDisabled: Boolean,
|
val areLimitsTemporarilyDisabled: Boolean,
|
||||||
val okByBattery: Boolean,
|
val okByBattery: Boolean,
|
||||||
val okByTempBlocking: Boolean,
|
val okByTempBlocking: Boolean,
|
||||||
|
val okByNetworkId: Boolean,
|
||||||
val okByBlockedTimeAreas: Boolean,
|
val okByBlockedTimeAreas: Boolean,
|
||||||
val okByTimeLimitRules: Boolean,
|
val okByTimeLimitRules: Boolean,
|
||||||
val okBySessionDurationLimits: Boolean,
|
val okBySessionDurationLimits: Boolean,
|
||||||
|
@ -50,11 +52,14 @@ data class CategoryItselfHandling (
|
||||||
val dependsOnBatteryCharging: Boolean,
|
val dependsOnBatteryCharging: Boolean,
|
||||||
val dependsOnMinBatteryLevel: Int,
|
val dependsOnMinBatteryLevel: Int,
|
||||||
val dependsOnMaxBatteryLevel: Int,
|
val dependsOnMaxBatteryLevel: Int,
|
||||||
|
val dependsOnNetworkId: Boolean,
|
||||||
val createdWithCategoryRelatedData: CategoryRelatedData,
|
val createdWithCategoryRelatedData: CategoryRelatedData,
|
||||||
val createdWithUserRelatedData: UserRelatedData,
|
val createdWithUserRelatedData: UserRelatedData,
|
||||||
val createdWithBatteryStatus: BatteryStatus,
|
val createdWithBatteryStatus: BatteryStatus,
|
||||||
val createdWithTemporarilyTrustTime: Boolean,
|
val createdWithTemporarilyTrustTime: Boolean,
|
||||||
val createdWithAssumeCurrentDevice: Boolean
|
val createdWithAssumeCurrentDevice: Boolean,
|
||||||
|
val createdWithNetworkId: String?,
|
||||||
|
val createdWithHasPremiumOrLocalMode: Boolean
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
fun calculate(
|
fun calculate(
|
||||||
|
@ -63,7 +68,9 @@ data class CategoryItselfHandling (
|
||||||
batteryStatus: BatteryStatus,
|
batteryStatus: BatteryStatus,
|
||||||
shouldTrustTimeTemporarily: Boolean,
|
shouldTrustTimeTemporarily: Boolean,
|
||||||
timeInMillis: Long,
|
timeInMillis: Long,
|
||||||
assumeCurrentDevice: Boolean
|
assumeCurrentDevice: Boolean,
|
||||||
|
currentNetworkId: String?,
|
||||||
|
hasPremiumOrLocalMode: Boolean
|
||||||
): CategoryItselfHandling {
|
): CategoryItselfHandling {
|
||||||
val dependsOnMinTime = timeInMillis
|
val dependsOnMinTime = timeInMillis
|
||||||
val dateInTimezone = DateInTimezone.newInstance(timeInMillis, user.timeZone)
|
val dateInTimezone = DateInTimezone.newInstance(timeInMillis, user.timeZone)
|
||||||
|
@ -88,6 +95,14 @@ data class CategoryItselfHandling (
|
||||||
val dependsOnMaxTimeByTemporarilyDisabledLimits = if (areLimitsTemporarilyDisabled) user.user.disableLimitsUntil else Long.MAX_VALUE
|
val dependsOnMaxTimeByTemporarilyDisabledLimits = if (areLimitsTemporarilyDisabled) user.user.disableLimitsUntil else Long.MAX_VALUE
|
||||||
// ignore it for this case: val requiresTrustedTimeForTempLimitsDisabled = user.user.disableLimitsUntil != 0L
|
// ignore it for this case: val requiresTrustedTimeForTempLimitsDisabled = user.user.disableLimitsUntil != 0L
|
||||||
|
|
||||||
|
val dependsOnNetworkId = categoryRelatedData.networks.isNotEmpty()
|
||||||
|
val okByNetworkId = if (categoryRelatedData.networks.isEmpty() || areLimitsTemporarilyDisabled || !hasPremiumOrLocalMode)
|
||||||
|
true
|
||||||
|
else if (currentNetworkId == null)
|
||||||
|
false
|
||||||
|
else
|
||||||
|
categoryRelatedData.networks.find { CategoryNetworkId.anonymizeNetworkId(itemId = it.networkItemId, networkId = currentNetworkId) == it.hashedNetworkId } != null
|
||||||
|
|
||||||
val missingNetworkTimeForBlockedTimeAreas = !categoryRelatedData.category.blockedMinutesInWeek.dataNotToModify.isEmpty
|
val missingNetworkTimeForBlockedTimeAreas = !categoryRelatedData.category.blockedMinutesInWeek.dataNotToModify.isEmpty
|
||||||
val okByBlockedTimeAreas = areLimitsTemporarilyDisabled || !categoryRelatedData.category.blockedMinutesInWeek.read(minuteInWeek)
|
val okByBlockedTimeAreas = areLimitsTemporarilyDisabled || !categoryRelatedData.category.blockedMinutesInWeek.read(minuteInWeek)
|
||||||
val dependsOnMaxMinuteOfWeekByBlockedTimeAreas = categoryRelatedData.category.blockedMinutesInWeek.let { blockedTimeAreas ->
|
val dependsOnMaxMinuteOfWeekByBlockedTimeAreas = categoryRelatedData.category.blockedMinutesInWeek.let { blockedTimeAreas ->
|
||||||
|
@ -195,7 +210,7 @@ data class CategoryItselfHandling (
|
||||||
else
|
else
|
||||||
emptySet()
|
emptySet()
|
||||||
|
|
||||||
val blockAllNotifications = categoryRelatedData.category.blockAllNotifications
|
val blockAllNotifications = categoryRelatedData.category.blockAllNotifications &&hasPremiumOrLocalMode
|
||||||
|
|
||||||
return CategoryItselfHandling(
|
return CategoryItselfHandling(
|
||||||
shouldCountTime = shouldCountTime,
|
shouldCountTime = shouldCountTime,
|
||||||
|
@ -205,6 +220,7 @@ data class CategoryItselfHandling (
|
||||||
areLimitsTemporarilyDisabled = areLimitsTemporarilyDisabled,
|
areLimitsTemporarilyDisabled = areLimitsTemporarilyDisabled,
|
||||||
okByBattery = okByBattery,
|
okByBattery = okByBattery,
|
||||||
okByTempBlocking = okByTempBlocking,
|
okByTempBlocking = okByTempBlocking,
|
||||||
|
okByNetworkId = okByNetworkId,
|
||||||
okByBlockedTimeAreas = okByBlockedTimeAreas,
|
okByBlockedTimeAreas = okByBlockedTimeAreas,
|
||||||
okByTimeLimitRules = okByTimeLimitRules,
|
okByTimeLimitRules = okByTimeLimitRules,
|
||||||
okBySessionDurationLimits = okBySessionDurationLimits,
|
okBySessionDurationLimits = okBySessionDurationLimits,
|
||||||
|
@ -219,22 +235,27 @@ data class CategoryItselfHandling (
|
||||||
dependsOnBatteryCharging = dependsOnBatteryCharging,
|
dependsOnBatteryCharging = dependsOnBatteryCharging,
|
||||||
dependsOnMinBatteryLevel = dependsOnMinBatteryLevel,
|
dependsOnMinBatteryLevel = dependsOnMinBatteryLevel,
|
||||||
dependsOnMaxBatteryLevel = dependsOnMaxBatteryLevel,
|
dependsOnMaxBatteryLevel = dependsOnMaxBatteryLevel,
|
||||||
|
dependsOnNetworkId = dependsOnNetworkId,
|
||||||
createdWithCategoryRelatedData = categoryRelatedData,
|
createdWithCategoryRelatedData = categoryRelatedData,
|
||||||
createdWithBatteryStatus = batteryStatus,
|
createdWithBatteryStatus = batteryStatus,
|
||||||
createdWithTemporarilyTrustTime = shouldTrustTimeTemporarily,
|
createdWithTemporarilyTrustTime = shouldTrustTimeTemporarily,
|
||||||
createdWithAssumeCurrentDevice = assumeCurrentDevice,
|
createdWithAssumeCurrentDevice = assumeCurrentDevice,
|
||||||
createdWithUserRelatedData = user
|
createdWithUserRelatedData = user,
|
||||||
|
createdWithNetworkId = currentNetworkId,
|
||||||
|
createdWithHasPremiumOrLocalMode = hasPremiumOrLocalMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val okBasic = okByBattery && okByTempBlocking && okByBlockedTimeAreas && okByTimeLimitRules && okBySessionDurationLimits && !missingNetworkTime
|
val okBasic = okByBattery && okByTempBlocking && okByBlockedTimeAreas && okByTimeLimitRules && okBySessionDurationLimits && !missingNetworkTime
|
||||||
val okAll = okBasic && okByCurrentDevice
|
val okAll = okBasic && okByCurrentDevice && okByNetworkId
|
||||||
val shouldBlockActivities = !okAll
|
val shouldBlockActivities = !okAll
|
||||||
val activityBlockingReason: BlockingReason = if (!okByBattery)
|
val activityBlockingReason: BlockingReason = if (!okByBattery)
|
||||||
BlockingReason.BatteryLimit
|
BlockingReason.BatteryLimit
|
||||||
else if (!okByTempBlocking)
|
else if (!okByTempBlocking)
|
||||||
BlockingReason.TemporarilyBlocked
|
BlockingReason.TemporarilyBlocked
|
||||||
|
else if (!okByNetworkId)
|
||||||
|
BlockingReason.MissingRequiredNetwork
|
||||||
else if (!okByBlockedTimeAreas)
|
else if (!okByBlockedTimeAreas)
|
||||||
BlockingReason.BlockedAtThisTime
|
BlockingReason.BlockedAtThisTime
|
||||||
else if (!okByTimeLimitRules)
|
else if (!okByTimeLimitRules)
|
||||||
|
@ -278,7 +299,9 @@ data class CategoryItselfHandling (
|
||||||
batteryStatus: BatteryStatus,
|
batteryStatus: BatteryStatus,
|
||||||
shouldTrustTimeTemporarily: Boolean,
|
shouldTrustTimeTemporarily: Boolean,
|
||||||
timeInMillis: Long,
|
timeInMillis: Long,
|
||||||
assumeCurrentDevice: Boolean
|
assumeCurrentDevice: Boolean,
|
||||||
|
currentNetworkId: String?,
|
||||||
|
hasPremiumOrLocalMode: Boolean
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (
|
if (
|
||||||
categoryRelatedData != createdWithCategoryRelatedData || user != createdWithUserRelatedData ||
|
categoryRelatedData != createdWithCategoryRelatedData || user != createdWithUserRelatedData ||
|
||||||
|
@ -299,6 +322,14 @@ data class CategoryItselfHandling (
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dependsOnNetworkId && currentNetworkId != createdWithNetworkId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasPremiumOrLocalMode != createdWithHasPremiumOrLocalMode) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -354,6 +354,21 @@ object ApplyServerDataStatus {
|
||||||
database.category().updateCategorySync(updatedCategory)
|
database.category().updateCategorySync(updatedCategory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apply networks
|
||||||
|
database.categoryNetworkId().deleteByCategoryId(newCategory.categoryId)
|
||||||
|
|
||||||
|
if (newCategory.networks.isNotEmpty()) {
|
||||||
|
database.categoryNetworkId().insertItemsSync(
|
||||||
|
newCategory.networks.map { network ->
|
||||||
|
CategoryNetworkId(
|
||||||
|
categoryId = newCategory.categoryId,
|
||||||
|
networkItemId = network.itemId,
|
||||||
|
hashedNetworkId = network.hashedNetworkId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -965,6 +965,50 @@ data class UpdateCategorySortingAction(val categoryIds: List<String>): ParentAct
|
||||||
writer.endObject()
|
writer.endObject()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class AddCategoryNetworkId(val categoryId: String, val itemId: String, val hashedNetworkId: String): ParentAction() {
|
||||||
|
companion object {
|
||||||
|
private const val TYPE_VALUE = "ADD_CATEGORY_NETWORK_ID"
|
||||||
|
private const val CATEGORY_ID = "categoryId"
|
||||||
|
private const val ITEM_ID = "itemId"
|
||||||
|
private const val HASHED_NETWORK_ID = "hashedNetworkId"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
IdGenerator.assertIdValid(itemId)
|
||||||
|
HexString.assertIsHexString(hashedNetworkId)
|
||||||
|
if (hashedNetworkId.length != CategoryNetworkId.ANONYMIZED_NETWORK_ID_LENGTH) throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(TYPE).value(TYPE_VALUE)
|
||||||
|
writer.name(CATEGORY_ID).value(categoryId)
|
||||||
|
writer.name(ITEM_ID).value(itemId)
|
||||||
|
writer.name(HASHED_NETWORK_ID).value(hashedNetworkId)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ResetCategoryNetworkIds(val categoryId: String): ParentAction() {
|
||||||
|
companion object {
|
||||||
|
private const val TYPE_VALUE = "RESET_CATEGORY_NETWORK_IDS"
|
||||||
|
private const val CATEGORY_ID = "categoryId"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(TYPE).value(TYPE_VALUE)
|
||||||
|
writer.name(CATEGORY_ID).value(categoryId)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DeviceDao
|
// DeviceDao
|
||||||
|
|
||||||
data class UpdateDeviceStatusAction(
|
data class UpdateDeviceStatusAction(
|
||||||
|
|
|
@ -70,6 +70,8 @@ object ActionParser {
|
||||||
// UpdateCategorySorting
|
// UpdateCategorySorting
|
||||||
// UpdateUserFlagsAction
|
// UpdateUserFlagsAction
|
||||||
// UpdateUserLimitLoginCategory
|
// UpdateUserLimitLoginCategory
|
||||||
|
// AddCategoryNetworkId
|
||||||
|
// ResetCategoryNetworkIds
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -729,6 +729,30 @@ object LocalDatabaseParentActionDispatcher {
|
||||||
)
|
)
|
||||||
) }
|
) }
|
||||||
}
|
}
|
||||||
|
is AddCategoryNetworkId -> {
|
||||||
|
DatabaseValidation.assertCategoryExists(database, action.categoryId)
|
||||||
|
|
||||||
|
val count = database.categoryNetworkId().countByCategoryIdSync(action.categoryId)
|
||||||
|
|
||||||
|
if (count + 1 > CategoryNetworkId.MAX_ITEMS) {
|
||||||
|
throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
val oldItem = database.categoryNetworkId().getByCategoryIdAndItemIdSync(categoryId = action.categoryId, itemId = action.itemId)
|
||||||
|
|
||||||
|
if (oldItem != null) {
|
||||||
|
throw IllegalArgumentException("id already used")
|
||||||
|
}
|
||||||
|
|
||||||
|
database.categoryNetworkId().insertItemSync(CategoryNetworkId(
|
||||||
|
categoryId = action.categoryId,
|
||||||
|
networkItemId = action.itemId,
|
||||||
|
hashedNetworkId = action.hashedNetworkId
|
||||||
|
))
|
||||||
|
}
|
||||||
|
is ResetCategoryNetworkIds -> {
|
||||||
|
database.categoryNetworkId().deleteByCategoryId(categoryId = action.categoryId)
|
||||||
|
}
|
||||||
}.let { }
|
}.let { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@ package io.timelimit.android.sync.network
|
||||||
|
|
||||||
import android.util.JsonReader
|
import android.util.JsonReader
|
||||||
import android.util.JsonToken
|
import android.util.JsonToken
|
||||||
|
import io.timelimit.android.crypto.HexString
|
||||||
|
import io.timelimit.android.data.IdGenerator
|
||||||
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
import io.timelimit.android.data.customtypes.ImmutableBitmask
|
||||||
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
import io.timelimit.android.data.customtypes.ImmutableBitmaskJson
|
||||||
import io.timelimit.android.data.model.*
|
import io.timelimit.android.data.model.*
|
||||||
|
@ -443,7 +445,8 @@ data class ServerUpdatedCategoryBaseData(
|
||||||
val timeWarnings: Int,
|
val timeWarnings: Int,
|
||||||
val minBatteryLevelCharging: Int,
|
val minBatteryLevelCharging: Int,
|
||||||
val minBatteryLevelMobile: Int,
|
val minBatteryLevelMobile: Int,
|
||||||
val sort: Int
|
val sort: Int,
|
||||||
|
val networks: List<ServerCategoryNetworkId>
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val CATEGORY_ID = "categoryId"
|
private const val CATEGORY_ID = "categoryId"
|
||||||
|
@ -461,6 +464,7 @@ data class ServerUpdatedCategoryBaseData(
|
||||||
private const val MIN_BATTERY_LEVEL_MOBILE = "mblMobile"
|
private const val MIN_BATTERY_LEVEL_MOBILE = "mblMobile"
|
||||||
private const val MIN_BATTERY_LEVEL_CHARGING = "mblCharging"
|
private const val MIN_BATTERY_LEVEL_CHARGING = "mblCharging"
|
||||||
private const val SORT = "sort"
|
private const val SORT = "sort"
|
||||||
|
private const val NETWORKS = "networks"
|
||||||
|
|
||||||
fun parse(reader: JsonReader): ServerUpdatedCategoryBaseData {
|
fun parse(reader: JsonReader): ServerUpdatedCategoryBaseData {
|
||||||
var categoryId: String? = null
|
var categoryId: String? = null
|
||||||
|
@ -479,6 +483,7 @@ data class ServerUpdatedCategoryBaseData(
|
||||||
var minBatteryLevelCharging = 0
|
var minBatteryLevelCharging = 0
|
||||||
var minBatteryLevelMobile = 0
|
var minBatteryLevelMobile = 0
|
||||||
var sort = 0
|
var sort = 0
|
||||||
|
var networks: List<ServerCategoryNetworkId> = emptyList()
|
||||||
|
|
||||||
reader.beginObject()
|
reader.beginObject()
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
|
@ -498,6 +503,7 @@ data class ServerUpdatedCategoryBaseData(
|
||||||
MIN_BATTERY_LEVEL_CHARGING -> minBatteryLevelCharging = reader.nextInt()
|
MIN_BATTERY_LEVEL_CHARGING -> minBatteryLevelCharging = reader.nextInt()
|
||||||
MIN_BATTERY_LEVEL_MOBILE -> minBatteryLevelMobile = reader.nextInt()
|
MIN_BATTERY_LEVEL_MOBILE -> minBatteryLevelMobile = reader.nextInt()
|
||||||
SORT -> sort = reader.nextInt()
|
SORT -> sort = reader.nextInt()
|
||||||
|
NETWORKS -> networks = ServerCategoryNetworkId.parseList(reader)
|
||||||
else -> reader.skipValue()
|
else -> reader.skipValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -518,7 +524,8 @@ data class ServerUpdatedCategoryBaseData(
|
||||||
timeWarnings = timeWarnings,
|
timeWarnings = timeWarnings,
|
||||||
minBatteryLevelCharging = minBatteryLevelCharging,
|
minBatteryLevelCharging = minBatteryLevelCharging,
|
||||||
minBatteryLevelMobile = minBatteryLevelMobile,
|
minBatteryLevelMobile = minBatteryLevelMobile,
|
||||||
sort = sort
|
sort = sort,
|
||||||
|
networks = networks
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -526,6 +533,41 @@ data class ServerUpdatedCategoryBaseData(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class ServerCategoryNetworkId(val itemId: String, val hashedNetworkId: String) {
|
||||||
|
companion object {
|
||||||
|
private const val ITEM_ID = "itemId"
|
||||||
|
private const val HASHED_NETWORK_ID = "hashedNetworkId"
|
||||||
|
|
||||||
|
fun parse(reader: JsonReader): ServerCategoryNetworkId {
|
||||||
|
var itemId: String? = null
|
||||||
|
var hashedNetworkId: String? = null
|
||||||
|
|
||||||
|
reader.beginObject()
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
when (reader.nextName()) {
|
||||||
|
ITEM_ID -> itemId = reader.nextString()
|
||||||
|
HASHED_NETWORK_ID -> hashedNetworkId = reader.nextString()
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
return ServerCategoryNetworkId(
|
||||||
|
itemId = itemId!!,
|
||||||
|
hashedNetworkId = hashedNetworkId!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseList(reader: JsonReader) = parseJsonArray(reader) { parse(reader) }
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(itemId)
|
||||||
|
HexString.assertIsHexString(hashedNetworkId)
|
||||||
|
if (hashedNetworkId.length != CategoryNetworkId.ANONYMIZED_NETWORK_ID_LENGTH) throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class ServerUpdatedCategoryAssignedApps(
|
data class ServerUpdatedCategoryAssignedApps(
|
||||||
val categoryId: String,
|
val categoryId: String,
|
||||||
val assignedApps: List<String>,
|
val assignedApps: List<String>,
|
||||||
|
|
|
@ -25,6 +25,8 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.databinding.FragmentDiagnoseConnectionBinding
|
import io.timelimit.android.databinding.FragmentDiagnoseConnectionBinding
|
||||||
|
import io.timelimit.android.integration.platform.NetworkId
|
||||||
|
import io.timelimit.android.livedata.liveDataFromFunction
|
||||||
import io.timelimit.android.livedata.liveDataFromValue
|
import io.timelimit.android.livedata.liveDataFromValue
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.sync.websocket.NetworkStatus
|
import io.timelimit.android.sync.websocket.NetworkStatus
|
||||||
|
@ -35,14 +37,14 @@ class DiagnoseConnectionFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
val binding = FragmentDiagnoseConnectionBinding.inflate(inflater, container, false)
|
val binding = FragmentDiagnoseConnectionBinding.inflate(inflater, container, false)
|
||||||
val logic = DefaultAppLogic.with(context!!)
|
val logic = DefaultAppLogic.with(context!!)
|
||||||
|
|
||||||
logic.networkStatus.observe(this, Observer {
|
logic.networkStatus.observe(viewLifecycleOwner, Observer {
|
||||||
binding.generalStatus = getString(when (it!!) {
|
binding.generalStatus = getString(when (it!!) {
|
||||||
NetworkStatus.Online -> R.string.diagnose_connection_yes
|
NetworkStatus.Online -> R.string.diagnose_connection_yes
|
||||||
NetworkStatus.Offline -> R.string.diagnose_connection_no
|
NetworkStatus.Offline -> R.string.diagnose_connection_no
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
logic.isConnected.observe(this, Observer {
|
logic.isConnected.observe(viewLifecycleOwner, Observer {
|
||||||
binding.ownServerStatus = getString(if (it == true)
|
binding.ownServerStatus = getString(if (it == true)
|
||||||
R.string.diagnose_connection_yes
|
R.string.diagnose_connection_yes
|
||||||
else
|
else
|
||||||
|
@ -50,6 +52,14 @@ class DiagnoseConnectionFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
liveDataFromFunction { logic.platformIntegration.getCurrentNetworkId() }.observe(viewLifecycleOwner, Observer {
|
||||||
|
binding.networkId = when (it) {
|
||||||
|
NetworkId.MissingPermission -> "missing permission"
|
||||||
|
NetworkId.NoNetworkConnected -> "no network connected"
|
||||||
|
is NetworkId.Network -> it.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,15 +15,19 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.ui.lock
|
package io.timelimit.android.ui.lock
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.database.sqlite.SQLiteConstraintException
|
import android.database.sqlite.SQLiteConstraintException
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.async.Threads
|
import io.timelimit.android.async.Threads
|
||||||
|
@ -36,10 +40,13 @@ import io.timelimit.android.databinding.LockFragmentBinding
|
||||||
import io.timelimit.android.databinding.LockFragmentCategoryButtonBinding
|
import io.timelimit.android.databinding.LockFragmentCategoryButtonBinding
|
||||||
import io.timelimit.android.date.DateInTimezone
|
import io.timelimit.android.date.DateInTimezone
|
||||||
import io.timelimit.android.integration.platform.BatteryStatus
|
import io.timelimit.android.integration.platform.BatteryStatus
|
||||||
|
import io.timelimit.android.integration.platform.NetworkId
|
||||||
|
import io.timelimit.android.integration.platform.getNetworkIdOrNull
|
||||||
import io.timelimit.android.livedata.*
|
import io.timelimit.android.livedata.*
|
||||||
import io.timelimit.android.logic.*
|
import io.timelimit.android.logic.*
|
||||||
import io.timelimit.android.logic.blockingreason.AppBaseHandling
|
import io.timelimit.android.logic.blockingreason.AppBaseHandling
|
||||||
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
|
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
|
||||||
|
import io.timelimit.android.logic.blockingreason.needsNetworkId
|
||||||
import io.timelimit.android.sync.actions.AddCategoryAppsAction
|
import io.timelimit.android.sync.actions.AddCategoryAppsAction
|
||||||
import io.timelimit.android.sync.actions.IncrementCategoryExtraTimeAction
|
import io.timelimit.android.sync.actions.IncrementCategoryExtraTimeAction
|
||||||
import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction
|
import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction
|
||||||
|
@ -50,6 +57,7 @@ import io.timelimit.android.ui.help.HelpDialogFragment
|
||||||
import io.timelimit.android.ui.main.ActivityViewModel
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
import io.timelimit.android.ui.main.AuthenticationFab
|
import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
import io.timelimit.android.ui.main.getActivityViewModel
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
|
import io.timelimit.android.ui.manage.category.settings.networks.RequestWifiPermission
|
||||||
import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs
|
import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs
|
||||||
import io.timelimit.android.ui.manage.child.advanced.managedisabletimelimits.ManageDisableTimelimitsViewHelper
|
import io.timelimit.android.ui.manage.child.advanced.managedisabletimelimits.ManageDisableTimelimitsViewHelper
|
||||||
import io.timelimit.android.ui.manage.child.category.create.CreateCategoryDialogFragment
|
import io.timelimit.android.ui.manage.child.category.create.CreateCategoryDialogFragment
|
||||||
|
@ -63,6 +71,7 @@ class LockFragment : Fragment() {
|
||||||
private const val EXTRA_PACKAGE_NAME = "pkg"
|
private const val EXTRA_PACKAGE_NAME = "pkg"
|
||||||
private const val EXTRA_ACTIVITY = "activitiy"
|
private const val EXTRA_ACTIVITY = "activitiy"
|
||||||
private const val STATUS_DID_OPEN_SET_CURRENT_DEVICE_SCREEN = "didOpenSetCurrentDeviceScreen"
|
private const val STATUS_DID_OPEN_SET_CURRENT_DEVICE_SCREEN = "didOpenSetCurrentDeviceScreen"
|
||||||
|
private const val LOCATION_REQUEST_CODE = 1
|
||||||
|
|
||||||
fun newInstance(packageName: String, activity: String?): LockFragment {
|
fun newInstance(packageName: String, activity: String?): LockFragment {
|
||||||
val result = LockFragment()
|
val result = LockFragment()
|
||||||
|
@ -96,6 +105,12 @@ class LockFragment : Fragment() {
|
||||||
private val batteryStatus: LiveData<BatteryStatus> by lazy {
|
private val batteryStatus: LiveData<BatteryStatus> by lazy {
|
||||||
logic.platformIntegration.getBatteryStatusLive()
|
logic.platformIntegration.getBatteryStatusLive()
|
||||||
}
|
}
|
||||||
|
private val needsNetworkIdLive = MutableLiveData<Boolean>().apply { value = false }
|
||||||
|
private val realNetworkIdLive: LiveData<NetworkId> by lazy { liveDataFromFunction { logic.platformIntegration.getCurrentNetworkId() } }
|
||||||
|
private val networkIdLive: LiveData<NetworkId?> by lazy { needsNetworkIdLive.switchMap { needsNetworkId ->
|
||||||
|
if (needsNetworkId) realNetworkIdLive as LiveData<NetworkId?> else liveDataFromValue(null as NetworkId?)
|
||||||
|
} }
|
||||||
|
private val hasPremiumOrLocalMode: LiveData<Boolean> by lazy { logic.fullVersion.shouldProvideFullVersionFunctions }
|
||||||
private lateinit var binding: LockFragmentBinding
|
private lateinit var binding: LockFragmentBinding
|
||||||
private val handlingCache = CategoryHandlingCache()
|
private val handlingCache = CategoryHandlingCache()
|
||||||
private val realTime = RealTime.newInstance()
|
private val realTime = RealTime.newInstance()
|
||||||
|
@ -115,6 +130,8 @@ class LockFragment : Fragment() {
|
||||||
private fun update() {
|
private fun update() {
|
||||||
val deviceAndUserRelatedData = deviceAndUserRelatedData.value ?: return
|
val deviceAndUserRelatedData = deviceAndUserRelatedData.value ?: return
|
||||||
val batteryStatus = batteryStatus.value ?: return
|
val batteryStatus = batteryStatus.value ?: return
|
||||||
|
val hasPremiumOrLocalMode = hasPremiumOrLocalMode.value ?: return
|
||||||
|
val networkId = networkIdLive.value
|
||||||
|
|
||||||
logic.realTimeLogic.getRealTime(realTime)
|
logic.realTimeLogic.getRealTime(realTime)
|
||||||
|
|
||||||
|
@ -125,14 +142,6 @@ class LockFragment : Fragment() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handlingCache.reportStatus(
|
|
||||||
user = deviceAndUserRelatedData.userRelatedData,
|
|
||||||
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(deviceAndUserRelatedData.deviceRelatedData, deviceAndUserRelatedData.userRelatedData),
|
|
||||||
batteryStatus = batteryStatus,
|
|
||||||
timeInMillis = realTime.timeInMillis,
|
|
||||||
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily
|
|
||||||
)
|
|
||||||
|
|
||||||
val appBaseHandling = AppBaseHandling.calculate(
|
val appBaseHandling = AppBaseHandling.calculate(
|
||||||
foregroundAppPackageName = packageName,
|
foregroundAppPackageName = packageName,
|
||||||
foregroundAppActivityName = activityName,
|
foregroundAppActivityName = activityName,
|
||||||
|
@ -142,6 +151,24 @@ class LockFragment : Fragment() {
|
||||||
pauseCounting = false
|
pauseCounting = false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val needsNetworkId = appBaseHandling.needsNetworkId()
|
||||||
|
|
||||||
|
if (needsNetworkId != needsNetworkIdLive.value) {
|
||||||
|
needsNetworkIdLive.value = needsNetworkId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsNetworkId && networkId == null) return
|
||||||
|
|
||||||
|
handlingCache.reportStatus(
|
||||||
|
user = deviceAndUserRelatedData.userRelatedData,
|
||||||
|
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(deviceAndUserRelatedData.deviceRelatedData, deviceAndUserRelatedData.userRelatedData),
|
||||||
|
batteryStatus = batteryStatus,
|
||||||
|
timeInMillis = realTime.timeInMillis,
|
||||||
|
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
|
||||||
|
currentNetworkId = networkId?.getNetworkIdOrNull(),
|
||||||
|
hasPremiumOrLocalMode = hasPremiumOrLocalMode
|
||||||
|
)
|
||||||
|
|
||||||
binding.activityName = if (deviceAndUserRelatedData.deviceRelatedData.deviceEntry.enableActivityLevelBlocking)
|
binding.activityName = if (deviceAndUserRelatedData.deviceRelatedData.deviceEntry.enableActivityLevelBlocking)
|
||||||
activityName?.removePrefix(packageName)
|
activityName?.removePrefix(packageName)
|
||||||
else
|
else
|
||||||
|
@ -278,6 +305,10 @@ class LockFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setThisDeviceAsCurrentDevice() = this@LockFragment.setThisDeviceAsCurrentDevice()
|
override fun setThisDeviceAsCurrentDevice() = this@LockFragment.setThisDeviceAsCurrentDevice()
|
||||||
|
|
||||||
|
override fun requestLocationPermission() {
|
||||||
|
RequestWifiPermission.doRequest(this@LockFragment, LOCATION_REQUEST_CODE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,6 +404,12 @@ class LockFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initGrantPermissionView() {
|
||||||
|
networkIdLive.observe(viewLifecycleOwner, Observer {
|
||||||
|
binding.missingNetworkIdPermission = it is NetworkId.MissingPermission
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -411,6 +448,8 @@ class LockFragment : Fragment() {
|
||||||
|
|
||||||
deviceAndUserRelatedData.observe(viewLifecycleOwner, Observer { update() })
|
deviceAndUserRelatedData.observe(viewLifecycleOwner, Observer { update() })
|
||||||
batteryStatus.observe(viewLifecycleOwner, Observer { update() })
|
batteryStatus.observe(viewLifecycleOwner, Observer { update() })
|
||||||
|
networkIdLive.observe(viewLifecycleOwner, Observer { update() })
|
||||||
|
hasPremiumOrLocalMode.observe(viewLifecycleOwner, Observer { update() })
|
||||||
|
|
||||||
binding.packageName = packageName
|
binding.packageName = packageName
|
||||||
|
|
||||||
|
@ -418,6 +457,7 @@ class LockFragment : Fragment() {
|
||||||
binding.appIcon.setImageDrawable(logic.platformIntegration.getAppIcon(packageName))
|
binding.appIcon.setImageDrawable(logic.platformIntegration.getAppIcon(packageName))
|
||||||
|
|
||||||
initExtraTimeView()
|
initExtraTimeView()
|
||||||
|
initGrantPermissionView()
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
@ -441,6 +481,12 @@ class LockFragment : Fragment() {
|
||||||
|
|
||||||
unscheduleUpdate()
|
unscheduleUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
if (grantResults.find { it != PackageManager.PERMISSION_GRANTED } != null) {
|
||||||
|
Toast.makeText(context!!, R.string.generic_runtime_permission_rejected, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Handlers {
|
interface Handlers {
|
||||||
|
@ -452,4 +498,5 @@ interface Handlers {
|
||||||
fun disableTemporarilyLockForAllCategories()
|
fun disableTemporarilyLockForAllCategories()
|
||||||
fun showAuthenticationScreen()
|
fun showAuthenticationScreen()
|
||||||
fun setThisDeviceAsCurrentDevice()
|
fun setThisDeviceAsCurrentDevice()
|
||||||
|
fun requestLocationPermission()
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,9 @@ object AllowUserLoginStatusUtil {
|
||||||
assumeCurrentDevice = true,
|
assumeCurrentDevice = true,
|
||||||
timeInMillis = time.timeInMillis,
|
timeInMillis = time.timeInMillis,
|
||||||
batteryStatus = batteryStatus,
|
batteryStatus = batteryStatus,
|
||||||
shouldTrustTimeTemporarily = time.shouldTrustTimeTemporarily
|
shouldTrustTimeTemporarily = time.shouldTrustTimeTemporarily,
|
||||||
|
currentNetworkId = null, // only checks shouldBlockAtSystemLevel which ignores the network id
|
||||||
|
hasPremiumOrLocalMode = data.deviceRelatedData.isLocalMode || data.deviceRelatedData.isConnectedAndHasPremium
|
||||||
)
|
)
|
||||||
|
|
||||||
val categoryIds = data.limitLoginCategoryUserRelatedData.getCategoryWithParentCategories(data.loginRelatedData.limitLoginCategory.categoryId)
|
val categoryIds = data.limitLoginCategoryUserRelatedData.getCategoryWithParentCategories(data.loginRelatedData.limitLoginCategory.categoryId)
|
||||||
|
|
|
@ -329,6 +329,7 @@ class NewLoginFragment: DialogFragment() {
|
||||||
BlockingReason.NotificationsAreBlocked -> getString(R.string.lock_reason_short_notification_blocking)
|
BlockingReason.NotificationsAreBlocked -> getString(R.string.lock_reason_short_notification_blocking)
|
||||||
BlockingReason.BatteryLimit -> getString(R.string.lock_reason_short_battery_limit)
|
BlockingReason.BatteryLimit -> getString(R.string.lock_reason_short_battery_limit)
|
||||||
BlockingReason.SessionDurationLimit -> getString(R.string.lock_reason_short_session_duration)
|
BlockingReason.SessionDurationLimit -> getString(R.string.lock_reason_short_session_duration)
|
||||||
|
BlockingReason.MissingRequiredNetwork -> getString(R.string.lock_reason_short_missing_required_network)
|
||||||
BlockingReason.NotPartOfAnCategory -> "???"
|
BlockingReason.NotPartOfAnCategory -> "???"
|
||||||
BlockingReason.None -> "???"
|
BlockingReason.None -> "???"
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,10 +15,12 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.ui.manage.category.settings
|
package io.timelimit.android.ui.manage.category.settings
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
@ -36,11 +38,14 @@ import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
import io.timelimit.android.ui.main.getActivityViewModel
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
import io.timelimit.android.ui.manage.category.ManageCategoryFragmentArgs
|
import io.timelimit.android.ui.manage.category.ManageCategoryFragmentArgs
|
||||||
import io.timelimit.android.ui.manage.category.settings.addusedtime.AddUsedTimeDialogFragment
|
import io.timelimit.android.ui.manage.category.settings.addusedtime.AddUsedTimeDialogFragment
|
||||||
|
import io.timelimit.android.ui.manage.category.settings.networks.ManageCategoryNetworksView
|
||||||
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
|
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
|
||||||
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
||||||
|
|
||||||
class CategorySettingsFragment : Fragment() {
|
class CategorySettingsFragment : Fragment() {
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val PERMISSION_REQUEST_CODE = 1
|
||||||
|
|
||||||
fun newInstance(params: ManageCategoryFragmentArgs): CategorySettingsFragment {
|
fun newInstance(params: ManageCategoryFragmentArgs): CategorySettingsFragment {
|
||||||
val result = CategorySettingsFragment()
|
val result = CategorySettingsFragment()
|
||||||
result.arguments = params.toBundle()
|
result.arguments = params.toBundle()
|
||||||
|
@ -117,6 +122,16 @@ class CategorySettingsFragment : Fragment() {
|
||||||
fragmentManager = parentFragmentManager
|
fragmentManager = parentFragmentManager
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ManageCategoryNetworksView.bind(
|
||||||
|
view = binding.networks,
|
||||||
|
auth = auth,
|
||||||
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
|
fragmentManager = parentFragmentManager,
|
||||||
|
fragment = this,
|
||||||
|
permissionRequestCode = PERMISSION_REQUEST_CODE,
|
||||||
|
categoryId = params.categoryId
|
||||||
|
)
|
||||||
|
|
||||||
binding.btnDeleteCategory.setOnClickListener { deleteCategory() }
|
binding.btnDeleteCategory.setOnClickListener { deleteCategory() }
|
||||||
binding.editCategoryTitleGo.setOnClickListener { renameCategory() }
|
binding.editCategoryTitleGo.setOnClickListener { renameCategory() }
|
||||||
binding.addUsedTimeBtn.setOnClickListener { addUsedTime() }
|
binding.addUsedTimeBtn.setOnClickListener { addUsedTime() }
|
||||||
|
@ -231,4 +246,12 @@ class CategorySettingsFragment : Fragment() {
|
||||||
).show(parentFragmentManager)
|
).show(parentFragmentManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
if (requestCode == PERMISSION_REQUEST_CODE) {
|
||||||
|
if (grantResults.find { it != PackageManager.PERMISSION_GRANTED } != null) {
|
||||||
|
Toast.makeText(context!!, R.string.generic_runtime_permission_rejected, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2020 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.category.settings.networks
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.IdGenerator
|
||||||
|
import io.timelimit.android.data.model.CategoryNetworkId
|
||||||
|
import io.timelimit.android.databinding.ManageCategoryNetworksViewBinding
|
||||||
|
import io.timelimit.android.integration.platform.NetworkId
|
||||||
|
import io.timelimit.android.livedata.liveDataFromFunction
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
import io.timelimit.android.livedata.switchMap
|
||||||
|
import io.timelimit.android.sync.actions.AddCategoryNetworkId
|
||||||
|
import io.timelimit.android.sync.actions.ResetCategoryNetworkIds
|
||||||
|
import io.timelimit.android.ui.help.HelpDialogFragment
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
|
||||||
|
|
||||||
|
object ManageCategoryNetworksView {
|
||||||
|
fun bind(
|
||||||
|
view: ManageCategoryNetworksViewBinding,
|
||||||
|
auth: ActivityViewModel,
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
fragmentManager: FragmentManager,
|
||||||
|
categoryId: String,
|
||||||
|
fragment: Fragment,
|
||||||
|
permissionRequestCode: Int
|
||||||
|
) {
|
||||||
|
fun networkId(): NetworkId = auth.logic.platformIntegration.getCurrentNetworkId()
|
||||||
|
|
||||||
|
val context = view.root.context
|
||||||
|
val networkIdLive = liveDataFromFunction { networkId() }
|
||||||
|
val networksLive = auth.database.categoryNetworkId().getByCategoryIdLive(categoryId)
|
||||||
|
val isFullVersionLive = auth.logic.fullVersion.shouldProvideFullVersionFunctions
|
||||||
|
|
||||||
|
view.titleView.setOnClickListener {
|
||||||
|
HelpDialogFragment.newInstance(
|
||||||
|
title = R.string.category_networks_title,
|
||||||
|
text = R.string.category_networks_help
|
||||||
|
).show(fragmentManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
networksLive.switchMap { networks ->
|
||||||
|
networkIdLive.map { networkId ->
|
||||||
|
networks to networkId
|
||||||
|
}
|
||||||
|
}.observe(lifecycleOwner, Observer { (networks, networkId) ->
|
||||||
|
view.showRemoveNetworksButton = networks.isNotEmpty()
|
||||||
|
|
||||||
|
view.addedNetworksText = if (networks.isEmpty())
|
||||||
|
context.getString(R.string.category_networks_empty)
|
||||||
|
else
|
||||||
|
context.getString(
|
||||||
|
R.string.category_networks_not_empty,
|
||||||
|
context.resources.getQuantityString(R.plurals.category_networks_counter, networks.size, networks.size)
|
||||||
|
)
|
||||||
|
|
||||||
|
view.status = when (networkId) {
|
||||||
|
NetworkId.MissingPermission -> NetworkStatus.MissingPermission
|
||||||
|
NetworkId.NoNetworkConnected -> NetworkStatus.NoneConnected
|
||||||
|
is NetworkId.Network -> {
|
||||||
|
val hasItem = networks.find {item ->
|
||||||
|
CategoryNetworkId.anonymizeNetworkId(networkId = networkId.id, itemId = item.networkItemId) == item.hashedNetworkId
|
||||||
|
} != null
|
||||||
|
|
||||||
|
if (hasItem)
|
||||||
|
NetworkStatus.ConnectedAndAdded
|
||||||
|
else if (networks.size + 1 > CategoryNetworkId.MAX_ITEMS)
|
||||||
|
NetworkStatus.ConnectedNotAddedButFull
|
||||||
|
else
|
||||||
|
NetworkStatus.ConnectedButNotAdded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
view.removeBtn.setOnClickListener {
|
||||||
|
val oldList = networksLive.value ?: return@setOnClickListener
|
||||||
|
|
||||||
|
if (
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
ResetCategoryNetworkIds(categoryId = categoryId)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Snackbar.make(view.root, R.string.category_networks_toast_all_removed, Snackbar.LENGTH_LONG)
|
||||||
|
.setAction(R.string.generic_undo) {
|
||||||
|
val isEmpty = networksLive.value?.isEmpty() ?: false
|
||||||
|
|
||||||
|
if (isEmpty) {
|
||||||
|
auth.tryDispatchParentActions(
|
||||||
|
oldList.map { item ->
|
||||||
|
AddCategoryNetworkId(
|
||||||
|
categoryId = item.categoryId,
|
||||||
|
itemId = item.networkItemId,
|
||||||
|
hashedNetworkId = item.hashedNetworkId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.grantPermissionButton.setOnClickListener {
|
||||||
|
RequestWifiPermission.doRequest(fragment, permissionRequestCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
isFullVersionLive.observe(lifecycleOwner, Observer { isFullVersion ->
|
||||||
|
view.addNetworkButton.setOnClickListener {
|
||||||
|
if (isFullVersion) {
|
||||||
|
val itemId = IdGenerator.generateId()
|
||||||
|
val networkId = networkId()
|
||||||
|
|
||||||
|
if (!(networkId is NetworkId.Network)) return@setOnClickListener
|
||||||
|
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
AddCategoryNetworkId(
|
||||||
|
categoryId = categoryId,
|
||||||
|
itemId = itemId,
|
||||||
|
hashedNetworkId = CategoryNetworkId.anonymizeNetworkId(itemId = itemId, networkId = networkId.id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
RequiresPurchaseDialogFragment().show(fragmentManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class NetworkStatus {
|
||||||
|
MissingPermission,
|
||||||
|
NoneConnected,
|
||||||
|
ConnectedButNotAdded,
|
||||||
|
ConnectedNotAddedButFull,
|
||||||
|
ConnectedAndAdded
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* TimeLimit Copyright <C> 2019 - 2020 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.category.settings.networks
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.location.LocationManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import io.timelimit.android.R
|
||||||
|
|
||||||
|
object RequestWifiPermission {
|
||||||
|
private fun isLocationEnabled(context: Context): Boolean = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
|
||||||
|
Settings.Secure.getInt(context.contentResolver, Settings.Secure.LOCATION_MODE) == Settings.Secure.LOCATION_MODE_OFF
|
||||||
|
else {
|
||||||
|
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||||
|
|
||||||
|
locationManager.isLocationEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doRequest(fragment: Fragment, permissionRequestCode: Int) {
|
||||||
|
if (ContextCompat.checkSelfPermission(fragment.requireContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
fragment.requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), permissionRequestCode)
|
||||||
|
} else if (!isLocationEnabled(fragment.requireContext())) {
|
||||||
|
Toast.makeText(fragment.requireContext(), R.string.category_networks_toast_enable_location_service, Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
|
fragment.startActivity(
|
||||||
|
Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Toast.makeText(fragment.requireContext(), R.string.error_general, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -82,7 +82,9 @@ object TimesWidgetItems {
|
||||||
timeInMillis = realTime.timeInMillis,
|
timeInMillis = realTime.timeInMillis,
|
||||||
batteryStatus = logic.platformIntegration.getBatteryStatus(),
|
batteryStatus = logic.platformIntegration.getBatteryStatus(),
|
||||||
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
|
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
|
||||||
assumeCurrentDevice = true
|
assumeCurrentDevice = true,
|
||||||
|
currentNetworkId = null, // not relevant here
|
||||||
|
hasPremiumOrLocalMode = false // not relevant here
|
||||||
)
|
)
|
||||||
|
|
||||||
var maxTime = Long.MAX_VALUE
|
var maxTime = Long.MAX_VALUE
|
||||||
|
|
10
app/src/main/res/drawable/ic_baseline_wifi_lock_24.xml
Normal file
10
app/src/main/res/drawable/ic_baseline_wifi_lock_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M20.5,9.5c0.28,0 0.55,0.04 0.81,0.08L24,6c-3.34,-2.51 -7.5,-4 -12,-4S3.34,3.49 0,6l12,16 3.5,-4.67L15.5,14.5c0,-2.76 2.24,-5 5,-5zM23,16v-1.5c0,-1.38 -1.12,-2.5 -2.5,-2.5S18,13.12 18,14.5L18,16c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1h5c0.55,0 1,-0.45 1,-1v-4c0,-0.55 -0.45,-1 -1,-1zM22,16h-3v-1.5c0,-0.83 0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5L22,16z"/>
|
||||||
|
</vector>
|
|
@ -95,6 +95,9 @@
|
||||||
<include android:id="@+id/notification_filter"
|
<include android:id="@+id/notification_filter"
|
||||||
layout="@layout/category_notification_filter" />
|
layout="@layout/category_notification_filter" />
|
||||||
|
|
||||||
|
<include android:id="@+id/networks"
|
||||||
|
layout="@layout/manage_category_networks_view" />
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
app:cardUseCompatPadding="true"
|
app:cardUseCompatPadding="true"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<!--
|
<!--
|
||||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -25,45 +25,82 @@
|
||||||
<variable
|
<variable
|
||||||
name="ownServerStatus"
|
name="ownServerStatus"
|
||||||
type="String" />
|
type="String" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="networkId"
|
||||||
|
type="String" />
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
android:id="@+id/scroll"
|
android:id="@+id/scroll"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
<androidx.cardview.widget.CardView
|
<LinearLayout
|
||||||
android:layout_margin="8dp"
|
android:padding="8dp"
|
||||||
app:cardUseCompatPadding="true"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
<LinearLayout
|
android:orientation="vertical">
|
||||||
android:padding="8dp"
|
|
||||||
android:orientation="vertical"
|
<androidx.cardview.widget.CardView
|
||||||
|
app:cardUseCompatPadding="true"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
<LinearLayout
|
||||||
<TextView
|
android:padding="8dp"
|
||||||
android:text="@string/diagnose_connection_title"
|
android:orientation="vertical"
|
||||||
android:textAppearance="?android:textAppearanceLarge"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
tools:text="@string/diagnose_connection_general"
|
android:text="@string/diagnose_connection_title"
|
||||||
android:text="@{@string/diagnose_connection_general(generalStatus)}"
|
android:textAppearance="?android:textAppearanceLarge"
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
tools:text="@string/diagnose_connection_general"
|
||||||
|
android:text="@{@string/diagnose_connection_general(generalStatus)}"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
tools:text="@string/diagnose_connection_own_server"
|
||||||
|
android:text="@{@string/diagnose_connection_own_server(ownServerStatus)}"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardUseCompatPadding="true">
|
||||||
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
tools:text="@string/diagnose_connection_own_server"
|
android:text="@string/diagnose_connection_network_id"
|
||||||
android:text="@{@string/diagnose_connection_own_server(ownServerStatus)}"
|
android:textAppearance="?android:textAppearanceLarge"
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="wrap_content" />
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
<TextView
|
||||||
</androidx.cardview.widget.CardView>
|
tools:text="ABCDEF"
|
||||||
|
android:text="@{networkId}"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
</layout>
|
</layout>
|
||||||
|
|
|
@ -51,6 +51,10 @@
|
||||||
name="blockedKindLabel"
|
name="blockedKindLabel"
|
||||||
type="String" />
|
type="String" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="missingNetworkIdPermission"
|
||||||
|
type="boolean" />
|
||||||
|
|
||||||
<import type="android.view.View" />
|
<import type="android.view.View" />
|
||||||
<import type="io.timelimit.android.logic.BlockingReason" />
|
<import type="io.timelimit.android.logic.BlockingReason" />
|
||||||
<import type="android.text.TextUtils" />
|
<import type="android.text.TextUtils" />
|
||||||
|
@ -281,11 +285,11 @@
|
||||||
tools:ignore="UnusedAttribute"
|
tools:ignore="UnusedAttribute"
|
||||||
android:drawablePadding="16dp"
|
android:drawablePadding="16dp"
|
||||||
android:drawableTint="?colorOnSurface"
|
android:drawableTint="?colorOnSurface"
|
||||||
android:drawableStart="@drawable/ic_pause_circle_outline_black_24dp"
|
android:drawableStart="@drawable/ic_baseline_wifi_lock_24"
|
||||||
android:visibility="@{reason == BlockingReason.SessionDurationLimit ? View.VISIBLE : View.GONE}"
|
android:visibility="@{reason == BlockingReason.MissingRequiredNetwork ? View.VISIBLE : View.GONE}"
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
android:text="@{@string/lock_reason_session_duration(blockedKindLabel)}"
|
android:text="@{@string/lock_reason_missing_required_network(blockedKindLabel)}"
|
||||||
tools:text="@string/lock_reason_session_duration"
|
tools:text="@string/lock_reason_missing_required_network"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
@ -566,6 +570,34 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:visibility="@{reason == BlockingReason.MissingRequiredNetwork && missingNetworkIdPermission ? View.VISIBLE : View.GONE}"
|
||||||
|
android:foreground="?selectableItemBackground"
|
||||||
|
android:onClick="@{() -> handlers.requestLocationPermission()}"
|
||||||
|
app:cardUseCompatPadding="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:text="@string/lock_grant_permission_title"
|
||||||
|
android:textAppearance="?android:textAppearanceLarge"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:text="@string/lock_grant_permission_text"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
android:foreground="?selectableItemBackground"
|
android:foreground="?selectableItemBackground"
|
||||||
android:onClick="@{() -> handlers.openMainApp()}"
|
android:onClick="@{() -> handlers.openMainApp()}"
|
||||||
|
|
135
app/src/main/res/layout/manage_category_networks_view.xml
Normal file
135
app/src/main/res/layout/manage_category_networks_view.xml
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation version 3 of the License.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
<variable
|
||||||
|
name="status"
|
||||||
|
type="NetworkStatus" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="addedNetworksText"
|
||||||
|
type="String" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="showRemoveNetworksButton"
|
||||||
|
type="boolean" />
|
||||||
|
|
||||||
|
<import type="android.view.View" />
|
||||||
|
<import type="io.timelimit.android.ui.manage.category.settings.networks.ManageCategoryNetworksView.NetworkStatus" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
app:cardUseCompatPadding="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<LinearLayout
|
||||||
|
android:padding="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
tools:ignore="UnusedAttribute"
|
||||||
|
android:drawableTint="?colorOnSurface"
|
||||||
|
android:id="@+id/title_view"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
android:drawableEnd="@drawable/ic_info_outline_black_24dp"
|
||||||
|
android:textAppearance="?android:textAppearanceLarge"
|
||||||
|
android:text="@string/category_networks_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
tools:text="@string/category_networks_empty"
|
||||||
|
android:text="@{addedNetworksText}"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:visibility="@{showRemoveNetworksButton ? View.VISIBLE : View.GONE}"
|
||||||
|
android:id="@+id/remove_btn"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:text="@string/category_networks_action_remove"
|
||||||
|
style="?borderlessButtonStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:visibility="@{status == NetworkStatus.MissingPermission ? View.VISIBLE : View.GONE}"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:text="@string/category_networks_status_missing_permission"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:visibility="@{status == NetworkStatus.NoneConnected ? View.VISIBLE : View.GONE}"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:text="@string/category_networks_status_none_connected"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:visibility="@{status == NetworkStatus.ConnectedButNotAdded ? View.VISIBLE : View.GONE}"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:text="@string/category_networks_status_connected_no_match"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:visibility="@{status == NetworkStatus.ConnectedNotAddedButFull ? View.VISIBLE : View.GONE}"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:text="@string/category_networks_status_connected_no_match_full"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:visibility="@{status == NetworkStatus.ConnectedAndAdded ? View.VISIBLE : View.GONE}"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:text="@string/category_networks_status_connected_has_match"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/grant_permission_button"
|
||||||
|
android:visibility="@{status == NetworkStatus.MissingPermission ? View.VISIBLE : View.GONE}"
|
||||||
|
style="?materialButtonOutlinedStyle"
|
||||||
|
android:text="@string/category_networks_action_grant"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/add_network_button"
|
||||||
|
android:visibility="@{status == NetworkStatus.ConnectedButNotAdded ? View.VISIBLE : View.GONE}"
|
||||||
|
style="?materialButtonOutlinedStyle"
|
||||||
|
android:text="@string/category_networks_action_add"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:textAppearance="?android:textAppearanceSmall"
|
||||||
|
android:text="@string/purchase_required_info_local_mode_free"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
</layout>
|
|
@ -32,4 +32,5 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="generic_swipe_to_dismiss">Se können diesen Hinweis entfernen, indem Sie ihn zur Seite wischen</string>
|
<string name="generic_swipe_to_dismiss">Se 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>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
<string name="diagnose_connection_own_server">TimeLimit-Server: %s</string>
|
<string name="diagnose_connection_own_server">TimeLimit-Server: %s</string>
|
||||||
<string name="diagnose_connection_yes">verbunden</string>
|
<string name="diagnose_connection_yes">verbunden</string>
|
||||||
<string name="diagnose_connection_no">nicht verbunden</string>
|
<string name="diagnose_connection_no">nicht verbunden</string>
|
||||||
|
<string name="diagnose_connection_network_id">Netzwerk-ID</string>
|
||||||
|
|
||||||
<string name="diagnose_sync_title">Synchronisation</string>
|
<string name="diagnose_sync_title">Synchronisation</string>
|
||||||
<string name="diagnose_sync_empty">Es gibt Nichts das synchronisiert werden müsste</string>
|
<string name="diagnose_sync_empty">Es gibt Nichts das synchronisiert werden müsste</string>
|
||||||
|
|
|
@ -50,6 +50,9 @@
|
||||||
<string name="lock_confirm_time_title">Systemzeit manuell bestätigen</string>
|
<string name="lock_confirm_time_title">Systemzeit manuell bestätigen</string>
|
||||||
<string name="lock_confirm_time_btn">Diese Zeit ist richtig</string>
|
<string name="lock_confirm_time_btn">Diese Zeit ist richtig</string>
|
||||||
|
|
||||||
|
<string name="lock_grant_permission_title">Berechtigung erteilen</string>
|
||||||
|
<string name="lock_grant_permission_text">TimeLimit ermöglichen, das verbundene Netzwerk zu erkennen</string>
|
||||||
|
|
||||||
<string name="lock_reason_no_category">
|
<string name="lock_reason_no_category">
|
||||||
Die App wurde zu keiner Kategorie zugeordnet. Das bedeutet, dass es keine Einschränkungs-Einstellungen gibt. Und da ist es die einfachste Lösung, die App zu sperren.
|
Die App wurde zu keiner Kategorie zugeordnet. Das bedeutet, dass es keine Einschränkungs-Einstellungen gibt. Und da ist es die einfachste Lösung, die App zu sperren.
|
||||||
</string>
|
</string>
|
||||||
|
@ -84,6 +87,10 @@
|
||||||
Für diese %s gibt es eine Sitzungsdauerbegrenzung.
|
Für diese %s gibt es eine Sitzungsdauerbegrenzung.
|
||||||
Nach dem Ablauf der Pausenzeit wird die Sperre aufgehoben.
|
Nach dem Ablauf der Pausenzeit wird die Sperre aufgehoben.
|
||||||
</string>
|
</string>
|
||||||
|
<string name="lock_reason_missing_required_network">
|
||||||
|
Diese %s darf nur in bestimmten Netzwerken verwendet werden,
|
||||||
|
aber es wurde keine Verbindung zu einem entsprechenden Netzwerk gefunden.
|
||||||
|
</string>
|
||||||
|
|
||||||
<string name="lock_reason_short_no_category">keine Kategorie</string>
|
<string name="lock_reason_short_no_category">keine Kategorie</string>
|
||||||
<string name="lock_reason_short_temporarily_blocked">vorübergehend gesperrt</string>
|
<string name="lock_reason_short_temporarily_blocked">vorübergehend gesperrt</string>
|
||||||
|
@ -94,6 +101,7 @@
|
||||||
<string name="lock_reason_short_notification_blocking">alle Benachrichtigungen werden blockiert</string>
|
<string name="lock_reason_short_notification_blocking">alle Benachrichtigungen werden blockiert</string>
|
||||||
<string name="lock_reason_short_battery_limit">Akkulimit unterschritten</string>
|
<string name="lock_reason_short_battery_limit">Akkulimit unterschritten</string>
|
||||||
<string name="lock_reason_short_session_duration">Sitzungsdauergrenze erreicht</string>
|
<string name="lock_reason_short_session_duration">Sitzungsdauergrenze erreicht</string>
|
||||||
|
<string name="lock_reason_short_missing_required_network">kein erlaubtes Netzwerk</string>
|
||||||
|
|
||||||
<string name="lock_overlay_warning">
|
<string name="lock_overlay_warning">
|
||||||
Öffnen des Sperrbildschirms fehlgeschlagen.
|
Öffnen des Sperrbildschirms fehlgeschlagen.
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation version 3 of the License.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<string name="category_networks_title">Netzwerke</string>
|
||||||
|
<string name="category_networks_help">Hier können WLAN-Drahtlosnetzwerke hinzugefügt werden.
|
||||||
|
Diese Kategorie wird gesperrt, wenn Netzwerke hinzugefügt wurden und es keine
|
||||||
|
Verbindung zu einem der angegebenen Netzwerke gibt.
|
||||||
|
Ein Netzwerk wird durch einen Access Point identifiziert. Im Fall von mehreren Access Point/ Repeatern
|
||||||
|
müssen alle einzeln hinzugefügt werden.
|
||||||
|
Das vorübergehende Deaktivieren von Zeitbegrenzungen deaktiviert auch die Beschränkungen durch diese Funktion.
|
||||||
|
</string>
|
||||||
|
<string name="category_networks_empty">Sie haben keine Netzwerke hinzugefügt. Diese Kategorie wird
|
||||||
|
nicht abhängig vom Netzwerk gesperrt.
|
||||||
|
</string>
|
||||||
|
<string name="category_networks_not_empty">Es wurden %s hinzugefügt. Diese Kategorie kann nur in den
|
||||||
|
angegebenen Netzwerken verwendet werden.</string>
|
||||||
|
<plurals name="category_networks_counter">
|
||||||
|
<item quantity="one">%d Netzwerk</item>
|
||||||
|
<item quantity="other">%d Netzwerke</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="category_networks_status_missing_permission">Die Berechtigung zum Abrufen vom aktuellen Netzwerk fehlt</string>
|
||||||
|
<string name="category_networks_status_none_connected">Sie sind mit keinem WLAN verbunden</string>
|
||||||
|
<string name="category_networks_status_connected_no_match">Das aktuelle WLAN wurde nicht hinzugefügt</string>
|
||||||
|
<string name="category_networks_status_connected_no_match_full">Das aktuelle WLAN wurde nicht hinzugefügt,
|
||||||
|
aber es können keine weiteren WLANs hinzugefügt werden
|
||||||
|
</string>
|
||||||
|
<string name="category_networks_status_connected_has_match">Das aktuelle WLAN wurde hinzugefügt</string>
|
||||||
|
<string name="category_networks_action_add">Netzwerk hinzufügen</string>
|
||||||
|
<string name="category_networks_action_grant">Berechtigung erteilen</string>
|
||||||
|
<string name="category_networks_action_remove">Alle Netzwerke entfernen</string>
|
||||||
|
<string name="category_networks_toast_all_removed">alle Netzwerke entfernt</string>
|
||||||
|
|
||||||
|
<string name="category_networks_toast_enable_location_service">Bitte den Standortzugriff aktivieren</string>
|
||||||
|
</resources>
|
|
@ -32,4 +32,5 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="generic_swipe_to_dismiss">Swipe to the side to remove this message</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>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
<string name="diagnose_connection_own_server">TimeLimit Server: %s</string>
|
<string name="diagnose_connection_own_server">TimeLimit Server: %s</string>
|
||||||
<string name="diagnose_connection_yes">connected</string>
|
<string name="diagnose_connection_yes">connected</string>
|
||||||
<string name="diagnose_connection_no">not connected</string>
|
<string name="diagnose_connection_no">not connected</string>
|
||||||
|
<string name="diagnose_connection_network_id">Network ID</string>
|
||||||
|
|
||||||
<string name="diagnose_sync_title">Synchronization</string>
|
<string name="diagnose_sync_title">Synchronization</string>
|
||||||
<string name="diagnose_sync_empty">There is nothing which needs to be synchronized</string>
|
<string name="diagnose_sync_empty">There is nothing which needs to be synchronized</string>
|
||||||
|
|
|
@ -52,6 +52,9 @@
|
||||||
<string name="lock_confirm_time_title">Confirm time manually</string>
|
<string name="lock_confirm_time_title">Confirm time manually</string>
|
||||||
<string name="lock_confirm_time_btn">This time is correct</string>
|
<string name="lock_confirm_time_btn">This time is correct</string>
|
||||||
|
|
||||||
|
<string name="lock_grant_permission_title">Grant permission</string>
|
||||||
|
<string name="lock_grant_permission_text">Allow TimeLimit to see the connected network</string>
|
||||||
|
|
||||||
<string name="lock_reason_no_category">
|
<string name="lock_reason_no_category">
|
||||||
This App was not assigned to any category.
|
This App was not assigned to any category.
|
||||||
Due to that, there are no restriction settings.
|
Due to that, there are no restriction settings.
|
||||||
|
@ -89,6 +92,9 @@
|
||||||
and this limit was reached.
|
and this limit was reached.
|
||||||
It will be unlocked after the break duration.
|
It will be unlocked after the break duration.
|
||||||
</string>
|
</string>
|
||||||
|
<string name="lock_reason_missing_required_network">
|
||||||
|
This %s is only allowed in some networks, but no connection to such network was found.
|
||||||
|
</string>
|
||||||
|
|
||||||
<string name="lock_reason_short_no_category">no category</string>
|
<string name="lock_reason_short_no_category">no category</string>
|
||||||
<string name="lock_reason_short_temporarily_blocked">temporarily blocked</string>
|
<string name="lock_reason_short_temporarily_blocked">temporarily blocked</string>
|
||||||
|
@ -99,6 +105,7 @@
|
||||||
<string name="lock_reason_short_notification_blocking">all notifications are blocked</string>
|
<string name="lock_reason_short_notification_blocking">all notifications are blocked</string>
|
||||||
<string name="lock_reason_short_battery_limit">battery limit reached</string>
|
<string name="lock_reason_short_battery_limit">battery limit reached</string>
|
||||||
<string name="lock_reason_short_session_duration">session duration limit reached</string>
|
<string name="lock_reason_short_session_duration">session duration limit reached</string>
|
||||||
|
<string name="lock_reason_short_missing_required_network">no allowed network</string>
|
||||||
|
|
||||||
<string name="lock_overlay_warning">
|
<string name="lock_overlay_warning">
|
||||||
Failed to open the lock screen.
|
Failed to open the lock screen.
|
||||||
|
|
43
app/src/main/res/values/strings-manage-category-networks.xml
Normal file
43
app/src/main/res/values/strings-manage-category-networks.xml
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation version 3 of the License.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<string name="category_networks_title">Networks</string>
|
||||||
|
<string name="category_networks_help">Here you can add WiFi networks. This category will be blocked
|
||||||
|
if there are WiFi networks added and there is no connection to a specified network.
|
||||||
|
A network is identified by an access point. In case of multiple access points/ repeaters,
|
||||||
|
you have to add all of them one by one.
|
||||||
|
Disabling the time limits temporarily additionally disables limitations caused by this feature.
|
||||||
|
</string>
|
||||||
|
<string name="category_networks_empty">You did not add any networks. This category is not limited to specific networks.</string>
|
||||||
|
<string name="category_networks_not_empty">You added %s. This category is limited to the specified networks.</string>
|
||||||
|
<plurals name="category_networks_counter">
|
||||||
|
<item quantity="one">%d network</item>
|
||||||
|
<item quantity="other">%d networks</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="category_networks_status_missing_permission">The permission to get the connected network is missing</string>
|
||||||
|
<string name="category_networks_status_none_connected">You are not connected to any WiFi network</string>
|
||||||
|
<string name="category_networks_status_connected_no_match">You are connected to a network which was not added</string>
|
||||||
|
<string name="category_networks_status_connected_no_match_full">You are connected to a network which was not added,
|
||||||
|
but you can not add more networks to this category
|
||||||
|
</string>
|
||||||
|
<string name="category_networks_status_connected_has_match">You are connected to a network which was added</string>
|
||||||
|
<string name="category_networks_action_add">Add network</string>
|
||||||
|
<string name="category_networks_action_grant">Grant permission</string>
|
||||||
|
<string name="category_networks_action_remove">Remove all networks</string>
|
||||||
|
<string name="category_networks_toast_all_removed">All networks removed</string>
|
||||||
|
|
||||||
|
<string name="category_networks_toast_enable_location_service">Please enable location access</string>
|
||||||
|
</resources>
|
Loading…
Add table
Add a link
Reference in a new issue