Add option to block Apps at system level

This commit is contained in:
Jonas Lochmann 2019-08-19 00:00:00 +00:00
parent 3fbf598eb5
commit 03bd3a5100
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
13 changed files with 427 additions and 18 deletions

View file

@ -223,7 +223,8 @@ abstract class ConfigDao {
fun getEnableAlternativeDurationSelectionAsync() = getValueOfKeyAsync(ConfigurationItemType.EnableAlternativeDurationSelection).map { it == "1" }
fun setEnableAlternativeDurationSelectionSync(enable: Boolean) = updateValueSync(ConfigurationItemType.EnableAlternativeDurationSelection, if (enable) "1" else "0")
fun getExperimentalFlagsLive(): LiveData<Long> {
protected fun getExperimentalFlagsLive(): LiveData<Long> {
return getValueOfKeyAsync(ConfigurationItemType.ExperimentalFlags).map {
if (it == null) {
0
@ -233,6 +234,8 @@ abstract class ConfigDao {
}
}
val experimentalFlags: LiveData<Long> by lazy { getExperimentalFlagsLive() }
private fun getExperimentalFlagsSync(): Long {
val v = getValueOfKeySync(ConfigurationItemType.ExperimentalFlags)
@ -243,7 +246,7 @@ abstract class ConfigDao {
}
}
fun isExperimentalFlagsSetAsync(flags: Long) = getExperimentalFlagsLive().map {
fun isExperimentalFlagsSetAsync(flags: Long) = experimentalFlags.map {
(it and flags) == flags
}.ignoreUnchanged()
@ -253,9 +256,9 @@ abstract class ConfigDao {
updateValueSync(
ConfigurationItemType.ExperimentalFlags,
if (enable)
(getShownHintsSync() or flags).toString(16)
(getExperimentalFlagsSync() or flags).toString(16)
else
(getShownHintsSync() and (flags.inv())).toString(16)
(getExperimentalFlagsSync() and (flags.inv())).toString(16)
)
}
}

View file

@ -191,4 +191,5 @@ object HintsToShow {
object ExperimentalFlags {
const val DISABLE_BLOCK_ON_MANIPULATION = 1L
const val SYSTEM_LEVEL_BLOCKING = 2L
}

View file

@ -52,6 +52,12 @@ object AndroidIntegrationApps {
ignoredApps["com.google.android.packageinstaller"] = AndroidIntegrationApps.IgnoredAppHandling.IgnoreOnStoreOtherwiseWhitelistAndDontDisable
}
private val ignoredActivities = setOf<String>(
"com.android.settings:com.android.settings.enterprise.ActionDisabledByAdminDialog"
)
fun shouldIgnoreActivity(packageName: String, activityName: String) = ignoredActivities.contains("$packageName:$activityName")
fun getLocalApps(deviceId: String, context: Context): Collection<App> {
val packageManager = context.packageManager

View file

@ -99,6 +99,7 @@ class AppLogic(
}
val manipulationLogic = ManipulationLogic(this)
val suspendAppsLogic = SuspendAppsLogic(this)
fun shutdown() {
enable.value = false

View file

@ -296,7 +296,9 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
AndroidIntegrationApps.IgnoredAppHandling.Ignore -> true
AndroidIntegrationApps.IgnoredAppHandling.IgnoreOnStoreOtherwiseWhitelistAndDontDisable -> BuildConfig.storeCompilant
}
})
}) ||
(foregroundAppPackageName != null && foregroundAppActivityName != null &&
AndroidIntegrationApps.shouldIgnoreActivity(foregroundAppPackageName, foregroundAppActivityName))
) {
usedTimeUpdateHelper?.commit(appLogic)
showStatusMessageWithCurrentAppTitle(

View file

@ -357,7 +357,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
}
}
private fun getTemporarilyTrustedTimeInMillis(): LiveData<Long?> {
fun getTemporarilyTrustedTimeInMillis(): LiveData<Long?> {
val realTime = RealTime.newInstance()
return liveDataFromFunction {
@ -417,7 +417,7 @@ class BlockingReasonUtil(private val appLogic: AppLogic) {
}.ignoreUnchanged()
}
private fun getTrustedDateLive(timeZone: TimeZone): LiveData<DateInTimezone?> {
fun getTrustedDateLive(timeZone: TimeZone): LiveData<DateInTimezone?> {
val realTime = RealTime.newInstance()
return object: LiveData<DateInTimezone?>() {

View file

@ -0,0 +1,221 @@
/*
* TimeLimit Copyright <C> 2019 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.logic
import android.util.Log
import android.util.SparseLongArray
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import io.timelimit.android.BuildConfig
import io.timelimit.android.data.model.*
import io.timelimit.android.date.DateInTimezone
import io.timelimit.android.livedata.*
import java.util.*
class CategoriesBlockingReasonUtil(private val appLogic: AppLogic) {
companion object {
private const val LOG_TAG = "CategoryBlockingReason"
}
private val blockingReason = BlockingReasonUtil(appLogic)
private val temporarilyTrustedTimeInMillis = blockingReason.getTemporarilyTrustedTimeInMillis()
// NOTE: this ignores the current device rule
fun getCategoryBlockingReasons(
childDisableLimitsUntil: LiveData<Long>,
timeZone: LiveData<TimeZone>,
categories: List<Category>
): LiveData<Map<String, BlockingReason>> {
val result = MediatorLiveData<Map<String, BlockingReason>>()
val status = mutableMapOf<String, BlockingReason>()
val reasons = getCategoryBlockingReasonsInternal(childDisableLimitsUntil, timeZone, categories)
var missing = reasons.size
reasons.entries.forEach { (k, v) ->
var ready = false
result.addSource(v) { newStatus ->
status[k] = newStatus
if (!ready) {
ready = true
missing--
}
if (missing == 0) {
result.value = status.toMap()
}
}
}
return result
}
// NOTE: this ignores the current device rule
private fun getCategoryBlockingReasonsInternal(
childDisableLimitsUntil: LiveData<Long>,
timeZone: LiveData<TimeZone>,
categories: List<Category>
): Map<String, LiveData<BlockingReason>> {
val result = mutableMapOf<String, LiveData<BlockingReason>>()
val categoryById = categories.associateBy { it.id }
val areLimitsTemporarilyDisabled = areLimitsDisabled(
temporarilyTrustedTimeInMillis = temporarilyTrustedTimeInMillis,
childDisableLimitsUntil = childDisableLimitsUntil
)
val temporarilyTrustedMinuteOfWeek = timeZone.switchMap { timeZone ->
blockingReason.getTrustedMinuteOfWeekLive(timeZone)
}
val temporarilyTrustedDate = timeZone.switchMap { timeZone ->
blockingReason.getTrustedDateLive(timeZone)
}
fun handleCategory(categoryId: String) {
categoryById[categoryId]?.let { category ->
result[categoryId] = result[categoryId] ?: getCategoryBlockingReason(
category = liveDataFromValue(category),
temporarilyTrustedMinuteOfWeek = temporarilyTrustedMinuteOfWeek,
temporarilyTrustedDate = temporarilyTrustedDate,
areLimitsTemporarilyDisabled = areLimitsTemporarilyDisabled
)
}
}
categoryById.keys.forEach { handleCategory(it) }
return result
}
// NOTE: this ignores parent categories (otherwise would check parent category if category has no blocking reason)
// NOTE: this ignores the current device rule
private fun getCategoryBlockingReason(
category: LiveData<Category>,
temporarilyTrustedMinuteOfWeek: LiveData<Int?>,
temporarilyTrustedDate: LiveData<DateInTimezone?>,
areLimitsTemporarilyDisabled: LiveData<Boolean>
): LiveData<BlockingReason> {
return category.switchMap { category ->
if (category.temporarilyBlocked) {
liveDataFromValue(BlockingReason.TemporarilyBlocked)
} else {
areLimitsTemporarilyDisabled.switchMap { areLimitsTemporarilyDisabled ->
if (areLimitsTemporarilyDisabled) {
liveDataFromValue(BlockingReason.None)
} else {
checkCategoryBlockedTimeAreas(
temporarilyTrustedMinuteOfWeek = temporarilyTrustedMinuteOfWeek,
blockedMinutesInWeek = category.blockedMinutesInWeek.dataNotToModify
).switchMap { blockedTimeAreasReason ->
if (blockedTimeAreasReason != BlockingReason.None) {
liveDataFromValue(blockedTimeAreasReason)
} else {
checkCategoryTimeLimitRules(
temporarilyTrustedDate = temporarilyTrustedDate,
category = category,
rules = appLogic.database.timeLimitRules().getTimeLimitRulesByCategory(category.id)
)
}
}
}
}
}
}
}
private fun areLimitsDisabled(
temporarilyTrustedTimeInMillis: LiveData<Long?>,
childDisableLimitsUntil: LiveData<Long>
): LiveData<Boolean> = childDisableLimitsUntil.switchMap { childDisableLimitsUntil ->
if (childDisableLimitsUntil == 0L) {
liveDataFromValue(false)
} else {
temporarilyTrustedTimeInMillis.map {
trustedTimeInMillis ->
trustedTimeInMillis != null && childDisableLimitsUntil > trustedTimeInMillis
}.ignoreUnchanged()
}
}
private fun checkCategoryBlockedTimeAreas(blockedMinutesInWeek: BitSet, temporarilyTrustedMinuteOfWeek: LiveData<Int?>): LiveData<BlockingReason> {
if (blockedMinutesInWeek.isEmpty) {
return liveDataFromValue(BlockingReason.None)
} else {
return temporarilyTrustedMinuteOfWeek.map { temporarilyTrustedMinuteOfWeek ->
if (temporarilyTrustedMinuteOfWeek == null) {
BlockingReason.MissingNetworkTime
} else if (blockedMinutesInWeek[temporarilyTrustedMinuteOfWeek]) {
BlockingReason.BlockedAtThisTime
} else {
BlockingReason.None
}
}.ignoreUnchanged()
}
}
private fun checkCategoryTimeLimitRules(
temporarilyTrustedDate: LiveData<DateInTimezone?>,
rules: LiveData<List<TimeLimitRule>>,
category: Category
): LiveData<BlockingReason> = rules.switchMap { rules ->
if (rules.isEmpty()) {
liveDataFromValue(BlockingReason.None)
} else {
temporarilyTrustedDate.switchMap { temporarilyTrustedDate ->
if (temporarilyTrustedDate == null) {
liveDataFromValue(BlockingReason.MissingNetworkTime)
} else {
getBlockingReasonStep7(
category = category,
nowTrustedDate = temporarilyTrustedDate,
rules = rules
)
}
}
}
}
private fun getBlockingReasonStep7(category: Category, nowTrustedDate: DateInTimezone, rules: List<TimeLimitRule>): LiveData<BlockingReason> {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "step 7")
}
return appLogic.database.usedTimes().getUsedTimesOfWeek(category.id, nowTrustedDate.dayOfEpoch - nowTrustedDate.dayOfWeek).map { usedTimes ->
val usedTimesSparseArray = SparseLongArray()
for (i in 0..6) {
val usedTimesItem = usedTimes[i]?.usedMillis
usedTimesSparseArray.put(i, (if (usedTimesItem != null) usedTimesItem else 0))
}
val remaining = RemainingTime.getRemainingTime(nowTrustedDate.dayOfWeek, usedTimesSparseArray, rules, category.extraTimeInMillis)
if (remaining == null || remaining.includingExtraTime > 0) {
BlockingReason.None
} else {
if (category.extraTimeInMillis > 0) {
BlockingReason.TimeOverExtraTimeCanBeUsedLater
} else {
BlockingReason.TimeOver
}
}
}.ignoreUnchanged()
}
}

View file

@ -0,0 +1,163 @@
/*
* TimeLimit Copyright <C> 2019 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.logic
import androidx.lifecycle.LiveData
import io.timelimit.android.data.model.Category
import io.timelimit.android.data.model.CategoryApp
import io.timelimit.android.data.model.ExperimentalFlags
import io.timelimit.android.data.model.UserType
import io.timelimit.android.livedata.ignoreUnchanged
import io.timelimit.android.livedata.liveDataFromValue
import io.timelimit.android.livedata.map
import io.timelimit.android.livedata.switchMap
import java.util.*
class SuspendAppsLogic(private val appLogic: AppLogic) {
private val blockingAtActivityLevel = appLogic.deviceEntry.map { it?.enableActivityLevelBlocking ?: false }
private val blockingReasonUtil = CategoriesBlockingReasonUtil(appLogic)
private val knownInstalledApps = appLogic.deviceId.switchMap { deviceId ->
if (deviceId.isNullOrEmpty()) {
liveDataFromValue(emptyList())
} else {
appLogic.database.app().getAppsByDeviceIdAsync(deviceId).map { apps ->
apps.map { it.packageName }
}
}
}.ignoreUnchanged()
private val categoryData = appLogic.deviceUserEntry.switchMap { deviceUser ->
if (deviceUser?.type == UserType.Child) {
appLogic.database.category().getCategoriesByChildId(deviceUser.id).switchMap { categories ->
appLogic.database.categoryApp().getCategoryApps(categories.map { it.id }).map { categoryApps ->
RealCategoryData(
categoryForUnassignedApps = deviceUser.categoryForNotAssignedApps,
categories = categories,
categoryApps = categoryApps
) as CategoryData
}
}
} else {
liveDataFromValue(NoChildUser as CategoryData)
}
}.ignoreUnchanged()
private val categoryBlockingReasons: LiveData<Map<String, BlockingReason>?> = appLogic.deviceUserEntry.switchMap { deviceUser ->
if (deviceUser?.type == UserType.Child) {
appLogic.database.category().getCategoriesByChildId(deviceUser.id).switchMap { categories ->
blockingReasonUtil.getCategoryBlockingReasons(
childDisableLimitsUntil = liveDataFromValue(deviceUser.disableLimitsUntil),
timeZone = liveDataFromValue(TimeZone.getTimeZone(deviceUser.timeZone)),
categories = categories
) as LiveData<Map<String, BlockingReason>?>
}
} else {
liveDataFromValue(null as Map<String, BlockingReason>?)
}
}.ignoreUnchanged()
private val appsToBlock = categoryBlockingReasons.switchMap { blockingReasons ->
if (blockingReasons == null) {
liveDataFromValue(emptyList<String>())
} else {
categoryData.switchMap { categories ->
when (categories) {
is NoChildUser -> liveDataFromValue(emptyList<String>())
is RealCategoryData -> {
knownInstalledApps.switchMap { installedApps ->
blockingAtActivityLevel.map { blockingAtActivityLevel ->
val prepared = getAppsWithCategories(installedApps, categories, blockingAtActivityLevel)
val result = mutableListOf<String>()
installedApps.forEach { packageName ->
val appCategories = prepared[packageName] ?: emptySet()
if (appCategories.find { categoryId -> (blockingReasons[categoryId] ?: BlockingReason.None) == BlockingReason.None } == null) {
result.add(packageName)
}
}
result
}
}
}
} as LiveData<List<String>>
}
}
}
private val realAppsToBlock = appLogic.database.config().isExperimentalFlagsSetAsync(ExperimentalFlags.SYSTEM_LEVEL_BLOCKING).switchMap { systemLevelBlocking ->
if (systemLevelBlocking) {
appsToBlock
} else {
liveDataFromValue(emptyList())
}
}
private fun getAppsWithCategories(packageNames: List<String>, data: RealCategoryData, blockingAtActivityLevel: Boolean): Map<String, Set<String>> {
val categoryForUnassignedApps = if (data.categories.find { it.id == data.categoryForUnassignedApps } != null) data.categoryForUnassignedApps else null
if (blockingAtActivityLevel) {
val categoriesByPackageName = data.categoryApps.groupBy { it.packageNameWithoutActivityName }
val result = mutableMapOf<String, Set<String>>()
packageNames.forEach { packageName ->
val categoriesItems = categoriesByPackageName[packageName]
val categories = (categoriesItems?.map { it.categoryId }?.toSet() ?: emptySet()).toMutableSet()
val isMainAppIncluded = categoriesItems?.find { !it.specifiesActivity } != null
if (!isMainAppIncluded) {
if (categoryForUnassignedApps != null) {
categories.add(categoryForUnassignedApps)
}
}
result[packageName] = categories
}
return result
} else {
val categoryByPackageName = data.categoryApps.associateBy { it.packageName }
val result = mutableMapOf<String, Set<String>>()
packageNames.forEach { packageName ->
val category = categoryByPackageName[packageName]?.categoryId ?: categoryForUnassignedApps
result[packageName] = if (category != null) setOf(category) else emptySet()
}
return result
}
}
init {
realAppsToBlock.observeForever { appsToBlock ->
appLogic.platformIntegration.stopSuspendingForAllApps()
appLogic.platformIntegration.setSuspendedApps(appsToBlock, true)
}
}
}
internal sealed class CategoryData
internal object NoChildUser: CategoryData()
internal class RealCategoryData(
val categoryForUnassignedApps: String,
val categories: List<Category>,
val categoryApps: List<CategoryApp>
): CategoryData()

View file

@ -391,9 +391,11 @@ object ApplyServerDataStatus {
assignedAppsVersion = item.version
)
if (thisDeviceUserCategoryIds.contains(item.categoryId)) {
runAsync {
platformIntegration.setSuspendedApps(item.assignedApps, false)
if (!database.config().isExperimentalFlagsSetSync(ExperimentalFlags.SYSTEM_LEVEL_BLOCKING)) {
if (thisDeviceUserCategoryIds.contains(item.categoryId)) {
runAsync {
platformIntegration.setSuspendedApps(item.assignedApps, false)
}
}
}
}

View file

@ -22,6 +22,7 @@ import io.timelimit.android.async.Threads
import io.timelimit.android.coroutines.executeAndWait
import io.timelimit.android.crypto.Sha512
import io.timelimit.android.data.Database
import io.timelimit.android.data.model.ExperimentalFlags
import io.timelimit.android.data.model.PendingSyncAction
import io.timelimit.android.data.model.PendingSyncActionType
import io.timelimit.android.data.model.UserType
@ -164,15 +165,17 @@ object ApplyActionUtil {
LocalDatabaseParentActionDispatcher.dispatchParentActionSync(action, database)
// disable suspending the assigned app
if (action is AddCategoryAppsAction) {
val thisDeviceId = database.config().getOwnDeviceIdSync()!!
val thisDeviceEntry = database.device().getDeviceByIdSync(thisDeviceId)!!
if (!database.config().isExperimentalFlagsSetSync(ExperimentalFlags.SYSTEM_LEVEL_BLOCKING)) {
if (action is AddCategoryAppsAction) {
val thisDeviceId = database.config().getOwnDeviceIdSync()!!
val thisDeviceEntry = database.device().getDeviceByIdSync(thisDeviceId)!!
if (thisDeviceEntry.currentUserId != "") {
val userCategories = database.category().getCategoriesByChildIdSync(thisDeviceEntry.currentUserId)
if (thisDeviceEntry.currentUserId != "") {
val userCategories = database.category().getCategoriesByChildIdSync(thisDeviceEntry.currentUserId)
if (userCategories.find { category -> category.id == action.categoryId } != null) {
platformIntegration.setSuspendedApps(action.packageNames, false)
if (userCategories.find { category -> category.id == action.categoryId } != null) {
platformIntegration.setSuspendedApps(action.packageNames, false)
}
}
}
}

View file

@ -60,7 +60,7 @@ class DiagnoseExperimentalFlagFragment : Fragment() {
checkboxes.forEach { binding.container.addView(it) }
database.config().getExperimentalFlagsLive().observe(this, Observer { setFlags ->
database.config().experimentalFlags.observe(this, Observer { setFlags ->
flags.forEachIndexed { index, flag ->
val checkbox = checkboxes[index]
val isFlagSet = (setFlags and flag.flag) == flag.flag
@ -96,6 +96,11 @@ data class DiagnoseExperimentalFlagItem(
label = R.string.diagnose_exf_lom,
flag = ExperimentalFlags.DISABLE_BLOCK_ON_MANIPULATION,
enable = !BuildConfig.storeCompilant
),
DiagnoseExperimentalFlagItem(
label = R.string.diagnose_exf_slb,
flag = ExperimentalFlags.SYSTEM_LEVEL_BLOCKING,
enable = !BuildConfig.storeCompilant
)
)
}

View file

@ -54,4 +54,5 @@
<string name="diagnose_exf_title">Experimentelle Parameter</string>
<string name="diagnose_exf_lom">Sperrung nach Manipulationen deaktivieren</string>
<string name="diagnose_exf_slb">Apps auf Systemebene sperren</string>
</resources>

View file

@ -54,4 +54,5 @@
<string name="diagnose_exf_title">Experimental flags</string>
<string name="diagnose_exf_lom">Disable locking after manipulations</string>
<string name="diagnose_exf_slb">Block Apps at system level</string>
</resources>