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.CALL_PHONE" />
<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.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
* 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"))))
}
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 derivedDataDao(): DerivedDataDao
fun userLimitLoginCategoryDao(): UserLimitLoginCategoryDao
fun categoryNetworkId(): CategoryNetworkIdDao
fun <T> runInTransaction(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`)")
}
}
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,
UserKey::class,
SessionDuration::class,
UserLimitLoginCategory::class
], version = 31)
UserLimitLoginCategory::class,
CategoryNetworkId::class
], version = 32)
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
companion 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_V29,
DatabaseMigrations.MIGRATE_TO_V30,
DatabaseMigrations.MIGRATE_TO_V31
DatabaseMigrations.MIGRATE_TO_V31,
DatabaseMigrations.MIGRATE_TO_V32
)
.setQueryExecutor(Threads.database)
.build()

View file

@ -43,6 +43,7 @@ object DatabaseBackupLowlevel {
private const val USER_KEY = "userKey"
private const val SESSION_DURATION = "sessionDuration"
private const val USER_LIMIT_LOGIN_CATEGORY = "userLimitLoginCategory"
private const val CATEGORY_NETWORK_ID = "categoryNetworkId"
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
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(SESSION_DURATION) { offset, pageSize -> database.sessionDuration().getSessionDurationPageSync(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()
}
@ -98,6 +100,7 @@ object DatabaseBackupLowlevel {
val reader = JsonReader(InputStreamReader(inputStream, Charsets.UTF_8))
var userLoginLimitCategories = emptyList<UserLimitLoginCategory>()
var categoryNetworkId = emptyList<CategoryNetworkId>()
database.runInTransaction {
database.deleteAllData()
@ -251,12 +254,26 @@ object DatabaseBackupLowlevel {
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()
}
}
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,
User,
UserKey,
UserLimitLoginCategory
UserLimitLoginCategory,
CategoryNetworkId
}
object TableNames {
@ -52,6 +53,7 @@ object TableNames {
const val USER = "user"
const val USER_KEY = "user_key"
const val USER_LIMIT_LOGIN_CATEGORY = "user_limit_login_category"
const val CATEGORY_NETWORK_ID = "category_network_id"
}
object TableUtil {
@ -72,6 +74,7 @@ object TableUtil {
Table.User -> TableNames.USER
Table.UserKey -> TableNames.USER_KEY
Table.UserLimitLoginCategory -> TableNames.USER_LIMIT_LOGIN_CATEGORY
Table.CategoryNetworkId -> TableNames.CATEGORY_NETWORK_ID
}
fun toEnum(value: String): Table = when (value) {
@ -91,6 +94,7 @@ object TableUtil {
TableNames.USER -> Table.User
TableNames.USER_KEY -> Table.UserKey
TableNames.USER_LIMIT_LOGIN_CATEGORY -> Table.UserLimitLoginCategory
TableNames.CATEGORY_NETWORK_ID -> Table.CategoryNetworkId
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
import io.timelimit.android.data.Database
import io.timelimit.android.data.model.Category
import io.timelimit.android.data.model.SessionDuration
import io.timelimit.android.data.model.TimeLimitRule
import io.timelimit.android.data.model.UsedTimeItem
import io.timelimit.android.data.model.*
data class CategoryRelatedData(
val category: Category,
val rules: List<TimeLimitRule>,
val usedTimes: List<UsedTimeItem>,
val durations: List<SessionDuration>
val durations: List<SessionDuration>,
val networks: List<CategoryNetworkId>
) {
companion object {
fun load(category: Category, database: Database): CategoryRelatedData = database.runInUnobservedTransaction {
val rules = database.timeLimitRules().getTimeLimitRulesByCategorySync(category.id)
val usedTimes = database.usedTimes().getUsedTimeItemsByCategoryId(category.id)
val durations = database.sessionDuration().getSessionDurationItemsByCategoryIdSync(category.id)
val networks = database.categoryNetworkId().getByCategoryIdSync(category.id)
CategoryRelatedData(
category = category,
rules = rules,
usedTimes = usedTimes,
durations = durations
durations = durations,
networks = networks
)
}
}
@ -48,6 +48,7 @@ data class CategoryRelatedData(
updateRules: Boolean,
updateTimes: Boolean,
updateDurations: Boolean,
updateNetworks: Boolean,
database: Database
): CategoryRelatedData = database.runInUnobservedTransaction {
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 usedTimes = if (updateTimes) database.usedTimes().getUsedTimeItemsByCategoryId(category.id) else usedTimes
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
} else {
CategoryRelatedData(
category = category,
rules = rules,
usedTimes = usedTimes,
durations = durations
durations = durations,
networks = networks
)
}
}

View file

@ -38,7 +38,8 @@ data class UserRelatedData(
private val relatedTables = arrayOf(
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 {
@ -82,9 +83,10 @@ data class UserRelatedData(
private var usedTimesInvalidated = false
private var sessionDurationsInvalidated = false
private var categoryAppsInvalidated = false
private var categoryNetworksInvalidated = false
private val invalidated
get() = userInvalidated || categoriesInvalidated || rulesInvalidated || usedTimesInvalidated || sessionDurationsInvalidated || categoryAppsInvalidated
get() = userInvalidated || categoriesInvalidated || rulesInvalidated || usedTimesInvalidated || sessionDurationsInvalidated || categoryAppsInvalidated || categoryNetworksInvalidated
override fun onInvalidated(tables: Set<Table>) {
tables.forEach {
@ -95,6 +97,7 @@ data class UserRelatedData(
Table.UsedTimeItem -> usedTimesInvalidated = true
Table.SessionDuration -> sessionDurationsInvalidated = true
Table.CategoryApp -> categoryAppsInvalidated = true
Table.CategoryNetworkId -> categoryNetworksInvalidated = true
else -> {/* do nothing */}
}
}
@ -117,20 +120,22 @@ data class UserRelatedData(
database = database,
updateDurations = sessionDurationsInvalidated,
updateRules = rulesInvalidated,
updateTimes = usedTimesInvalidated
updateTimes = usedTimesInvalidated,
updateNetworks = categoryNetworksInvalidated
) ?: CategoryRelatedData.load(
category = category,
database = database
)
}
} else if (sessionDurationsInvalidated || rulesInvalidated || usedTimesInvalidated) {
} else if (sessionDurationsInvalidated || rulesInvalidated || usedTimesInvalidated || categoryNetworksInvalidated) {
categories.map {
it.update(
category = it.category,
database = database,
updateDurations = sessionDurationsInvalidated,
updateRules = rulesInvalidated,
updateTimes = usedTimesInvalidated
updateTimes = usedTimesInvalidated,
updateNetworks = categoryNetworksInvalidated
)
}
} else {

View file

@ -74,6 +74,8 @@ abstract class PlatformIntegration(
abstract fun restartApp()
abstract fun getCurrentNetworkId(): NetworkId
var installedAppsChangeListener: 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 overlay = OverlayUtil(context as Application)
private val battery = BatteryStatusUtil(context)
private val connectedNetwork = ConnectedNetworkUtil(context)
init {
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.runAsync
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.blockingreason.AppBaseHandling
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.BatteryLimit -> getString(R.string.lock_reason_short_battery_limit)
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()
}
)
@ -165,12 +168,16 @@ class NotificationListener: NotificationListenerService() {
val time = RealTime.newInstance()
val battery = appLogic.platformIntegration.getBatteryStatus()
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)
val categoryHandlings = appHandling.categoryIds.map { categoryId ->
val categoryRelatedData = deviceAndUserRelatedData.userRelatedData.categoryById[categoryId]!!
CategoryItselfHandling.calculate(
categoryRelatedData = deviceAndUserRelatedData.userRelatedData.categoryById[categoryId]!!,
categoryRelatedData = categoryRelatedData,
user = deviceAndUserRelatedData.userRelatedData,
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(
device = deviceAndUserRelatedData.deviceRelatedData,
@ -178,7 +185,9 @@ class NotificationListener: NotificationListenerService() {
),
batteryStatus = battery,
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 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.ProtectionLevel
import io.timelimit.android.integration.platform.android.AccessibilityService
import io.timelimit.android.integration.platform.getNetworkIdOrNull
import io.timelimit.android.livedata.*
import io.timelimit.android.logic.blockingreason.AppBaseHandling
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.apply.ApplyActionUtil
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(
appLogic.getForegroundAppQueryInterval(),
appLogic.getEnableMultiAppDetection()
@ -304,6 +296,21 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
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
val blockedForegroundApp = foregroundAppWithBaseHandlings.find { (_, foregroundAppBaseHandling) ->
foregroundAppBaseHandling is AppBaseHandling.BlockDueToNoCategory ||

View file

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

View file

@ -112,7 +112,9 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
assumeCurrentDevice = CurrentDeviceLogic.handleDeviceAsCurrentDevice(
device = userAndDeviceRelatedData.deviceRelatedData,
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

View file

@ -32,7 +32,8 @@ sealed class AppBaseHandling {
data class UseCategories(
val categoryIds: Set<String>,
val shouldCount: Boolean,
val level: BlockingLevel
val level: BlockingLevel,
val needsNetworkId: Boolean
): AppBaseHandling() {
init {
if (categoryIds.isEmpty()) {
@ -85,14 +86,19 @@ sealed class AppBaseHandling {
if (startCategory == null) {
return BlockDueToNoCategory
} else {
val categoryIds = userRelatedData.getCategoryWithParentCategories(startCategoryId = startCategory.category.id)
return UseCategories(
categoryIds = userRelatedData.getCategoryWithParentCategories(startCategoryId = startCategory.category.id),
categoryIds = categoryIds,
shouldCount = !pauseCounting,
level = when (appCategory?.specifiesActivity) {
null -> BlockingLevel.Activity // occurs when using a default category
true -> BlockingLevel.Activity
false -> BlockingLevel.App
}
},
needsNetworkId = categoryIds.find { categoryId ->
userRelatedData.categoryById[categoryId]!!.networks.isNotEmpty()
} != null
)
}
} 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 timeInMillis: Long = 0
private var assumeCurrentDevice: Boolean = false
private var currentNetworkId: String? = null
private var hasPremiumOrLocalMode: Boolean = false
fun reportStatus(
user: UserRelatedData,
batteryStatus: BatteryStatus,
shouldTrustTimeTemporarily: Boolean,
timeInMillis: Long,
assumeCurrentDevice: Boolean
assumeCurrentDevice: Boolean,
currentNetworkId: String?,
hasPremiumOrLocalMode: Boolean
) {
this.user = user
this.batteryStatus = batteryStatus
this.shouldTrustTimeTemporarily = shouldTrustTimeTemporarily
this.timeInMillis = timeInMillis
this.assumeCurrentDevice = assumeCurrentDevice
this.currentNetworkId = currentNetworkId
this.hasPremiumOrLocalMode = hasPremiumOrLocalMode
val iterator = cachedItems.iterator()
@ -54,7 +60,9 @@ class CategoryHandlingCache {
batteryStatus = batteryStatus,
assumeCurrentDevice = assumeCurrentDevice,
shouldTrustTimeTemporarily = shouldTrustTimeTemporarily,
timeInMillis = timeInMillis
timeInMillis = timeInMillis,
currentNetworkId = currentNetworkId,
hasPremiumOrLocalMode = hasPremiumOrLocalMode
)
) {
iterator.remove()
@ -76,6 +84,8 @@ class CategoryHandlingCache {
batteryStatus = batteryStatus,
assumeCurrentDevice = assumeCurrentDevice,
shouldTrustTimeTemporarily = shouldTrustTimeTemporarily,
timeInMillis = timeInMillis
timeInMillis = timeInMillis,
currentNetworkId = currentNetworkId,
hasPremiumOrLocalMode = hasPremiumOrLocalMode
)
}

View file

@ -15,6 +15,7 @@
*/
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.UserRelatedData
import io.timelimit.android.date.DateInTimezone
@ -37,6 +38,7 @@ data class CategoryItselfHandling (
val areLimitsTemporarilyDisabled: Boolean,
val okByBattery: Boolean,
val okByTempBlocking: Boolean,
val okByNetworkId: Boolean,
val okByBlockedTimeAreas: Boolean,
val okByTimeLimitRules: Boolean,
val okBySessionDurationLimits: Boolean,
@ -50,11 +52,14 @@ data class CategoryItselfHandling (
val dependsOnBatteryCharging: Boolean,
val dependsOnMinBatteryLevel: Int,
val dependsOnMaxBatteryLevel: Int,
val dependsOnNetworkId: Boolean,
val createdWithCategoryRelatedData: CategoryRelatedData,
val createdWithUserRelatedData: UserRelatedData,
val createdWithBatteryStatus: BatteryStatus,
val createdWithTemporarilyTrustTime: Boolean,
val createdWithAssumeCurrentDevice: Boolean
val createdWithAssumeCurrentDevice: Boolean,
val createdWithNetworkId: String?,
val createdWithHasPremiumOrLocalMode: Boolean
) {
companion object {
fun calculate(
@ -63,7 +68,9 @@ data class CategoryItselfHandling (
batteryStatus: BatteryStatus,
shouldTrustTimeTemporarily: Boolean,
timeInMillis: Long,
assumeCurrentDevice: Boolean
assumeCurrentDevice: Boolean,
currentNetworkId: String?,
hasPremiumOrLocalMode: Boolean
): CategoryItselfHandling {
val dependsOnMinTime = timeInMillis
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
// 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 okByBlockedTimeAreas = areLimitsTemporarilyDisabled || !categoryRelatedData.category.blockedMinutesInWeek.read(minuteInWeek)
val dependsOnMaxMinuteOfWeekByBlockedTimeAreas = categoryRelatedData.category.blockedMinutesInWeek.let { blockedTimeAreas ->
@ -195,7 +210,7 @@ data class CategoryItselfHandling (
else
emptySet()
val blockAllNotifications = categoryRelatedData.category.blockAllNotifications
val blockAllNotifications = categoryRelatedData.category.blockAllNotifications &&hasPremiumOrLocalMode
return CategoryItselfHandling(
shouldCountTime = shouldCountTime,
@ -205,6 +220,7 @@ data class CategoryItselfHandling (
areLimitsTemporarilyDisabled = areLimitsTemporarilyDisabled,
okByBattery = okByBattery,
okByTempBlocking = okByTempBlocking,
okByNetworkId = okByNetworkId,
okByBlockedTimeAreas = okByBlockedTimeAreas,
okByTimeLimitRules = okByTimeLimitRules,
okBySessionDurationLimits = okBySessionDurationLimits,
@ -219,22 +235,27 @@ data class CategoryItselfHandling (
dependsOnBatteryCharging = dependsOnBatteryCharging,
dependsOnMinBatteryLevel = dependsOnMinBatteryLevel,
dependsOnMaxBatteryLevel = dependsOnMaxBatteryLevel,
dependsOnNetworkId = dependsOnNetworkId,
createdWithCategoryRelatedData = categoryRelatedData,
createdWithBatteryStatus = batteryStatus,
createdWithTemporarilyTrustTime = shouldTrustTimeTemporarily,
createdWithAssumeCurrentDevice = assumeCurrentDevice,
createdWithUserRelatedData = user
createdWithUserRelatedData = user,
createdWithNetworkId = currentNetworkId,
createdWithHasPremiumOrLocalMode = hasPremiumOrLocalMode
)
}
}
val okBasic = okByBattery && okByTempBlocking && okByBlockedTimeAreas && okByTimeLimitRules && okBySessionDurationLimits && !missingNetworkTime
val okAll = okBasic && okByCurrentDevice
val okAll = okBasic && okByCurrentDevice && okByNetworkId
val shouldBlockActivities = !okAll
val activityBlockingReason: BlockingReason = if (!okByBattery)
BlockingReason.BatteryLimit
else if (!okByTempBlocking)
BlockingReason.TemporarilyBlocked
else if (!okByNetworkId)
BlockingReason.MissingRequiredNetwork
else if (!okByBlockedTimeAreas)
BlockingReason.BlockedAtThisTime
else if (!okByTimeLimitRules)
@ -278,7 +299,9 @@ data class CategoryItselfHandling (
batteryStatus: BatteryStatus,
shouldTrustTimeTemporarily: Boolean,
timeInMillis: Long,
assumeCurrentDevice: Boolean
assumeCurrentDevice: Boolean,
currentNetworkId: String?,
hasPremiumOrLocalMode: Boolean
): Boolean {
if (
categoryRelatedData != createdWithCategoryRelatedData || user != createdWithUserRelatedData ||
@ -299,6 +322,14 @@ data class CategoryItselfHandling (
return false
}
if (dependsOnNetworkId && currentNetworkId != createdWithNetworkId) {
return false
}
if (hasPremiumOrLocalMode != createdWithHasPremiumOrLocalMode) {
return false
}
return true
}
}

View file

@ -354,6 +354,21 @@ object ApplyServerDataStatus {
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()
}
}
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
data class UpdateDeviceStatusAction(

View file

@ -70,6 +70,8 @@ object ActionParser {
// UpdateCategorySorting
// UpdateUserFlagsAction
// UpdateUserLimitLoginCategory
// AddCategoryNetworkId
// ResetCategoryNetworkIds
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 { }
}
}

View file

@ -17,6 +17,8 @@ package io.timelimit.android.sync.network
import android.util.JsonReader
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.ImmutableBitmaskJson
import io.timelimit.android.data.model.*
@ -443,7 +445,8 @@ data class ServerUpdatedCategoryBaseData(
val timeWarnings: Int,
val minBatteryLevelCharging: Int,
val minBatteryLevelMobile: Int,
val sort: Int
val sort: Int,
val networks: List<ServerCategoryNetworkId>
) {
companion object {
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_CHARGING = "mblCharging"
private const val SORT = "sort"
private const val NETWORKS = "networks"
fun parse(reader: JsonReader): ServerUpdatedCategoryBaseData {
var categoryId: String? = null
@ -479,6 +483,7 @@ data class ServerUpdatedCategoryBaseData(
var minBatteryLevelCharging = 0
var minBatteryLevelMobile = 0
var sort = 0
var networks: List<ServerCategoryNetworkId> = emptyList()
reader.beginObject()
while (reader.hasNext()) {
@ -498,6 +503,7 @@ data class ServerUpdatedCategoryBaseData(
MIN_BATTERY_LEVEL_CHARGING -> minBatteryLevelCharging = reader.nextInt()
MIN_BATTERY_LEVEL_MOBILE -> minBatteryLevelMobile = reader.nextInt()
SORT -> sort = reader.nextInt()
NETWORKS -> networks = ServerCategoryNetworkId.parseList(reader)
else -> reader.skipValue()
}
}
@ -518,7 +524,8 @@ data class ServerUpdatedCategoryBaseData(
timeWarnings = timeWarnings,
minBatteryLevelCharging = minBatteryLevelCharging,
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(
val categoryId: String,
val assignedApps: List<String>,

View file

@ -25,6 +25,8 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import io.timelimit.android.R
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.logic.DefaultAppLogic
import io.timelimit.android.sync.websocket.NetworkStatus
@ -35,14 +37,14 @@ class DiagnoseConnectionFragment : Fragment(), FragmentWithCustomTitle {
val binding = FragmentDiagnoseConnectionBinding.inflate(inflater, container, false)
val logic = DefaultAppLogic.with(context!!)
logic.networkStatus.observe(this, Observer {
logic.networkStatus.observe(viewLifecycleOwner, Observer {
binding.generalStatus = getString(when (it!!) {
NetworkStatus.Online -> R.string.diagnose_connection_yes
NetworkStatus.Offline -> R.string.diagnose_connection_no
})
})
logic.isConnected.observe(this, Observer {
logic.isConnected.observe(viewLifecycleOwner, Observer {
binding.ownServerStatus = getString(if (it == true)
R.string.diagnose_connection_yes
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
}

View file

@ -15,15 +15,19 @@
*/
package io.timelimit.android.ui.lock
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.database.sqlite.SQLiteConstraintException
import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import io.timelimit.android.R
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.date.DateInTimezone
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.logic.*
import io.timelimit.android.logic.blockingreason.AppBaseHandling
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.IncrementCategoryExtraTimeAction
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.AuthenticationFab
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.advanced.managedisabletimelimits.ManageDisableTimelimitsViewHelper
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_ACTIVITY = "activitiy"
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 {
val result = LockFragment()
@ -96,6 +105,12 @@ class LockFragment : Fragment() {
private val batteryStatus: LiveData<BatteryStatus> by lazy {
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 val handlingCache = CategoryHandlingCache()
private val realTime = RealTime.newInstance()
@ -115,6 +130,8 @@ class LockFragment : Fragment() {
private fun update() {
val deviceAndUserRelatedData = deviceAndUserRelatedData.value ?: return
val batteryStatus = batteryStatus.value ?: return
val hasPremiumOrLocalMode = hasPremiumOrLocalMode.value ?: return
val networkId = networkIdLive.value
logic.realTimeLogic.getRealTime(realTime)
@ -125,14 +142,6 @@ class LockFragment : Fragment() {
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(
foregroundAppPackageName = packageName,
foregroundAppActivityName = activityName,
@ -142,6 +151,24 @@ class LockFragment : Fragment() {
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)
activityName?.removePrefix(packageName)
else
@ -278,6 +305,10 @@ class LockFragment : Fragment() {
}
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?) {
super.onCreate(savedInstanceState)
@ -411,6 +448,8 @@ class LockFragment : Fragment() {
deviceAndUserRelatedData.observe(viewLifecycleOwner, Observer { update() })
batteryStatus.observe(viewLifecycleOwner, Observer { update() })
networkIdLive.observe(viewLifecycleOwner, Observer { update() })
hasPremiumOrLocalMode.observe(viewLifecycleOwner, Observer { update() })
binding.packageName = packageName
@ -418,6 +457,7 @@ class LockFragment : Fragment() {
binding.appIcon.setImageDrawable(logic.platformIntegration.getAppIcon(packageName))
initExtraTimeView()
initGrantPermissionView()
return binding.root
}
@ -441,6 +481,12 @@ class LockFragment : Fragment() {
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 {
@ -452,4 +498,5 @@ interface Handlers {
fun disableTemporarilyLockForAllCategories()
fun showAuthenticationScreen()
fun setThisDeviceAsCurrentDevice()
fun requestLocationPermission()
}

View file

@ -73,7 +73,9 @@ object AllowUserLoginStatusUtil {
assumeCurrentDevice = true,
timeInMillis = time.timeInMillis,
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)

View file

@ -329,6 +329,7 @@ class NewLoginFragment: DialogFragment() {
BlockingReason.NotificationsAreBlocked -> getString(R.string.lock_reason_short_notification_blocking)
BlockingReason.BatteryLimit -> getString(R.string.lock_reason_short_battery_limit)
BlockingReason.SessionDurationLimit -> getString(R.string.lock_reason_short_session_duration)
BlockingReason.MissingRequiredNetwork -> getString(R.string.lock_reason_short_missing_required_network)
BlockingReason.NotPartOfAnCategory -> "???"
BlockingReason.None -> "???"
}

View file

@ -15,10 +15,12 @@
*/
package io.timelimit.android.ui.manage.category.settings
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
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.manage.category.ManageCategoryFragmentArgs
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.view.SelectTimeSpanViewListener
class CategorySettingsFragment : Fragment() {
companion object {
private const val PERMISSION_REQUEST_CODE = 1
fun newInstance(params: ManageCategoryFragmentArgs): CategorySettingsFragment {
val result = CategorySettingsFragment()
result.arguments = params.toBundle()
@ -117,6 +122,16 @@ class CategorySettingsFragment : Fragment() {
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.editCategoryTitleGo.setOnClickListener { renameCategory() }
binding.addUsedTimeBtn.setOnClickListener { addUsedTime() }
@ -231,4 +246,12 @@ class CategorySettingsFragment : Fragment() {
).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,
batteryStatus = logic.platformIntegration.getBatteryStatus(),
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
assumeCurrentDevice = true
assumeCurrentDevice = true,
currentNetworkId = null, // not relevant here
hasPremiumOrLocalMode = false // not relevant here
)
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"
layout="@layout/category_notification_filter" />
<include android:id="@+id/networks"
layout="@layout/manage_category_networks_view" />
<androidx.cardview.widget.CardView
app:cardUseCompatPadding="true"
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
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License.
@ -25,45 +25,82 @@
<variable
name="ownServerStatus"
type="String" />
<variable
name="networkId"
type="String" />
</data>
<ScrollView
android:id="@+id/scroll"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.cardview.widget.CardView
android:layout_margin="8dp"
app:cardUseCompatPadding="true"
<LinearLayout
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:padding="8dp"
android:orientation="vertical"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.cardview.widget.CardView
app:cardUseCompatPadding="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:text="@string/diagnose_connection_title"
android:textAppearance="?android:textAppearanceLarge"
<LinearLayout
android:padding="8dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="wrap_content">
<TextView
tools:text="@string/diagnose_connection_general"
android:text="@{@string/diagnose_connection_general(generalStatus)}"
android:textAppearance="?android:textAppearanceMedium"
<TextView
android:text="@string/diagnose_connection_title"
android:textAppearance="?android:textAppearanceLarge"
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_height="wrap_content" />
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<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" />
<TextView
android:text="@string/diagnose_connection_network_id"
android:textAppearance="?android:textAppearanceLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<TextView
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>
</layout>

View file

@ -51,6 +51,10 @@
name="blockedKindLabel"
type="String" />
<variable
name="missingNetworkIdPermission"
type="boolean" />
<import type="android.view.View" />
<import type="io.timelimit.android.logic.BlockingReason" />
<import type="android.text.TextUtils" />
@ -281,11 +285,11 @@
tools:ignore="UnusedAttribute"
android:drawablePadding="16dp"
android:drawableTint="?colorOnSurface"
android:drawableStart="@drawable/ic_pause_circle_outline_black_24dp"
android:visibility="@{reason == BlockingReason.SessionDurationLimit ? View.VISIBLE : View.GONE}"
android:drawableStart="@drawable/ic_baseline_wifi_lock_24"
android:visibility="@{reason == BlockingReason.MissingRequiredNetwork ? View.VISIBLE : View.GONE}"
android:textAppearance="?android:textAppearanceMedium"
android:text="@{@string/lock_reason_session_duration(blockedKindLabel)}"
tools:text="@string/lock_reason_session_duration"
android:text="@{@string/lock_reason_missing_required_network(blockedKindLabel)}"
tools:text="@string/lock_reason_missing_required_network"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
@ -566,6 +570,34 @@
</LinearLayout>
</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
android:foreground="?selectableItemBackground"
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>
<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>

View file

@ -34,6 +34,7 @@
<string name="diagnose_connection_own_server">TimeLimit-Server: %s</string>
<string name="diagnose_connection_yes">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_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_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">
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>
@ -84,6 +87,10 @@
Für diese %s gibt es eine Sitzungsdauerbegrenzung.
Nach dem Ablauf der Pausenzeit wird die Sperre aufgehoben.
</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_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_battery_limit">Akkulimit unterschritten</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">
Ö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>
<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>

View file

@ -34,6 +34,7 @@
<string name="diagnose_connection_own_server">TimeLimit Server: %s</string>
<string name="diagnose_connection_yes">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_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_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">
This App was not assigned to any category.
Due to that, there are no restriction settings.
@ -89,6 +92,9 @@
and this limit was reached.
It will be unlocked after the break duration.
</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_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_battery_limit">battery 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">
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>