Add option to limit categories depending on the current network

This commit is contained in:
Jonas Lochmann 2020-08-24 02:00:00 +02:00
parent 1387ef86e2
commit 20ca6bcc91
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
49 changed files with 2265 additions and 92 deletions

File diff suppressed because it is too large Load diff

View file

@ -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" />

View file

@ -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)
} }

View file

@ -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

View file

@ -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 )")
}
}
} }

View file

@ -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()

View file

@ -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) }
} }
} }
} }

View file

@ -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)
}

View file

@ -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()
} }
} }

View file

@ -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()
}
}

View file

@ -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
) )
} }
} }

View file

@ -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 {

View file

@ -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
} }
@ -217,3 +219,11 @@ data class BatteryStatus(
} }
} }
} }
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

View file

@ -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()
} }

View file

@ -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))
}
}

View file

@ -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
) )
} }

View file

@ -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
} }

View file

@ -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 ||

View file

@ -32,7 +32,8 @@ enum class BlockingReason {
RequiresCurrentDevice, RequiresCurrentDevice,
NotificationsAreBlocked, NotificationsAreBlocked,
BatteryLimit, BatteryLimit,
SessionDurationLimit SessionDurationLimit,
MissingRequiredNetwork
} }
enum class BlockingLevel { enum class BlockingLevel {

View file

@ -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

View file

@ -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 {
@ -113,3 +119,5 @@ sealed class AppBaseHandling {
} }
} }
} }
fun AppBaseHandling.needsNetworkId(): Boolean = if (this is AppBaseHandling.UseCategories) this.needsNetworkId else false

View file

@ -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
) )
} }

View file

@ -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
} }
} }

View file

@ -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
)
}
)
}
} }
} }
} }

View file

@ -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(

View file

@ -70,6 +70,8 @@ object ActionParser {
// UpdateCategorySorting // UpdateCategorySorting
// UpdateUserFlagsAction // UpdateUserFlagsAction
// UpdateUserLimitLoginCategory // UpdateUserLimitLoginCategory
// AddCategoryNetworkId
// ResetCategoryNetworkIds
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }

View file

@ -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 { }
} }
} }

View file

@ -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>,

View file

@ -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
} }

View file

@ -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()
} }

View file

@ -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)

View file

@ -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 -> "???"
} }

View file

@ -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()
}
}
}
} }

View file

@ -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
}
}

View file

@ -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()
}
}
}

View file

@ -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

View 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>

View file

@ -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"

View file

@ -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>

View file

@ -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 &amp;&amp; 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()}"

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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.

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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.

View 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>