mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +02:00
Add option to limit categories depending on the current network
This commit is contained in:
parent
1387ef86e2
commit
20ca6bcc91
49 changed files with 2265 additions and 92 deletions
1115
app/schemas/io.timelimit.android.data.RoomDatabase/32.json
Normal file
1115
app/schemas/io.timelimit.android.data.RoomDatabase/32.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -33,6 +33,9 @@
|
|||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.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" />
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 )")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import io.timelimit.android.data.model.CategoryNetworkId
|
||||
|
||||
@Dao
|
||||
interface CategoryNetworkIdDao {
|
||||
@Query("SELECT * FROM category_network_id LIMIT :pageSize OFFSET :offset")
|
||||
fun getPageSync(offset: Int, pageSize: Int): List<CategoryNetworkId>
|
||||
|
||||
@Query("SELECT * FROM category_network_id WHERE category_id = :categoryId")
|
||||
fun getByCategoryIdLive(categoryId: String): LiveData<List<CategoryNetworkId>>
|
||||
|
||||
@Query("SELECT * FROM category_network_id WHERE category_id = :categoryId")
|
||||
fun getByCategoryIdSync(categoryId: String): List<CategoryNetworkId>
|
||||
|
||||
@Query("SELECT * FROM category_network_id WHERE category_id = :categoryId AND network_item_id = :itemId")
|
||||
fun getByCategoryIdAndItemIdSync(categoryId: String, itemId: String): CategoryNetworkId?
|
||||
|
||||
@Query("SELECT COUNT(*) FROM category_network_id WHERE category_id = :categoryId")
|
||||
fun countByCategoryIdSync(categoryId: String): Long
|
||||
|
||||
@Insert
|
||||
fun insertItemsSync(items: List<CategoryNetworkId>)
|
||||
|
||||
@Insert
|
||||
fun insertItemSync(item: CategoryNetworkId)
|
||||
|
||||
@Query("DELETE FROM category_network_id WHERE category_id = :categoryId")
|
||||
fun deleteByCategoryId(categoryId: String)
|
||||
}
|
|
@ -32,7 +32,8 @@ enum class Table {
|
|||
UsedTimeItem,
|
||||
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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.data.model
|
||||
|
||||
import android.util.JsonReader
|
||||
import android.util.JsonWriter
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import io.timelimit.android.crypto.HexString
|
||||
import io.timelimit.android.crypto.Sha512
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
|
||||
@Entity(
|
||||
tableName = "category_network_id",
|
||||
primaryKeys = ["category_id", "network_item_id"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Category::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["category_id"],
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
data class CategoryNetworkId(
|
||||
@ColumnInfo(name = "category_id")
|
||||
val categoryId: String,
|
||||
@ColumnInfo(name = "network_item_id")
|
||||
val networkItemId: String,
|
||||
@ColumnInfo(name = "hashed_network_id")
|
||||
val hashedNetworkId: String
|
||||
): JsonSerializable {
|
||||
companion object {
|
||||
private const val CATEGORY_ID = "categoryId"
|
||||
private const val NETWORK_ITEM_ID = "networkItemId"
|
||||
private const val HASHED_NETWORK_ID = "hashedNetworkId"
|
||||
const val ANONYMIZED_NETWORK_ID_LENGTH = 8
|
||||
const val MAX_ITEMS = 8
|
||||
|
||||
fun anonymizeNetworkId(itemId: String, networkId: String) = Sha512.hashSync(itemId + networkId).substring(0, ANONYMIZED_NETWORK_ID_LENGTH)
|
||||
|
||||
fun parse(reader: JsonReader): CategoryNetworkId {
|
||||
var categoryId: String? = null
|
||||
var networkItemId: String? = null
|
||||
var hashedNetworkId: String? = null
|
||||
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
CATEGORY_ID -> categoryId = reader.nextString()
|
||||
NETWORK_ITEM_ID -> networkItemId = reader.nextString()
|
||||
HASHED_NETWORK_ID -> hashedNetworkId = reader.nextString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
return CategoryNetworkId(
|
||||
categoryId = categoryId!!,
|
||||
networkItemId = networkItemId!!,
|
||||
hashedNetworkId = hashedNetworkId!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
IdGenerator.assertIdValid(categoryId)
|
||||
IdGenerator.assertIdValid(networkItemId)
|
||||
if (hashedNetworkId.length != ANONYMIZED_NETWORK_ID_LENGTH) throw IllegalArgumentException()
|
||||
HexString.assertIsHexString(hashedNetworkId)
|
||||
}
|
||||
|
||||
override fun serialize(writer: JsonWriter) {
|
||||
writer.beginObject()
|
||||
|
||||
writer.name(CATEGORY_ID).value(categoryId)
|
||||
writer.name(NETWORK_ITEM_ID).value(networkItemId)
|
||||
writer.name(HASHED_NETWORK_ID).value(hashedNetworkId)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
|
@ -17,28 +17,28 @@
|
|||
package io.timelimit.android.data.model.derived
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.integration.platform.android
|
||||
|
||||
import android.content.Context
|
||||
import android.net.wifi.WifiInfo
|
||||
import android.net.wifi.WifiManager
|
||||
import io.timelimit.android.crypto.Sha512
|
||||
import io.timelimit.android.integration.platform.NetworkId
|
||||
|
||||
class ConnectedNetworkUtil (context: Context) {
|
||||
private val workContext = context.applicationContext
|
||||
private val wifiManager = workContext.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||
|
||||
fun getNetworkId(): NetworkId {
|
||||
val info: WifiInfo? = wifiManager.connectionInfo
|
||||
|
||||
info ?: return NetworkId.NoNetworkConnected
|
||||
|
||||
val ssid: String? = info.ssid
|
||||
val bssid: String? = info.bssid
|
||||
|
||||
if (ssid == null || bssid == null) return NetworkId.NoNetworkConnected
|
||||
if (ssid == WifiManager.UNKNOWN_SSID) return NetworkId.MissingPermission
|
||||
|
||||
return NetworkId.Network(Sha512.hashSync(ssid + bssid).substring(0, 16))
|
||||
}
|
||||
}
|
|
@ -31,6 +31,8 @@ import io.timelimit.android.async.Threads
|
|||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.coroutines.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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -175,4 +175,6 @@ class DummyIntegration(
|
|||
override fun setForceNetworkTime(enable: Boolean) = Unit
|
||||
|
||||
override fun restartApp() = Unit
|
||||
|
||||
override fun getCurrentNetworkId(): NetworkId = NetworkId.NoNetworkConnected
|
||||
}
|
||||
|
|
|
@ -31,9 +31,11 @@ import io.timelimit.android.date.DateInTimezone
|
|||
import io.timelimit.android.integration.platform.AppStatusMessage
|
||||
import io.timelimit.android.integration.platform.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 ||
|
||||
|
|
|
@ -32,7 +32,8 @@ enum class BlockingReason {
|
|||
RequiresCurrentDevice,
|
||||
NotificationsAreBlocked,
|
||||
BatteryLimit,
|
||||
SessionDurationLimit
|
||||
SessionDurationLimit,
|
||||
MissingRequiredNetwork
|
||||
}
|
||||
|
||||
enum class BlockingLevel {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -70,6 +70,8 @@ object ActionParser {
|
|||
// UpdateCategorySorting
|
||||
// UpdateUserFlagsAction
|
||||
// UpdateUserLimitLoginCategory
|
||||
// AddCategoryNetworkId
|
||||
// ResetCategoryNetworkIds
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -729,6 +729,30 @@ object LocalDatabaseParentActionDispatcher {
|
|||
)
|
||||
) }
|
||||
}
|
||||
is AddCategoryNetworkId -> {
|
||||
DatabaseValidation.assertCategoryExists(database, action.categoryId)
|
||||
|
||||
val count = database.categoryNetworkId().countByCategoryIdSync(action.categoryId)
|
||||
|
||||
if (count + 1 > CategoryNetworkId.MAX_ITEMS) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
val oldItem = database.categoryNetworkId().getByCategoryIdAndItemIdSync(categoryId = action.categoryId, itemId = action.itemId)
|
||||
|
||||
if (oldItem != null) {
|
||||
throw IllegalArgumentException("id already used")
|
||||
}
|
||||
|
||||
database.categoryNetworkId().insertItemSync(CategoryNetworkId(
|
||||
categoryId = action.categoryId,
|
||||
networkItemId = action.itemId,
|
||||
hashedNetworkId = action.hashedNetworkId
|
||||
))
|
||||
}
|
||||
is ResetCategoryNetworkIds -> {
|
||||
database.categoryNetworkId().deleteByCategoryId(categoryId = action.categoryId)
|
||||
}
|
||||
}.let { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 -> "???"
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.category.settings.networks
|
||||
|
||||
import android.Manifest
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.model.CategoryNetworkId
|
||||
import io.timelimit.android.databinding.ManageCategoryNetworksViewBinding
|
||||
import io.timelimit.android.integration.platform.NetworkId
|
||||
import io.timelimit.android.livedata.liveDataFromFunction
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.switchMap
|
||||
import io.timelimit.android.sync.actions.AddCategoryNetworkId
|
||||
import io.timelimit.android.sync.actions.ResetCategoryNetworkIds
|
||||
import io.timelimit.android.ui.help.HelpDialogFragment
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
|
||||
|
||||
object ManageCategoryNetworksView {
|
||||
fun bind(
|
||||
view: ManageCategoryNetworksViewBinding,
|
||||
auth: ActivityViewModel,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
fragmentManager: FragmentManager,
|
||||
categoryId: String,
|
||||
fragment: Fragment,
|
||||
permissionRequestCode: Int
|
||||
) {
|
||||
fun networkId(): NetworkId = auth.logic.platformIntegration.getCurrentNetworkId()
|
||||
|
||||
val context = view.root.context
|
||||
val networkIdLive = liveDataFromFunction { networkId() }
|
||||
val networksLive = auth.database.categoryNetworkId().getByCategoryIdLive(categoryId)
|
||||
val isFullVersionLive = auth.logic.fullVersion.shouldProvideFullVersionFunctions
|
||||
|
||||
view.titleView.setOnClickListener {
|
||||
HelpDialogFragment.newInstance(
|
||||
title = R.string.category_networks_title,
|
||||
text = R.string.category_networks_help
|
||||
).show(fragmentManager)
|
||||
}
|
||||
|
||||
networksLive.switchMap { networks ->
|
||||
networkIdLive.map { networkId ->
|
||||
networks to networkId
|
||||
}
|
||||
}.observe(lifecycleOwner, Observer { (networks, networkId) ->
|
||||
view.showRemoveNetworksButton = networks.isNotEmpty()
|
||||
|
||||
view.addedNetworksText = if (networks.isEmpty())
|
||||
context.getString(R.string.category_networks_empty)
|
||||
else
|
||||
context.getString(
|
||||
R.string.category_networks_not_empty,
|
||||
context.resources.getQuantityString(R.plurals.category_networks_counter, networks.size, networks.size)
|
||||
)
|
||||
|
||||
view.status = when (networkId) {
|
||||
NetworkId.MissingPermission -> NetworkStatus.MissingPermission
|
||||
NetworkId.NoNetworkConnected -> NetworkStatus.NoneConnected
|
||||
is NetworkId.Network -> {
|
||||
val hasItem = networks.find {item ->
|
||||
CategoryNetworkId.anonymizeNetworkId(networkId = networkId.id, itemId = item.networkItemId) == item.hashedNetworkId
|
||||
} != null
|
||||
|
||||
if (hasItem)
|
||||
NetworkStatus.ConnectedAndAdded
|
||||
else if (networks.size + 1 > CategoryNetworkId.MAX_ITEMS)
|
||||
NetworkStatus.ConnectedNotAddedButFull
|
||||
else
|
||||
NetworkStatus.ConnectedButNotAdded
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
view.removeBtn.setOnClickListener {
|
||||
val oldList = networksLive.value ?: return@setOnClickListener
|
||||
|
||||
if (
|
||||
auth.tryDispatchParentAction(
|
||||
ResetCategoryNetworkIds(categoryId = categoryId)
|
||||
)
|
||||
) {
|
||||
Snackbar.make(view.root, R.string.category_networks_toast_all_removed, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.generic_undo) {
|
||||
val isEmpty = networksLive.value?.isEmpty() ?: false
|
||||
|
||||
if (isEmpty) {
|
||||
auth.tryDispatchParentActions(
|
||||
oldList.map { item ->
|
||||
AddCategoryNetworkId(
|
||||
categoryId = item.categoryId,
|
||||
itemId = item.networkItemId,
|
||||
hashedNetworkId = item.hashedNetworkId
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
view.grantPermissionButton.setOnClickListener {
|
||||
RequestWifiPermission.doRequest(fragment, permissionRequestCode)
|
||||
}
|
||||
|
||||
isFullVersionLive.observe(lifecycleOwner, Observer { isFullVersion ->
|
||||
view.addNetworkButton.setOnClickListener {
|
||||
if (isFullVersion) {
|
||||
val itemId = IdGenerator.generateId()
|
||||
val networkId = networkId()
|
||||
|
||||
if (!(networkId is NetworkId.Network)) return@setOnClickListener
|
||||
|
||||
auth.tryDispatchParentAction(
|
||||
AddCategoryNetworkId(
|
||||
categoryId = categoryId,
|
||||
itemId = itemId,
|
||||
hashedNetworkId = CategoryNetworkId.anonymizeNetworkId(itemId = itemId, networkId = networkId.id)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
RequiresPurchaseDialogFragment().show(fragmentManager)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
enum class NetworkStatus {
|
||||
MissingPermission,
|
||||
NoneConnected,
|
||||
ConnectedButNotAdded,
|
||||
ConnectedNotAddedButFull,
|
||||
ConnectedAndAdded
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.ui.manage.category.settings.networks
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.timelimit.android.R
|
||||
|
||||
object RequestWifiPermission {
|
||||
private fun isLocationEnabled(context: Context): Boolean = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
|
||||
Settings.Secure.getInt(context.contentResolver, Settings.Secure.LOCATION_MODE) == Settings.Secure.LOCATION_MODE_OFF
|
||||
else {
|
||||
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
||||
|
||||
locationManager.isLocationEnabled
|
||||
}
|
||||
|
||||
fun doRequest(fragment: Fragment, permissionRequestCode: Int) {
|
||||
if (ContextCompat.checkSelfPermission(fragment.requireContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||
fragment.requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), permissionRequestCode)
|
||||
} else if (!isLocationEnabled(fragment.requireContext())) {
|
||||
Toast.makeText(fragment.requireContext(), R.string.category_networks_toast_enable_location_service, Toast.LENGTH_SHORT).show()
|
||||
|
||||
fragment.startActivity(
|
||||
Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
} else {
|
||||
Toast.makeText(fragment.requireContext(), R.string.error_general, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -82,7 +82,9 @@ object TimesWidgetItems {
|
|||
timeInMillis = realTime.timeInMillis,
|
||||
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
|
||||
|
|
10
app/src/main/res/drawable/ic_baseline_wifi_lock_24.xml
Normal file
10
app/src/main/res/drawable/ic_baseline_wifi_lock_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20.5,9.5c0.28,0 0.55,0.04 0.81,0.08L24,6c-3.34,-2.51 -7.5,-4 -12,-4S3.34,3.49 0,6l12,16 3.5,-4.67L15.5,14.5c0,-2.76 2.24,-5 5,-5zM23,16v-1.5c0,-1.38 -1.12,-2.5 -2.5,-2.5S18,13.12 18,14.5L18,16c-0.55,0 -1,0.45 -1,1v4c0,0.55 0.45,1 1,1h5c0.55,0 1,-0.45 1,-1v-4c0,-0.55 -0.45,-1 -1,-1zM22,16h-3v-1.5c0,-0.83 0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5L22,16z"/>
|
||||
</vector>
|
|
@ -95,6 +95,9 @@
|
|||
<include android:id="@+id/notification_filter"
|
||||
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"
|
||||
|
|
|
@ -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,14 +25,23 @@
|
|||
<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">
|
||||
<LinearLayout
|
||||
android:padding="8dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_margin="8dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
@ -64,6 +73,34 @@
|
|||
|
||||
</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:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:text="@string/diagnose_connection_network_id"
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<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>
|
||||
|
|
|
@ -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 && 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()}"
|
||||
|
|
135
app/src/main/res/layout/manage_category_networks_view.xml
Normal file
135
app/src/main/res/layout/manage_category_networks_view.xml
Normal file
|
@ -0,0 +1,135 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="status"
|
||||
type="NetworkStatus" />
|
||||
|
||||
<variable
|
||||
name="addedNetworksText"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="showRemoveNetworksButton"
|
||||
type="boolean" />
|
||||
|
||||
<import type="android.view.View" />
|
||||
<import type="io.timelimit.android.ui.manage.category.settings.networks.ManageCategoryNetworksView.NetworkStatus" />
|
||||
</data>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
app:cardUseCompatPadding="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<LinearLayout
|
||||
android:padding="8dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
tools:ignore="UnusedAttribute"
|
||||
android:drawableTint="?colorOnSurface"
|
||||
android:id="@+id/title_view"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawableEnd="@drawable/ic_info_outline_black_24dp"
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
android:text="@string/category_networks_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
tools:text="@string/category_networks_empty"
|
||||
android:text="@{addedNetworksText}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:visibility="@{showRemoveNetworksButton ? View.VISIBLE : View.GONE}"
|
||||
android:id="@+id/remove_btn"
|
||||
android:layout_gravity="end"
|
||||
android:text="@string/category_networks_action_remove"
|
||||
style="?borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:visibility="@{status == NetworkStatus.MissingPermission ? View.VISIBLE : View.GONE}"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@string/category_networks_status_missing_permission"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:visibility="@{status == NetworkStatus.NoneConnected ? View.VISIBLE : View.GONE}"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@string/category_networks_status_none_connected"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:visibility="@{status == NetworkStatus.ConnectedButNotAdded ? View.VISIBLE : View.GONE}"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@string/category_networks_status_connected_no_match"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:visibility="@{status == NetworkStatus.ConnectedNotAddedButFull ? View.VISIBLE : View.GONE}"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@string/category_networks_status_connected_no_match_full"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:visibility="@{status == NetworkStatus.ConnectedAndAdded ? View.VISIBLE : View.GONE}"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:text="@string/category_networks_status_connected_has_match"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/grant_permission_button"
|
||||
android:visibility="@{status == NetworkStatus.MissingPermission ? View.VISIBLE : View.GONE}"
|
||||
style="?materialButtonOutlinedStyle"
|
||||
android:text="@string/category_networks_action_grant"
|
||||
android:layout_gravity="end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/add_network_button"
|
||||
android:visibility="@{status == NetworkStatus.ConnectedButNotAdded ? View.VISIBLE : View.GONE}"
|
||||
style="?materialButtonOutlinedStyle"
|
||||
android:text="@string/category_networks_action_add"
|
||||
android:layout_gravity="end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
android:text="@string/purchase_required_info_local_mode_free"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</layout>
|
|
@ -32,4 +32,5 @@
|
|||
</plurals>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<resources>
|
||||
<string name="category_networks_title">Netzwerke</string>
|
||||
<string name="category_networks_help">Hier können WLAN-Drahtlosnetzwerke hinzugefügt werden.
|
||||
Diese Kategorie wird gesperrt, wenn Netzwerke hinzugefügt wurden und es keine
|
||||
Verbindung zu einem der angegebenen Netzwerke gibt.
|
||||
Ein Netzwerk wird durch einen Access Point identifiziert. Im Fall von mehreren Access Point/ Repeatern
|
||||
müssen alle einzeln hinzugefügt werden.
|
||||
Das vorübergehende Deaktivieren von Zeitbegrenzungen deaktiviert auch die Beschränkungen durch diese Funktion.
|
||||
</string>
|
||||
<string name="category_networks_empty">Sie haben keine Netzwerke hinzugefügt. Diese Kategorie wird
|
||||
nicht abhängig vom Netzwerk gesperrt.
|
||||
</string>
|
||||
<string name="category_networks_not_empty">Es wurden %s hinzugefügt. Diese Kategorie kann nur in den
|
||||
angegebenen Netzwerken verwendet werden.</string>
|
||||
<plurals name="category_networks_counter">
|
||||
<item quantity="one">%d Netzwerk</item>
|
||||
<item quantity="other">%d Netzwerke</item>
|
||||
</plurals>
|
||||
<string name="category_networks_status_missing_permission">Die Berechtigung zum Abrufen vom aktuellen Netzwerk fehlt</string>
|
||||
<string name="category_networks_status_none_connected">Sie sind mit keinem WLAN verbunden</string>
|
||||
<string name="category_networks_status_connected_no_match">Das aktuelle WLAN wurde nicht hinzugefügt</string>
|
||||
<string name="category_networks_status_connected_no_match_full">Das aktuelle WLAN wurde nicht hinzugefügt,
|
||||
aber es können keine weiteren WLANs hinzugefügt werden
|
||||
</string>
|
||||
<string name="category_networks_status_connected_has_match">Das aktuelle WLAN wurde hinzugefügt</string>
|
||||
<string name="category_networks_action_add">Netzwerk hinzufügen</string>
|
||||
<string name="category_networks_action_grant">Berechtigung erteilen</string>
|
||||
<string name="category_networks_action_remove">Alle Netzwerke entfernen</string>
|
||||
<string name="category_networks_toast_all_removed">alle Netzwerke entfernt</string>
|
||||
|
||||
<string name="category_networks_toast_enable_location_service">Bitte den Standortzugriff aktivieren</string>
|
||||
</resources>
|
|
@ -32,4 +32,5 @@
|
|||
</plurals>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
43
app/src/main/res/values/strings-manage-category-networks.xml
Normal file
43
app/src/main/res/values/strings-manage-category-networks.xml
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<resources>
|
||||
<string name="category_networks_title">Networks</string>
|
||||
<string name="category_networks_help">Here you can add WiFi networks. This category will be blocked
|
||||
if there are WiFi networks added and there is no connection to a specified network.
|
||||
A network is identified by an access point. In case of multiple access points/ repeaters,
|
||||
you have to add all of them one by one.
|
||||
Disabling the time limits temporarily additionally disables limitations caused by this feature.
|
||||
</string>
|
||||
<string name="category_networks_empty">You did not add any networks. This category is not limited to specific networks.</string>
|
||||
<string name="category_networks_not_empty">You added %s. This category is limited to the specified networks.</string>
|
||||
<plurals name="category_networks_counter">
|
||||
<item quantity="one">%d network</item>
|
||||
<item quantity="other">%d networks</item>
|
||||
</plurals>
|
||||
<string name="category_networks_status_missing_permission">The permission to get the connected network is missing</string>
|
||||
<string name="category_networks_status_none_connected">You are not connected to any WiFi network</string>
|
||||
<string name="category_networks_status_connected_no_match">You are connected to a network which was not added</string>
|
||||
<string name="category_networks_status_connected_no_match_full">You are connected to a network which was not added,
|
||||
but you can not add more networks to this category
|
||||
</string>
|
||||
<string name="category_networks_status_connected_has_match">You are connected to a network which was added</string>
|
||||
<string name="category_networks_action_add">Add network</string>
|
||||
<string name="category_networks_action_grant">Grant permission</string>
|
||||
<string name="category_networks_action_remove">Remove all networks</string>
|
||||
<string name="category_networks_toast_all_removed">All networks removed</string>
|
||||
|
||||
<string name="category_networks_toast_enable_location_service">Please enable location access</string>
|
||||
</resources>
|
Loading…
Add table
Add a link
Reference in a new issue