mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 17:59:51 +02:00
Allow device specific app category assignments
This commit is contained in:
parent
d4bfa37caf
commit
d9a5f31d5a
40 changed files with 957 additions and 563 deletions
|
@ -377,7 +377,7 @@
|
|||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"fieldPath": "appSpecifierString",
|
||||
"columnName": "package_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
|
|
|
@ -1,3 +1,18 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -6,6 +21,7 @@ import androidx.room.Insert
|
|||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import io.timelimit.android.data.model.AppActivity
|
||||
import io.timelimit.android.data.model.AppActivityTitleAndClassNameItem
|
||||
|
||||
@Dao
|
||||
interface AppActivityDao {
|
||||
|
@ -15,8 +31,8 @@ interface AppActivityDao {
|
|||
@Query("SELECT * FROM app_activity WHERE device_id IN (:deviceIds)")
|
||||
fun getAppActivitiesByDeviceIds(deviceIds: List<String>): LiveData<List<AppActivity>>
|
||||
|
||||
@Query("SELECT * FROM app_activity WHERE app_package_name = :packageName")
|
||||
fun getAppActivitiesByPackageName(packageName: String): LiveData<List<AppActivity>>
|
||||
@Query("SELECT DISTINCT activity_class_name, activity_title FROM app_activity WHERE app_package_name = :packageName")
|
||||
fun getAppActivitiesByPackageName(packageName: String): LiveData<List<AppActivityTitleAndClassNameItem>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun addAppActivitySync(item: AppActivity)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -17,9 +17,7 @@ package io.timelimit.android.data.dao
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import io.timelimit.android.data.model.Device
|
||||
import io.timelimit.android.data.model.NetworkTime
|
||||
import io.timelimit.android.data.model.NetworkTimeAdapter
|
||||
import io.timelimit.android.data.model.*
|
||||
import io.timelimit.android.integration.platform.NewPermissionStatusConverter
|
||||
import io.timelimit.android.integration.platform.ProtectionLevelConverter
|
||||
import io.timelimit.android.integration.platform.RuntimePermissionStatusConverter
|
||||
|
@ -41,6 +39,9 @@ abstract class DeviceDao {
|
|||
@Query("SELECT * FROM device ORDER BY id")
|
||||
abstract fun getAllDevicesLive(): LiveData<List<Device>>
|
||||
|
||||
@Query("SELECT id, name FROM device")
|
||||
abstract fun getDeviceNamesLive(): LiveData<List<DeviceName>>
|
||||
|
||||
@Query("SELECT * FROM device")
|
||||
abstract fun getAllDevicesSync(): List<Device>
|
||||
|
||||
|
@ -68,6 +69,9 @@ abstract class DeviceDao {
|
|||
@Query("SELECT * FROM device WHERE current_user_id = :userId")
|
||||
abstract fun getDevicesByUserId(userId: String): LiveData<List<Device>>
|
||||
|
||||
@Query("SELECT id FROM device WHERE current_user_id = :userId")
|
||||
abstract fun getDevicesIdByUserId(userId: String): LiveData<List<DeviceId>>
|
||||
|
||||
@Query("UPDATE device SET apps_version = \"\"")
|
||||
abstract fun deleteAllInstalledAppsVersions()
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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 androidx.room.ColumnInfo
|
||||
|
||||
class AppActivityTitleAndClassNameItem (
|
||||
@ColumnInfo(name = "activity_class_name")
|
||||
val activityClassName: String,
|
||||
@ColumnInfo(name = "activity_title")
|
||||
val title: String
|
||||
)
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -21,13 +21,14 @@ import androidx.room.ColumnInfo
|
|||
import androidx.room.Entity
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.JsonSerializable
|
||||
import io.timelimit.android.data.model.derived.AppSpecifier
|
||||
|
||||
@Entity(primaryKeys = ["category_id", "package_name"], tableName = "category_app")
|
||||
data class CategoryApp(
|
||||
@ColumnInfo(index = true, name = "category_id")
|
||||
val categoryId: String,
|
||||
@ColumnInfo(index = true, name = "package_name")
|
||||
val packageName: String
|
||||
val appSpecifierString: String // originally a packageName, but can contain more than that
|
||||
): JsonSerializable {
|
||||
companion object {
|
||||
private const val CATEGORY_ID = "c"
|
||||
|
@ -51,26 +52,18 @@ data class CategoryApp(
|
|||
|
||||
return CategoryApp(
|
||||
categoryId = categoryId!!,
|
||||
packageName = packageName!!
|
||||
appSpecifierString = packageName!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@delegate:Transient
|
||||
val packageNameWithoutActivityName: String by lazy {
|
||||
if (specifiesActivity)
|
||||
packageName.substring(0, packageName.indexOf(":"))
|
||||
else
|
||||
packageName
|
||||
}
|
||||
|
||||
@Transient
|
||||
val specifiesActivity = packageName.contains(":")
|
||||
val appSpecifier: AppSpecifier by lazy { AppSpecifier.decode(appSpecifierString) }
|
||||
|
||||
init {
|
||||
IdGenerator.assertIdValid(categoryId)
|
||||
|
||||
if (packageName.isEmpty()) {
|
||||
if (appSpecifierString.isEmpty()) {
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +72,7 @@ data class CategoryApp(
|
|||
writer.beginObject()
|
||||
|
||||
writer.name(CATEGORY_ID).value(categoryId)
|
||||
writer.name(PACKAGE_NAME).value(packageName)
|
||||
writer.name(PACKAGE_NAME).value(appSpecifierString)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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 androidx.room.ColumnInfo
|
||||
|
||||
data class DeviceId (
|
||||
@ColumnInfo(name = "id")
|
||||
val id: String
|
||||
)
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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 androidx.room.ColumnInfo
|
||||
|
||||
data class DeviceName (
|
||||
@ColumnInfo(name = "id")
|
||||
val id: String,
|
||||
@ColumnInfo(name = "name")
|
||||
val name: String
|
||||
)
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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.derived
|
||||
|
||||
import kotlin.text.StringBuilder
|
||||
|
||||
data class AppSpecifier(val packageName: String, val activityName: String?, val deviceId: String?) {
|
||||
companion object {
|
||||
fun decode(input: String): AppSpecifier {
|
||||
val activityIndex = input.indexOf(':')
|
||||
val deviceIndex = input.indexOf('@', startIndex = activityIndex)
|
||||
val packageNameEndIndex = if (activityIndex != -1) activityIndex else deviceIndex
|
||||
|
||||
val packageName = if (packageNameEndIndex == -1) input else input.substring(0, packageNameEndIndex)
|
||||
val activityName = if (activityIndex == -1) null else {
|
||||
input.substring(activityIndex + 1, if (deviceIndex == -1) input.length else deviceIndex)
|
||||
}
|
||||
val deviceId = if (deviceIndex == -1) null else input.substring(deviceIndex + 1)
|
||||
|
||||
return AppSpecifier(
|
||||
packageName = packageName,
|
||||
activityName = activityName,
|
||||
deviceId = deviceId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
if (packageName.indexOf(':') != -1 || packageName.indexOf(':') != -1) {
|
||||
throw InvalidValueException()
|
||||
}
|
||||
|
||||
if (activityName != null && activityName?.indexOf('@') != -1) {
|
||||
throw InvalidValueException()
|
||||
}
|
||||
}
|
||||
|
||||
fun encode(): String = StringBuilder().let { builder ->
|
||||
builder.append(packageName)
|
||||
|
||||
if (activityName != null) {
|
||||
builder.append(':').append(activityName)
|
||||
}
|
||||
|
||||
if (deviceId != null) {
|
||||
builder.append('@').append(deviceId)
|
||||
}
|
||||
|
||||
builder.trimToSize()
|
||||
builder.toString()
|
||||
}
|
||||
|
||||
class InvalidValueException: RuntimeException()
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -34,7 +34,7 @@ data class UserRelatedData(
|
|||
val categoryApps: List<CategoryApp>
|
||||
): Observer {
|
||||
companion object {
|
||||
private val notFoundCategoryApp = CategoryApp(categoryId = IdGenerator.generateId(), packageName = BuildConfig.APPLICATION_ID)
|
||||
private val notFoundCategoryApp = CategoryApp(categoryId = IdGenerator.generateId(), appSpecifierString = BuildConfig.APPLICATION_ID)
|
||||
|
||||
private val relatedTables = arrayOf(
|
||||
Table.User, Table.Category, Table.TimeLimitRule,
|
||||
|
@ -72,11 +72,11 @@ data class UserRelatedData(
|
|||
// notFoundCategoryApp is a workaround because the lru cache does not support null
|
||||
private val categoryAppLruCache = object: LruCache<String, CategoryApp>(8) {
|
||||
override fun create(key: String): CategoryApp {
|
||||
return categoryApps.find { it.packageName == key } ?: notFoundCategoryApp
|
||||
return categoryApps.find { it.appSpecifierString == key } ?: notFoundCategoryApp
|
||||
}
|
||||
}
|
||||
fun findCategoryApp(packageName: String): CategoryApp? {
|
||||
val item = categoryAppLruCache[packageName]
|
||||
private fun findCategoryApp(appSpecifier: AppSpecifier): CategoryApp? {
|
||||
val item = categoryAppLruCache[appSpecifier.encode()]
|
||||
|
||||
// important: strict equality/ same object instance
|
||||
if (item === notFoundCategoryApp) {
|
||||
|
@ -85,6 +85,15 @@ data class UserRelatedData(
|
|||
return item
|
||||
}
|
||||
}
|
||||
fun findCategoryAppTryDeviceSpecificFirst(packageName: String, activityName: String?, deviceId: String): CategoryApp? = findCategoryApp(AppSpecifier(
|
||||
packageName = packageName,
|
||||
activityName = activityName,
|
||||
deviceId = deviceId
|
||||
)) ?: findCategoryApp(AppSpecifier(
|
||||
packageName = packageName,
|
||||
activityName = activityName,
|
||||
deviceId = null
|
||||
))
|
||||
|
||||
private var userInvalidated = false
|
||||
private var categoriesInvalidated = false
|
||||
|
|
|
@ -223,7 +223,7 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
|||
.map {
|
||||
CategoryApp(
|
||||
categoryId = allowedAppsCategoryId,
|
||||
packageName = it.packageName
|
||||
appSpecifierString = it.packageName
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -152,7 +152,7 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
|
|||
lastDefaultCategory = defaultCategory
|
||||
|
||||
val installedApps = appLogic.platformIntegration.getLocalAppPackageNames()
|
||||
val prepared = getAppsWithCategories(installedApps, userRelatedData, blockingAtActivityLevel)
|
||||
val prepared = getAppsWithCategories(installedApps, userRelatedData, blockingAtActivityLevel, userAndDeviceRelatedData.deviceRelatedData.deviceEntry.id)
|
||||
val appsToBlock = mutableListOf<String>()
|
||||
|
||||
installedApps.forEach { packageName ->
|
||||
|
@ -169,19 +169,32 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getAppsWithCategories(packageNames: List<String>, data: UserRelatedData, blockingAtActivityLevel: Boolean): Map<String, Set<String>> {
|
||||
private fun getAppsWithCategories(packageNames: List<String>, data: UserRelatedData, blockingAtActivityLevel: Boolean, deviceId: String): Map<String, Set<String>> {
|
||||
val categoryForUnassignedApps = data.categoryById[data.user.categoryForNotAssignedApps]
|
||||
val categoryForOtherSystemApps = data.findCategoryApp(DummyApps.NOT_ASSIGNED_SYSTEM_IMAGE_APP)?.categoryId?.let { data.categoryById[it] }
|
||||
val categoryForOtherSystemApps = data.findCategoryAppTryDeviceSpecificFirst(
|
||||
packageName = DummyApps.NOT_ASSIGNED_SYSTEM_IMAGE_APP,
|
||||
activityName = null,
|
||||
deviceId = deviceId
|
||||
)?.categoryId?.let { data.categoryById[it] }
|
||||
|
||||
val globalCategoryApps = data.categoryApps.filter { it.appSpecifier.deviceId == null }
|
||||
val localCategoryApps = data.categoryApps.filter { it.appSpecifier.deviceId == deviceId }
|
||||
val localCategoryAppsParams = localCategoryApps.map { it.appSpecifier.packageName to it.appSpecifier.activityName }.toSet()
|
||||
val effectiveGlobalCategoryApps = globalCategoryApps.filter {
|
||||
!localCategoryAppsParams.contains(it.appSpecifier.packageName to it.appSpecifier.activityName) &&
|
||||
!localCategoryAppsParams.contains(it.appSpecifier.packageName to null)
|
||||
}
|
||||
val effectiveCategoryApps = effectiveGlobalCategoryApps + localCategoryApps
|
||||
|
||||
if (blockingAtActivityLevel) {
|
||||
val categoriesByPackageName = data.categoryApps.groupBy { it.packageNameWithoutActivityName }
|
||||
val categoriesByPackageName = effectiveCategoryApps.groupBy { it.appSpecifier.packageName }
|
||||
|
||||
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
|
||||
val isMainAppIncluded = categoriesItems?.find { it.appSpecifier.activityName == null } != null
|
||||
|
||||
if (!isMainAppIncluded) {
|
||||
if (categoryForOtherSystemApps != null && appLogic.platformIntegration.isSystemImageApp(packageName)) {
|
||||
|
@ -196,7 +209,9 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
|
|||
|
||||
return result
|
||||
} else {
|
||||
val categoryByPackageName = data.categoryApps.associateBy { it.packageName }
|
||||
val categoryByPackageName = effectiveCategoryApps
|
||||
.filter { it.appSpecifier.activityName == null }
|
||||
.associateBy { it.appSpecifier.packageName }
|
||||
|
||||
val result = mutableMapOf<String, Set<String>>()
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -73,15 +73,31 @@ sealed class AppBaseHandling {
|
|||
} else if (foregroundAppPackageName != null) {
|
||||
val appCategory = run {
|
||||
val tryActivityLevelBlocking = deviceRelatedData.deviceEntry.enableActivityLevelBlocking && foregroundAppActivityName != null
|
||||
val appLevelCategory = userRelatedData.findCategoryApp(foregroundAppPackageName) ?: run {
|
||||
if (isSystemImageApp) userRelatedData.findCategoryApp(DummyApps.NOT_ASSIGNED_SYSTEM_IMAGE_APP) else null
|
||||
val appLevelCategory = userRelatedData.findCategoryAppTryDeviceSpecificFirst(
|
||||
packageName = foregroundAppPackageName,
|
||||
activityName = null,
|
||||
deviceId = deviceRelatedData.deviceEntry.id
|
||||
) ?: run {
|
||||
if (isSystemImageApp) userRelatedData.findCategoryAppTryDeviceSpecificFirst(
|
||||
packageName = DummyApps.NOT_ASSIGNED_SYSTEM_IMAGE_APP,
|
||||
activityName = null,
|
||||
deviceId = deviceRelatedData.deviceEntry.id
|
||||
) else null
|
||||
}
|
||||
|
||||
(if (tryActivityLevelBlocking) {
|
||||
userRelatedData.findCategoryApp("$foregroundAppPackageName:$foregroundAppActivityName")
|
||||
} else {
|
||||
null
|
||||
}) ?: appLevelCategory
|
||||
if (tryActivityLevelBlocking) {
|
||||
val activityLevelCategory = userRelatedData.findCategoryAppTryDeviceSpecificFirst(
|
||||
packageName = foregroundAppPackageName,
|
||||
activityName = foregroundAppActivityName,
|
||||
deviceId = deviceRelatedData.deviceEntry.id
|
||||
)
|
||||
|
||||
val appLevelCategoryMoreImportant = appLevelCategory != null && activityLevelCategory != null &&
|
||||
appLevelCategory.appSpecifier.deviceId != null &&
|
||||
activityLevelCategory.appSpecifier.deviceId == null
|
||||
|
||||
if (activityLevelCategory == null || appLevelCategoryMoreImportant) appLevelCategory else activityLevelCategory
|
||||
} else appLevelCategory
|
||||
}
|
||||
|
||||
val startCategory = userRelatedData.categoryById[appCategory?.categoryId]
|
||||
|
@ -95,8 +111,7 @@ sealed class AppBaseHandling {
|
|||
return UseCategories(
|
||||
categoryIds = categoryIds,
|
||||
shouldCount = !pauseCounting,
|
||||
level = when (appCategory?.specifiesActivity) {
|
||||
null -> BlockingLevel.Activity // occurs when using a default category
|
||||
level = when (appCategory == null || appCategory.appSpecifier.activityName != null) {
|
||||
true -> BlockingLevel.Activity
|
||||
false -> BlockingLevel.App
|
||||
},
|
||||
|
|
|
@ -458,7 +458,7 @@ object ApplyServerDataStatus {
|
|||
database.categoryApp().addCategoryAppsSync(item.assignedApps.map {
|
||||
CategoryApp(
|
||||
categoryId = item.categoryId,
|
||||
packageName = it
|
||||
appSpecifierString = it
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -53,6 +53,10 @@ object LocalDatabaseParentActionDispatcher {
|
|||
val allCategoriesOfChild = database.category().getCategoriesByChildIdSync(categoryEntry.childId)
|
||||
|
||||
if (fromChildSelfLimitAddChildUserId != null) {
|
||||
if (action.packageNames.find { it.contains('@') } != null) {
|
||||
throw RuntimeException("can not do device specific assignments as child")
|
||||
}
|
||||
|
||||
val parentCategoriesOfTargetCategory = allCategoriesOfChild.getCategoryWithParentCategories(action.categoryId)
|
||||
val userEntry = database.user().getUserByIdSync(fromChildSelfLimitAddChildUserId) ?: throw RuntimeException("user not found")
|
||||
val validatedDefaultCategoryId = (allCategoriesOfChild.find {
|
||||
|
@ -99,7 +103,7 @@ object LocalDatabaseParentActionDispatcher {
|
|||
action.packageNames.map {
|
||||
CategoryApp(
|
||||
categoryId = action.categoryId,
|
||||
packageName = it
|
||||
appSpecifierString = it
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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.apps
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.switchMap
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.manage.category.apps.add.AddAppsParams
|
||||
|
||||
class AddAppsOrActivitiesModel(application: Application): AndroidViewModel(application) {
|
||||
private var didInit = false
|
||||
private var paramsLive = MutableLiveData<AddAppsParams>()
|
||||
|
||||
fun init(params: AddAppsParams) {
|
||||
if (didInit) return
|
||||
|
||||
paramsLive.value = params
|
||||
didInit = true
|
||||
}
|
||||
|
||||
fun isAuthValid(auth: ActivityViewModel) = paramsLive.switchMap { params ->
|
||||
auth.authenticatedUserOrChild.map {
|
||||
val parentAuthValid = it?.second?.type == UserType.Parent
|
||||
val childAuthValid = it?.second?.id == params.childId && params.isSelfLimitAddingMode
|
||||
val authValid = parentAuthValid || childAuthValid
|
||||
|
||||
authValid
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -18,39 +18,23 @@ package io.timelimit.android.ui.manage.category.apps.add
|
|||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.databinding.FragmentAddCategoryAppsItemBinding
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.logic.DummyApps
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class AddAppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
||||
var data: List<App>? by Delegates.observable(null as List<App>?) { _, _, _ -> notifyDataSetChanged() }
|
||||
var data: List<AddAppListItem> by Delegates.observable(emptyList()) { _, _, _ -> notifyDataSetChanged() }
|
||||
var listener: AddAppAdapterListener? = null
|
||||
var categoryTitleByPackageName: Map<String, String> by Delegates.observable(emptyMap()) { _, _, _ -> notifyDataSetChanged() }
|
||||
var selectedApps: Set<String> by Delegates.observable(emptySet()) { _, _, _ -> notifyDataSetChanged() }
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
private fun getItem(position: Int): App {
|
||||
return data!![position]
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return getItem(position).hashCode().toLong()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
val data = this.data
|
||||
|
||||
if (data == null) {
|
||||
return 0
|
||||
} else {
|
||||
return data.size
|
||||
}
|
||||
}
|
||||
private fun getItem(position: Int): AddAppListItem = data[position]
|
||||
override fun getItemId(position: Int): Long = getItem(position).hashCode().toLong()
|
||||
override fun getItemCount(): Int = data.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
FragmentAddCategoryAppsItemBinding.inflate(
|
||||
|
@ -65,10 +49,13 @@ class AddAppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
|||
val context = holder.itemView.context
|
||||
|
||||
holder.apply {
|
||||
binding.item = item
|
||||
binding.title = item.title
|
||||
binding.subtitle = item.packageName
|
||||
binding.currentCategoryTitle = item.currentCategoryName
|
||||
binding.checked = selectedApps.contains(item.packageName)
|
||||
binding.currentCategoryTitle = categoryTitleByPackageName[item.packageName]
|
||||
binding.handlers = listener
|
||||
binding.card.setOnClickListener { listener?.onAppClicked(item) }
|
||||
binding.card.setOnLongClickListener { listener?.onAppLongClicked(item) ?: false }
|
||||
binding.showIcon = true
|
||||
binding.executePendingBindings()
|
||||
|
||||
binding.icon.setImageDrawable(
|
||||
|
@ -83,6 +70,6 @@ class AddAppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
|||
class ViewHolder(val binding: FragmentAddCategoryAppsItemBinding): RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
interface AddAppAdapterListener {
|
||||
fun onAppClicked(app: App)
|
||||
fun onAppLongClicked(app: App): Boolean
|
||||
fun onAppClicked(app: AddAppListItem)
|
||||
fun onAppLongClicked(app: AddAppListItem): Boolean
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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.apps.add
|
||||
|
||||
data class AddAppListItem (
|
||||
val title: String,
|
||||
val packageName: String,
|
||||
val currentCategoryName: String?
|
||||
)
|
|
@ -0,0 +1,206 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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.apps.add
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.*
|
||||
import androidx.lifecycle.map
|
||||
import io.timelimit.android.data.extensions.getCategoryWithParentCategories
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.data.model.derived.CategoryRelatedData
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.livedata.switchMap
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.logic.DummyApps
|
||||
import io.timelimit.android.ui.view.AppFilterView
|
||||
import kotlin.collections.map
|
||||
|
||||
class AddAppsModel(application: Application): AndroidViewModel(application) {
|
||||
private var didInit = false
|
||||
private var paramsLive = MutableLiveData<AddAppsParams>()
|
||||
|
||||
fun init(params: AddAppsParams) {
|
||||
if (didInit) return
|
||||
|
||||
paramsLive.value = params
|
||||
didInit = true
|
||||
}
|
||||
|
||||
private val logic = DefaultAppLogic.with(application)
|
||||
private val database = logic.database
|
||||
|
||||
val showAppsFromOtherDevicesChecked = MutableLiveData<Boolean>().apply { value = false }
|
||||
val showAppsFromOtherCategories = MutableLiveData<Boolean>().apply { value = false }
|
||||
val assignToThisDeviceOnly = MutableLiveData<Boolean>().apply { value = false }
|
||||
val filter = MutableLiveData<AppFilterView.AppFilter>().apply { value = AppFilterView.AppFilter.dummy }
|
||||
|
||||
val isLocalMode = logic.fullVersion.isLocalMode
|
||||
val showDeviceSpecificAssignmentOption = isLocalMode.invert().and(paramsLive.map { !it.isSelfLimitAddingMode })
|
||||
val deviceIdLive = logic.deviceId
|
||||
|
||||
private val effectiveAssignToThisDeviceOnly = assignToThisDeviceOnly.and(showDeviceSpecificAssignmentOption)
|
||||
|
||||
private val realShowAppsFromAllDevices = isLocalMode.switchMap { localMode ->
|
||||
if (localMode) liveDataFromNonNullValue(true)
|
||||
else showAppsFromOtherDevicesChecked
|
||||
}
|
||||
|
||||
private val childDeviceIds = paramsLive.switchMap { params ->
|
||||
database.device().getDevicesIdByUserId(params.childId).map { devices -> devices.map { it.id } }
|
||||
}.ignoreUnchanged()
|
||||
|
||||
private val globalChildDeviceCounter = database.device().countDevicesWithChildUser()
|
||||
|
||||
private val hasChildDeviceIds = childDeviceIds.map { it.isNotEmpty() }
|
||||
|
||||
private val appsAtAssignedDevices = childDeviceIds
|
||||
.switchMap { database.app().getAppsByDeviceIds(it) }
|
||||
|
||||
private val appsAtAllDevices = database.app().getAllApps()
|
||||
|
||||
private val installedApps = realShowAppsFromAllDevices.switchMap { appsFromAllDevices ->
|
||||
if (appsFromAllDevices) appsAtAllDevices else appsAtAssignedDevices
|
||||
}.map { list ->
|
||||
if (list.isEmpty()) list
|
||||
else list + DummyApps.getApps(deviceId = list.first().deviceId, context = getApplication())
|
||||
}.map { apps -> apps.distinctBy { app -> app.packageName } }
|
||||
|
||||
private val userRelatedDataLive = paramsLive.switchMap { params ->
|
||||
database.derivedDataDao().getUserRelatedDataLive(params.childId)
|
||||
}
|
||||
|
||||
private val categoryByAppSpecifierLive = userRelatedDataLive.map { data ->
|
||||
data?.categoryApps?.associateBy { it.appSpecifierString }?.mapValues {
|
||||
data.categoryById.get(it.value.categoryId)
|
||||
} ?: emptyMap()
|
||||
}
|
||||
|
||||
private val installedAppsWithCurrentCategories = mergeLiveDataWaitForValues(deviceIdLive, categoryByAppSpecifierLive, installedApps, effectiveAssignToThisDeviceOnly)
|
||||
.map { (deviceId, categoryByAppSpecifier, apps, assignToThisDeviceOnly) ->
|
||||
apps.map {
|
||||
AppWithCategory(it, categoryByAppSpecifier.get(
|
||||
if (assignToThisDeviceOnly) "${it.packageName}@$deviceId" else it.packageName
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private val shownApps = mergeLiveDataWaitForValues(paramsLive, userRelatedDataLive, installedAppsWithCurrentCategories)
|
||||
.map { (params, userRelatedData, installedApps) ->
|
||||
if (params.isSelfLimitAddingMode) {
|
||||
if (userRelatedData == null || !userRelatedData.categoryById.containsKey(params.categoryId))
|
||||
emptyList()
|
||||
else {
|
||||
val parentCategories =
|
||||
userRelatedData.getCategoryWithParentCategories(params.categoryId)
|
||||
val defaultCategory =
|
||||
userRelatedData.categoryById[userRelatedData.user.categoryForNotAssignedApps]
|
||||
val allowAppsWithoutCategory =
|
||||
defaultCategory != null && parentCategories.contains(defaultCategory.category.id)
|
||||
val packageNameToCategoryId =
|
||||
userRelatedData.categoryApps
|
||||
.filter { it.appSpecifier.deviceId == null }
|
||||
.associateBy { it.appSpecifier.packageName }
|
||||
|
||||
installedApps.filter { app ->
|
||||
val appCategoryId = packageNameToCategoryId[app.app.packageName]?.categoryId
|
||||
val categoryNotFound = !userRelatedData.categoryById.containsKey(appCategoryId)
|
||||
|
||||
parentCategories.contains(appCategoryId) || (categoryNotFound && allowAppsWithoutCategory)
|
||||
}
|
||||
}
|
||||
} else installedApps
|
||||
}
|
||||
|
||||
val listItems = filter.switchMap { filter ->
|
||||
shownApps.map { filter to it }
|
||||
}.map { (search, apps) ->
|
||||
apps.filter { search.matches(it.app) }
|
||||
}.switchMap { apps ->
|
||||
showAppsFromOtherCategories.map { showOtherCategeories ->
|
||||
if (showOtherCategeories) apps
|
||||
else apps.filter { it.category == null }
|
||||
}
|
||||
}.map { apps ->
|
||||
apps.sortedBy { app -> app.app.title.lowercase() }
|
||||
}.map { apps ->
|
||||
apps.map { app ->
|
||||
AddAppListItem(
|
||||
title = app.app.title,
|
||||
packageName = app.app.packageName,
|
||||
currentCategoryName = app.category?.category?.title
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val emptyViewText: LiveData<EmptyViewText> = listItems.switchMap { items ->
|
||||
if (items.isNotEmpty()) {
|
||||
// list is not empty ...
|
||||
liveDataFromNonNullValue(EmptyViewText.None)
|
||||
} else /* items.isEmpty() */ {
|
||||
shownApps.switchMap { shownApps ->
|
||||
if (shownApps.isNotEmpty()) {
|
||||
liveDataFromNonNullValue(EmptyViewText.EmptyDueToFilter)
|
||||
} else /* if (shownApps.isEmpty()) */ {
|
||||
installedApps.switchMap { installedApps ->
|
||||
if (installedApps.isNotEmpty()) {
|
||||
liveDataFromNonNullValue(EmptyViewText.EmptyDueToChildMode)
|
||||
} else /* if (installedApps.isEmpty()) */ {
|
||||
isLocalMode.switchMap { isLocalMode ->
|
||||
if (isLocalMode) {
|
||||
liveDataFromNonNullValue(EmptyViewText.EmptyLocalMode)
|
||||
} else {
|
||||
showAppsFromOtherDevicesChecked.switchMap { showAppsFromOtherDevicesChecked ->
|
||||
if (showAppsFromOtherDevicesChecked) {
|
||||
globalChildDeviceCounter.map { childDeviceCounter ->
|
||||
if (childDeviceCounter == 0L) {
|
||||
EmptyViewText.EmptyAllDevicesNoAppsNoChildDevices
|
||||
} else {
|
||||
EmptyViewText.EmptyAllDevicesNoAppsButChildDevices
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hasChildDeviceIds.map { hasChildDeviceIds ->
|
||||
if (hasChildDeviceIds) {
|
||||
EmptyViewText.EmptyChildDevicesHaveNoApps
|
||||
} else {
|
||||
EmptyViewText.EmptyNoChildDevices
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class EmptyViewText {
|
||||
None,
|
||||
EmptyDueToFilter,
|
||||
EmptyDueToChildMode,
|
||||
EmptyLocalMode,
|
||||
EmptyAllDevicesNoAppsNoChildDevices,
|
||||
EmptyAllDevicesNoAppsButChildDevices,
|
||||
EmptyChildDevicesHaveNoApps,
|
||||
EmptyNoChildDevices
|
||||
}
|
||||
|
||||
internal data class AppWithCategory (val app: App, val category: CategoryRelatedData?)
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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.apps.add
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class AddAppsParams (
|
||||
val childId: String,
|
||||
val categoryId: String,
|
||||
val isSelfLimitAddingMode: Boolean
|
||||
): Parcelable
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -15,7 +15,6 @@
|
|||
*/
|
||||
package io.timelimit.android.ui.manage.category.apps.add
|
||||
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
|
@ -27,24 +26,18 @@ import androidx.appcompat.app.AlertDialog
|
|||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.data.extensions.getCategoryWithParentCategories
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.databinding.FragmentAddCategoryAppsBinding
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.logic.DummyApps
|
||||
import io.timelimit.android.sync.actions.AddCategoryAppsAction
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.main.getActivityViewModel
|
||||
import io.timelimit.android.ui.manage.category.apps.AddAppsOrActivitiesModel
|
||||
import io.timelimit.android.ui.manage.category.apps.addactivity.AddActivitiesParams
|
||||
import io.timelimit.android.ui.manage.category.apps.addactivity.AddAppActivitiesDialogFragment
|
||||
import io.timelimit.android.ui.view.AppFilterView
|
||||
|
||||
|
@ -53,23 +46,18 @@ class AddCategoryAppsFragment : DialogFragment() {
|
|||
private const val DIALOG_TAG = "x"
|
||||
private const val STATUS_PACKAGE_NAMES = "d"
|
||||
private const val STATUS_EDUCATED = "e"
|
||||
private const val PARAM_CHILD_ID = "childId"
|
||||
private const val PARAM_CATEGORY_ID = "categoryId"
|
||||
private const val PARAM_CHILD_ADD_LIMIT_MODE = "addLimitMode"
|
||||
private const val PARAMS = "params"
|
||||
|
||||
fun newInstance(childId: String, categoryId: String, childAddLimitMode: Boolean) = AddCategoryAppsFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(PARAM_CHILD_ID, childId)
|
||||
putString(PARAM_CATEGORY_ID, categoryId)
|
||||
putBoolean(PARAM_CHILD_ADD_LIMIT_MODE, childAddLimitMode)
|
||||
}
|
||||
fun newInstance(params: AddAppsParams) = AddCategoryAppsFragment().apply {
|
||||
arguments = Bundle().apply { putParcelable(PARAMS, params) }
|
||||
}
|
||||
}
|
||||
|
||||
private val database: Database by lazy { DefaultAppLogic.with(requireContext()).database }
|
||||
private val auth: ActivityViewModel by lazy { getActivityViewModel(requireActivity()) }
|
||||
private val adapter = AddAppAdapter()
|
||||
private var didEducateAboutAddingAssignedApps = false
|
||||
private val baseModel: AddAppsOrActivitiesModel by viewModels()
|
||||
private val model: AddAppsModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -89,167 +77,43 @@ class AddCategoryAppsFragment : DialogFragment() {
|
|||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val binding = FragmentAddCategoryAppsBinding.inflate(LayoutInflater.from(context))
|
||||
val childId = requireArguments().getString(PARAM_CHILD_ID)!!
|
||||
val categoryId = requireArguments().getString(PARAM_CATEGORY_ID)!!
|
||||
val childAddLimitMode = requireArguments().getBoolean(PARAM_CHILD_ADD_LIMIT_MODE)
|
||||
val params = requireArguments().getParcelable<AddAppsParams>(PARAMS)!!
|
||||
|
||||
auth.authenticatedUserOrChild.observe(this, Observer {
|
||||
val parentAuthValid = it?.second?.type == UserType.Parent
|
||||
val childAuthValid = it?.second?.id == childId && childAddLimitMode
|
||||
val authValid = parentAuthValid || childAuthValid
|
||||
baseModel.init(params)
|
||||
baseModel.isAuthValid(auth).observe(this) { if (!it) dismissAllowingStateLoss() }
|
||||
|
||||
if (!authValid) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
})
|
||||
model.init(params)
|
||||
|
||||
val filter = AppFilterView.getFilterLive(binding.filter)
|
||||
val isLocalMode = database.config().getDeviceAuthTokenAsync().map { it.isEmpty() }
|
||||
val showAppsFromOtherDevicesChecked = MutableLiveData<Boolean>().apply {
|
||||
value = binding.showAppsFromUnassignedDevices.isChecked
|
||||
}
|
||||
val realShowAppsFromAllDevices = isLocalMode.switchMap { localMode ->
|
||||
if (localMode) {
|
||||
liveDataFromNonNullValue(true)
|
||||
} else {
|
||||
showAppsFromOtherDevicesChecked
|
||||
}
|
||||
}
|
||||
model.showAppsFromOtherDevicesChecked.value = binding.showAppsFromUnassignedDevices.isChecked
|
||||
model.showAppsFromOtherCategories.value = binding.showOtherCategoriesApps.isChecked
|
||||
model.assignToThisDeviceOnly.value = binding.assignToThisDeviceOnly.isChecked
|
||||
|
||||
binding.showAppsFromUnassignedDevices.setOnCheckedChangeListener { _, isChecked ->
|
||||
showAppsFromOtherDevicesChecked.value = isChecked
|
||||
model.showAppsFromOtherDevicesChecked.value = isChecked
|
||||
}
|
||||
|
||||
isLocalMode.observe(this, Observer {
|
||||
binding.showAppsFromUnassignedDevices.visibility = if (it) View.GONE else View.VISIBLE
|
||||
})
|
||||
binding.assignToThisDeviceOnly.setOnCheckedChangeListener { _, isChecked ->
|
||||
model.assignToThisDeviceOnly.value = isChecked
|
||||
}
|
||||
|
||||
val showAppsFromOtherCategories = MutableLiveData<Boolean>().apply { value = binding.showOtherCategoriesApps.isChecked }
|
||||
binding.showOtherCategoriesApps.setOnCheckedChangeListener { _, isChecked -> showAppsFromOtherCategories.value = isChecked }
|
||||
AppFilterView.getFilterLive(binding.filter).observe(this) { model.filter.value = it }
|
||||
|
||||
model.isLocalMode.observe(this) {
|
||||
binding.showAppsFromUnassignedDevices.visibility = if (it) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
model.showDeviceSpecificAssignmentOption.observe(this) {
|
||||
binding.assignToThisDeviceOnly.visibility = if (it) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
binding.showOtherCategoriesApps.setOnCheckedChangeListener { _, isChecked ->
|
||||
model.showAppsFromOtherCategories.value = isChecked
|
||||
}
|
||||
|
||||
binding.recycler.layoutManager = LinearLayoutManager(context)
|
||||
binding.recycler.adapter = adapter
|
||||
|
||||
val childDeviceIds = database.device().getDevicesByUserId(childId)
|
||||
.map { devices -> devices.map { it.id } }
|
||||
.ignoreUnchanged()
|
||||
|
||||
val globalChildDeviceCounter = database.device().countDevicesWithChildUser()
|
||||
|
||||
val hasChildDeviceIds = childDeviceIds.map { it.isNotEmpty() }
|
||||
|
||||
val appsAtAssignedDevices = childDeviceIds
|
||||
.switchMap { database.app().getAppsByDeviceIds(it) }
|
||||
|
||||
val appsAtAllDevices = database.app().getAllApps()
|
||||
|
||||
val installedApps = realShowAppsFromAllDevices.switchMap { appsFromAllDevices ->
|
||||
if (appsFromAllDevices) appsAtAllDevices else appsAtAssignedDevices
|
||||
}.map { list ->
|
||||
if (list.isEmpty()) list else list + DummyApps.getApps(deviceId = list.first().deviceId, context = requireContext())
|
||||
}.map { apps -> apps.distinctBy { app -> app.packageName } }
|
||||
|
||||
val userRelatedDataLive = database.derivedDataDao().getUserRelatedDataLive(childId)
|
||||
|
||||
val categoryTitleByPackageName = userRelatedDataLive.map { userRelatedData ->
|
||||
val result = mutableMapOf<String, String>()
|
||||
|
||||
userRelatedData?.categoryApps?.forEach { app ->
|
||||
result[app.packageName] = userRelatedData.categoryById[app.categoryId]!!.category.title
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
val packageNamesAssignedToOtherCategories = userRelatedDataLive
|
||||
.map { it?.categoryApps?.map { app -> app.packageName }?.toSet() ?: emptySet() }
|
||||
|
||||
val shownApps = if (childAddLimitMode) {
|
||||
userRelatedDataLive.switchMap { userRelatedData ->
|
||||
installedApps.map { installedApps ->
|
||||
if (userRelatedData == null || !userRelatedData.categoryById.containsKey(categoryId))
|
||||
emptyList()
|
||||
else {
|
||||
val parentCategories = userRelatedData.getCategoryWithParentCategories(categoryId)
|
||||
val defaultCategory = userRelatedData.categoryById[userRelatedData.user.categoryForNotAssignedApps]
|
||||
val allowAppsWithoutCategory = defaultCategory != null && parentCategories.contains(defaultCategory.category.id)
|
||||
val packageNameToCategoryId = userRelatedData.categoryApps.associateBy { it.packageName }
|
||||
|
||||
installedApps.filter { app ->
|
||||
val appCategoryId = packageNameToCategoryId[app.packageName]?.categoryId
|
||||
val categoryNotFound = !userRelatedData.categoryById.containsKey(appCategoryId)
|
||||
|
||||
parentCategories.contains(appCategoryId) || (categoryNotFound && allowAppsWithoutCategory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else installedApps
|
||||
|
||||
val listItems = filter.switchMap { filter ->
|
||||
shownApps.map { filter to it }
|
||||
}.map { (search, apps) ->
|
||||
apps.filter { search.matches(it) }
|
||||
}.switchMap { apps ->
|
||||
showAppsFromOtherCategories.switchMap { showOtherCategeories ->
|
||||
if (showOtherCategeories) {
|
||||
liveDataFromNonNullValue(apps)
|
||||
} else {
|
||||
packageNamesAssignedToOtherCategories.map { packagesFromOtherCategories ->
|
||||
apps.filterNot { packagesFromOtherCategories.contains(it.packageName) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}.map { apps ->
|
||||
apps.sortedBy { app -> app.title.toLowerCase() }
|
||||
}
|
||||
|
||||
val emptyViewText: LiveData<String?> = listItems.switchMap { items ->
|
||||
if (items.isNotEmpty()) {
|
||||
// list is not empty ...
|
||||
liveDataFromNullableValue(null as String?)
|
||||
} else /* items.isEmpty() */ {
|
||||
shownApps.switchMap { shownApps ->
|
||||
if (shownApps.isNotEmpty()) {
|
||||
liveDataFromNullableValue(getString(R.string.category_apps_add_empty_due_to_filter) as String?)
|
||||
} else /* if (shownApps.isEmpty()) */ {
|
||||
installedApps.switchMap { installedApps ->
|
||||
if (installedApps.isNotEmpty()) {
|
||||
liveDataFromNullableValue(getString(R.string.category_apps_add_empty_due_to_child_mode) as String?)
|
||||
} else /* if (installedApps.isEmpty()) */ {
|
||||
isLocalMode.switchMap { isLocalMode ->
|
||||
if (isLocalMode) {
|
||||
liveDataFromNullableValue(getString(R.string.category_apps_add_empty_local_mode) as String?)
|
||||
} else {
|
||||
showAppsFromOtherDevicesChecked.switchMap { showAppsFromOtherDevicesChecked ->
|
||||
if (showAppsFromOtherDevicesChecked) {
|
||||
globalChildDeviceCounter.map { childDeviceCounter ->
|
||||
if (childDeviceCounter == 0L) {
|
||||
getString(R.string.category_apps_add_empty_all_devices_no_apps_no_childs) as String?
|
||||
} else {
|
||||
getString(R.string.category_apps_add_empty_all_devices_no_apps_but_child_devices) as String?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hasChildDeviceIds.map { hasChildDeviceIds ->
|
||||
if (hasChildDeviceIds) {
|
||||
getString(R.string.category_apps_add_empty_child_devices_no_apps) as String?
|
||||
} else {
|
||||
getString(R.string.category_apps_add_empty_no_child_devices) as String?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} as LiveData<String?>
|
||||
}
|
||||
}
|
||||
|
||||
listItems.observe(this, Observer {
|
||||
model.listItems.observe(this, Observer {
|
||||
val selectedPackageNames = adapter.selectedApps
|
||||
val visiblePackageNames = it.map { it.packageName }.toSet()
|
||||
val hiddenSelectedPackageNames = selectedPackageNames.toMutableSet().apply { removeAll(visiblePackageNames) }.size
|
||||
|
@ -261,45 +125,57 @@ class AddCategoryAppsFragment : DialogFragment() {
|
|||
resources.getQuantityString(R.plurals.category_apps_add_dialog_hidden_entries, hiddenSelectedPackageNames, hiddenSelectedPackageNames)
|
||||
})
|
||||
|
||||
emptyViewText.observe(this, Observer { binding.emptyText = it })
|
||||
model.emptyViewText.observe(this) {
|
||||
binding.emptyText = when (it!!) {
|
||||
AddAppsModel.EmptyViewText.None -> null
|
||||
AddAppsModel.EmptyViewText.EmptyDueToFilter -> getString(R.string.category_apps_add_empty_due_to_filter)
|
||||
AddAppsModel.EmptyViewText.EmptyDueToChildMode -> getString(R.string.category_apps_add_empty_due_to_child_mode)
|
||||
AddAppsModel.EmptyViewText.EmptyLocalMode -> getString(R.string.category_apps_add_empty_local_mode)
|
||||
AddAppsModel.EmptyViewText.EmptyAllDevicesNoAppsNoChildDevices -> getString(R.string.category_apps_add_empty_all_devices_no_apps_no_childs)
|
||||
AddAppsModel.EmptyViewText.EmptyAllDevicesNoAppsButChildDevices -> getString(R.string.category_apps_add_empty_all_devices_no_apps_but_child_devices)
|
||||
AddAppsModel.EmptyViewText.EmptyChildDevicesHaveNoApps -> getString(R.string.category_apps_add_empty_child_devices_no_apps)
|
||||
AddAppsModel.EmptyViewText.EmptyNoChildDevices -> getString(R.string.category_apps_add_empty_no_child_devices)
|
||||
}
|
||||
}
|
||||
|
||||
categoryTitleByPackageName.observe(this, Observer {
|
||||
adapter.categoryTitleByPackageName = it
|
||||
})
|
||||
binding.someOptionsDisabledDueToChildAuthentication = params.isSelfLimitAddingMode
|
||||
|
||||
binding.someOptionsDisabledDueToChildAuthentication = childAddLimitMode
|
||||
model.deviceIdLive.observe(this) {/* keep loaded */}
|
||||
|
||||
binding.addAppsButton.setOnClickListener {
|
||||
val packageNames = adapter.selectedApps.toList()
|
||||
|
||||
if (packageNames.isNotEmpty()) {
|
||||
val deviceSpecific = binding.assignToThisDeviceOnly.isChecked && !params.isSelfLimitAddingMode
|
||||
val deviceId = model.deviceIdLive.value
|
||||
|
||||
if (deviceSpecific && deviceId == null) return@setOnClickListener
|
||||
|
||||
auth.tryDispatchParentAction(
|
||||
action = AddCategoryAppsAction(
|
||||
categoryId = categoryId,
|
||||
packageNames = packageNames
|
||||
categoryId = params.categoryId,
|
||||
packageNames = if (deviceSpecific) packageNames.map { "$it@$deviceId" } else packageNames
|
||||
),
|
||||
allowAsChild = childAddLimitMode
|
||||
allowAsChild = params.isSelfLimitAddingMode
|
||||
)
|
||||
}
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.cancelButton.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
binding.cancelButton.setOnClickListener { dismiss() }
|
||||
|
||||
binding.selectAllButton.setOnClickListener {
|
||||
adapter.selectedApps = adapter.selectedApps + (adapter.data?.map { it.packageName }?.toSet() ?: emptySet())
|
||||
}
|
||||
|
||||
adapter.listener = object: AddAppAdapterListener {
|
||||
override fun onAppClicked(app: App) {
|
||||
override fun onAppClicked(app: AddAppListItem) {
|
||||
if (adapter.selectedApps.contains(app.packageName)) {
|
||||
adapter.selectedApps = adapter.selectedApps - setOf(app.packageName)
|
||||
} else {
|
||||
if (!didEducateAboutAddingAssignedApps) {
|
||||
if (adapter.categoryTitleByPackageName[app.packageName] != null) {
|
||||
if (app.currentCategoryName != null) {
|
||||
didEducateAboutAddingAssignedApps = true
|
||||
|
||||
AddAlreadyAssignedAppsInfoDialog().show(fragmentManager!!)
|
||||
|
@ -310,14 +186,12 @@ class AddCategoryAppsFragment : DialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onAppLongClicked(app: App): Boolean {
|
||||
override fun onAppLongClicked(app: AddAppListItem): Boolean {
|
||||
return if (adapter.selectedApps.isEmpty()) {
|
||||
AddAppActivitiesDialogFragment.newInstance(
|
||||
childId = childId,
|
||||
categoryId = categoryId,
|
||||
packageName = app.packageName,
|
||||
childAddLimitMode = childAddLimitMode
|
||||
).show(parentFragmentManager)
|
||||
AddAppActivitiesDialogFragment.newInstance(AddActivitiesParams(
|
||||
base = params,
|
||||
packageName = app.packageName
|
||||
)).show(parentFragmentManager)
|
||||
|
||||
dismissAllowingStateLoss()
|
||||
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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.apps.addactivity
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.timelimit.android.data.extensions.getCategoryWithParentCategories
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.mergeLiveDataWaitForValues
|
||||
import io.timelimit.android.livedata.switchMap
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
|
||||
class AddActivitiesModel(application: Application): AndroidViewModel(application) {
|
||||
private var didInit = false
|
||||
private val paramsLive = MutableLiveData<AddActivitiesParams>()
|
||||
|
||||
fun init(params: AddActivitiesParams) {
|
||||
if (didInit) return
|
||||
|
||||
paramsLive.value = params
|
||||
didInit = true
|
||||
}
|
||||
|
||||
val searchTerm = MutableLiveData<String>().apply { value = "" }
|
||||
|
||||
private val logic = DefaultAppLogic.with(application)
|
||||
|
||||
private val allActivitiesLive = paramsLive.switchMap { params ->
|
||||
logic.database.appActivity().getAppActivitiesByPackageName(params.packageName)
|
||||
}
|
||||
|
||||
private val userRelatedDataLive = paramsLive.switchMap { params ->
|
||||
logic.database.derivedDataDao().getUserRelatedDataLive(params.base.childId)
|
||||
}
|
||||
|
||||
private val installedAppsWithCurrentCategories = mergeLiveDataWaitForValues(paramsLive, allActivitiesLive, userRelatedDataLive)
|
||||
.map { (params, activities, userRelatedData) ->
|
||||
val categoryByAppSpecifier = userRelatedData?.categoryApps?.associateBy { it.appSpecifierString }?.mapValues {
|
||||
userRelatedData.categoryById.get(it.value.categoryId)
|
||||
} ?: emptyMap()
|
||||
|
||||
activities.map { activity ->
|
||||
val specifier = "${params.packageName}:${activity.activityClassName}"
|
||||
val category = categoryByAppSpecifier[specifier]?.category
|
||||
|
||||
AddActivityListItem(
|
||||
title = activity.title,
|
||||
className = activity.activityClassName,
|
||||
currentCategoryTitle = category?.title
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val shownActivities: LiveData<List<AddActivityListItem>> = mergeLiveDataWaitForValues(paramsLive, userRelatedDataLive, installedAppsWithCurrentCategories)
|
||||
.map { (params, userRelatedData, allActivities) ->
|
||||
if (params.base.isSelfLimitAddingMode) {
|
||||
if (userRelatedData == null || !userRelatedData.categoryById.containsKey(params.base.categoryId))
|
||||
emptyList()
|
||||
else {
|
||||
val parentCategories = userRelatedData.getCategoryWithParentCategories(params.base.categoryId)
|
||||
val defaultCategory = userRelatedData.categoryById[userRelatedData.user.categoryForNotAssignedApps]
|
||||
|
||||
val componentToCategoryApp = userRelatedData.categoryApps
|
||||
.filter { it.appSpecifier.packageName == params.packageName && it.appSpecifier.deviceId == null }
|
||||
.associateBy { it.appSpecifier.activityName ?: ":" }
|
||||
|
||||
val baseAppCategoryOrDefaultCategory =
|
||||
userRelatedData.categoryById[componentToCategoryApp[":"]?.categoryId]
|
||||
?: defaultCategory
|
||||
|
||||
val isBaseAppInParentCategory = parentCategories.contains(baseAppCategoryOrDefaultCategory?.category?.id)
|
||||
|
||||
allActivities.filter { activity ->
|
||||
val activityCategoryItem = userRelatedData.categoryById[componentToCategoryApp[activity.className]?.categoryId]
|
||||
val activityItselfInParentCategory = parentCategories.contains(activityCategoryItem?.category?.id)
|
||||
val activityItselfUnassigned = activityCategoryItem == null
|
||||
|
||||
(isBaseAppInParentCategory && activityItselfUnassigned) || activityItselfInParentCategory
|
||||
}
|
||||
}
|
||||
} else allActivities
|
||||
}
|
||||
|
||||
val filteredActivities = shownActivities.switchMap { activities ->
|
||||
searchTerm.map { term ->
|
||||
if (term.isEmpty()) {
|
||||
activities
|
||||
} else {
|
||||
activities.filter { it.className.contains(term, ignoreCase = true) or it.title.contains(term, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val emptyViewText = allActivitiesLive.switchMap { all ->
|
||||
shownActivities.switchMap { shown ->
|
||||
filteredActivities.map { filtered ->
|
||||
if (filtered.isNotEmpty())
|
||||
EmptyViewText.None
|
||||
else if (all.isNotEmpty())
|
||||
if (shown.isEmpty())
|
||||
EmptyViewText.EmptyShown
|
||||
else
|
||||
EmptyViewText.EmptyFiltered
|
||||
else /* (all.isEmpty()) */
|
||||
EmptyViewText.EmptyUnfiltered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class EmptyViewText {
|
||||
None,
|
||||
EmptyShown,
|
||||
EmptyFiltered,
|
||||
EmptyUnfiltered
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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.apps.addactivity
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.timelimit.android.ui.manage.category.apps.add.AddAppsParams
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class AddActivitiesParams (
|
||||
val base: AddAppsParams,
|
||||
val packageName: String
|
||||
): Parcelable
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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.apps.addactivity
|
||||
|
||||
data class AddActivityListItem (
|
||||
val title: String,
|
||||
val className: String,
|
||||
val currentCategoryTitle: String?
|
||||
)
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -21,173 +21,100 @@ import android.view.LayoutInflater
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.extensions.getCategoryWithParentCategories
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.databinding.FragmentAddCategoryActivitiesBinding
|
||||
import io.timelimit.android.extensions.addOnTextChangedListener
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.switchMap
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.sync.actions.AddCategoryAppsAction
|
||||
import io.timelimit.android.ui.main.getActivityViewModel
|
||||
import io.timelimit.android.ui.manage.category.apps.AddAppsOrActivitiesModel
|
||||
|
||||
class AddAppActivitiesDialogFragment: DialogFragment() {
|
||||
companion object {
|
||||
private const val DIALOG_TAG = "AddAppActivitiesDialogFragment"
|
||||
private const val CHILD_ID = "childId"
|
||||
private const val CATEGORY_ID = "categoryId"
|
||||
private const val PACKAGE_NAME = "packageName"
|
||||
private const val PARAMS = "params"
|
||||
private const val SELECTED_ACTIVITIES = "selectedActivities"
|
||||
private const val CHILD_ADD_LIMIT_MODE = "childAddLimitMode"
|
||||
|
||||
fun newInstance(childId: String, categoryId: String, packageName: String, childAddLimitMode: Boolean) = AddAppActivitiesDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(CHILD_ID, childId)
|
||||
putString(CATEGORY_ID, categoryId)
|
||||
putString(PACKAGE_NAME, packageName)
|
||||
putBoolean(CHILD_ADD_LIMIT_MODE, childAddLimitMode)
|
||||
}
|
||||
fun newInstance(params: AddActivitiesParams) = AddAppActivitiesDialogFragment().apply {
|
||||
arguments = Bundle().apply { putParcelable(PARAMS, params) }
|
||||
}
|
||||
}
|
||||
|
||||
val adapter = AddAppActivityAdapter()
|
||||
private val adapter = AddAppActivityAdapter()
|
||||
private val baseModel: AddAppsOrActivitiesModel by viewModels()
|
||||
private val model: AddActivitiesModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
adapter.selectedActiviities.clear()
|
||||
savedInstanceState.getStringArray(SELECTED_ACTIVITIES)!!.forEach { adapter.selectedActiviities.add(it) }
|
||||
adapter.selectedActivities.clear()
|
||||
savedInstanceState.getStringArray(SELECTED_ACTIVITIES)!!.forEach { adapter.selectedActivities.add(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putStringArray(SELECTED_ACTIVITIES, adapter.selectedActiviities.toTypedArray())
|
||||
outState.putStringArray(SELECTED_ACTIVITIES, adapter.selectedActivities.toTypedArray())
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val appPackageName = arguments!!.getString(PACKAGE_NAME)!!
|
||||
val categoryId = arguments!!.getString(CATEGORY_ID)!!
|
||||
val childId = arguments!!.getString(CHILD_ID)!!
|
||||
val childAddLimitMode = arguments!!.getBoolean(CHILD_ADD_LIMIT_MODE)
|
||||
val auth = getActivityViewModel(activity!!)
|
||||
val binding = FragmentAddCategoryActivitiesBinding.inflate(LayoutInflater.from(context!!))
|
||||
val searchTerm = MutableLiveData<String>().apply { value = binding.search.text.toString() }
|
||||
binding.search.addOnTextChangedListener { searchTerm.value = binding.search.text.toString() }
|
||||
val params = requireArguments().getParcelable<AddActivitiesParams>(PARAMS)!!
|
||||
val auth = getActivityViewModel(requireActivity())
|
||||
val binding = FragmentAddCategoryActivitiesBinding.inflate(LayoutInflater.from(context))
|
||||
|
||||
auth.authenticatedUserOrChild.observe(this, Observer {
|
||||
val parentAuthenticated = it?.second?.type == UserType.Parent
|
||||
val childAuthenticated = it?.second?.id == childId && childAddLimitMode
|
||||
val anyoneAuthenticated = parentAuthenticated || childAuthenticated
|
||||
baseModel.init(params.base)
|
||||
baseModel.isAuthValid(auth).observe(this) { if (!it) dismissAllowingStateLoss() }
|
||||
|
||||
if (!anyoneAuthenticated) {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
})
|
||||
model.init(params)
|
||||
model.searchTerm.value = binding.search.text.toString()
|
||||
binding.search.addOnTextChangedListener { model.searchTerm.value = binding.search.text.toString() }
|
||||
|
||||
val logic = DefaultAppLogic.with(context!!)
|
||||
val allActivitiesLive = logic.database.appActivity().getAppActivitiesByPackageName(appPackageName).map { activities ->
|
||||
activities.distinctBy { it.activityClassName }
|
||||
}
|
||||
|
||||
val userRelatedDataLive = logic.database.derivedDataDao().getUserRelatedDataLive(childId)
|
||||
|
||||
val shownActivities = if (childAddLimitMode) {
|
||||
userRelatedDataLive.switchMap { userRelatedData ->
|
||||
allActivitiesLive.map { allActivities ->
|
||||
if (userRelatedData == null || !userRelatedData.categoryById.containsKey(categoryId))
|
||||
emptyList()
|
||||
else {
|
||||
val parentCategories = userRelatedData.getCategoryWithParentCategories(categoryId)
|
||||
val defaultCategory = userRelatedData.categoryById[userRelatedData.user.categoryForNotAssignedApps]
|
||||
val relatedPackageNameToCategoryId = userRelatedData.categoryApps
|
||||
.filter { it.packageNameWithoutActivityName == appPackageName }
|
||||
.associateBy { it.packageName }
|
||||
val baseAppCategoryOrDefaultCategory = userRelatedData.categoryById[relatedPackageNameToCategoryId[appPackageName]?.categoryId] ?: defaultCategory
|
||||
val baseAppCategoryInParentCategoryOrMatchingUnassigned = parentCategories.contains(baseAppCategoryOrDefaultCategory?.category?.id)
|
||||
|
||||
allActivities.filter { activity ->
|
||||
val activityCategoryItem = userRelatedData.categoryById[relatedPackageNameToCategoryId[activity.appPackageName + ":" + activity.activityClassName]?.categoryId]
|
||||
val activityItselfInParentCategory = parentCategories.contains(activityCategoryItem?.category?.id)
|
||||
val activityItselfUnassigned = activityCategoryItem == null
|
||||
|
||||
(baseAppCategoryInParentCategoryOrMatchingUnassigned && activityItselfUnassigned) || activityItselfInParentCategory
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else allActivitiesLive
|
||||
|
||||
val filteredActivities = shownActivities.switchMap { activities ->
|
||||
searchTerm.map { term ->
|
||||
if (term.isEmpty()) {
|
||||
activities
|
||||
} else {
|
||||
activities.filter { it.activityClassName.contains(term, ignoreCase = true) or it.title.contains(term, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.recycler.layoutManager = LinearLayoutManager(context!!)
|
||||
binding.recycler.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recycler.adapter = adapter
|
||||
|
||||
filteredActivities.observe(this, Observer { list ->
|
||||
val selectedActivities = adapter.selectedActiviities
|
||||
val visibleActivities = list.map { it.activityClassName }
|
||||
model.filteredActivities.observe(this) { list ->
|
||||
val selectedActivities = adapter.selectedActivities
|
||||
val visibleActivities = list.map { it.className }
|
||||
val hiddenSelectedActivities = selectedActivities.toMutableSet().apply { removeAll(visibleActivities) }.size
|
||||
|
||||
adapter.data = list
|
||||
|
||||
binding.hiddenEntries = if (hiddenSelectedActivities == 0)
|
||||
null
|
||||
else
|
||||
resources.getQuantityString(R.plurals.category_apps_add_dialog_hidden_entries, hiddenSelectedActivities, hiddenSelectedActivities)
|
||||
})
|
||||
|
||||
val emptyViewText = allActivitiesLive.switchMap { all ->
|
||||
shownActivities.switchMap { shown ->
|
||||
filteredActivities.map { filtered ->
|
||||
if (filtered.isNotEmpty())
|
||||
null
|
||||
else if (all.isNotEmpty())
|
||||
if (shown.isEmpty())
|
||||
getString(R.string.category_apps_add_activity_empty_shown)
|
||||
else
|
||||
getString(R.string.category_apps_add_activity_empty_filtered)
|
||||
else /* (all.isEmpty()) */
|
||||
getString(R.string.category_apps_add_activity_empty_unfiltered)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emptyViewText.observe(this, Observer {
|
||||
binding.emptyViewText = it
|
||||
})
|
||||
|
||||
binding.someOptionsDisabledDueToChildAuthentication = childAddLimitMode
|
||||
model.emptyViewText.observe(this) {
|
||||
binding.emptyViewText = when (it!!) {
|
||||
AddActivitiesModel.EmptyViewText.None -> null
|
||||
AddActivitiesModel.EmptyViewText.EmptyShown -> getString(R.string.category_apps_add_activity_empty_shown)
|
||||
AddActivitiesModel.EmptyViewText.EmptyFiltered -> getString(R.string.category_apps_add_activity_empty_filtered)
|
||||
AddActivitiesModel.EmptyViewText.EmptyUnfiltered -> getString(R.string.category_apps_add_activity_empty_unfiltered)
|
||||
}
|
||||
}
|
||||
|
||||
binding.someOptionsDisabledDueToChildAuthentication = params.base.isSelfLimitAddingMode
|
||||
binding.cancelButton.setOnClickListener { dismissAllowingStateLoss() }
|
||||
binding.addActivitiesButton.setOnClickListener {
|
||||
if (adapter.selectedActiviities.isNotEmpty()) {
|
||||
if (adapter.selectedActivities.isNotEmpty()) {
|
||||
auth.tryDispatchParentAction(
|
||||
action = AddCategoryAppsAction(
|
||||
categoryId = categoryId,
|
||||
packageNames = adapter.selectedActiviities.toList().map { "$appPackageName:$it" }
|
||||
categoryId = params.base.categoryId,
|
||||
packageNames = adapter.selectedActivities.toList().map { "${params.packageName}:$it" }
|
||||
),
|
||||
allowAsChild = childAddLimitMode
|
||||
allowAsChild = params.base.isSelfLimitAddingMode
|
||||
)
|
||||
}
|
||||
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
return AlertDialog.Builder(context!!, R.style.AppTheme)
|
||||
return AlertDialog.Builder(requireContext(), R.style.AppTheme)
|
||||
.setView(binding.root)
|
||||
.create()
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -18,58 +18,54 @@ package io.timelimit.android.ui.manage.category.apps.addactivity
|
|||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.timelimit.android.data.model.AppActivity
|
||||
import io.timelimit.android.databinding.FragmentAddCategoryActivitiesItemBinding
|
||||
import io.timelimit.android.databinding.FragmentAddCategoryAppsItemBinding
|
||||
import io.timelimit.android.extensions.toggle
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class AddAppActivityAdapter: RecyclerView.Adapter<ViewHolder>() {
|
||||
var data: List<AppActivity>? by Delegates.observable(null as List<AppActivity>?) { _, _, _ -> notifyDataSetChanged() }
|
||||
val selectedActiviities = mutableSetOf<String>()
|
||||
var data: List<AddActivityListItem>? by Delegates.observable(emptyList()) { _, _, _ -> notifyDataSetChanged() }
|
||||
|
||||
private val itemHandlers = object: ItemHandlers {
|
||||
override fun onActivityClicked(activity: AppActivity) {
|
||||
selectedActiviities.toggle(activity.activityClassName)
|
||||
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
val selectedActivities = mutableSetOf<String>()
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
private fun getItem(position: Int): AppActivity {
|
||||
private fun getItem(position: Int): AddActivityListItem {
|
||||
return data!![position]
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return getItem(position).activityClassName.hashCode().toLong()
|
||||
return getItem(position).className.hashCode().toLong()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = this.data?.size ?: 0
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||
FragmentAddCategoryActivitiesItemBinding.inflate(
|
||||
FragmentAddCategoryAppsItemBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
).apply { handlers = itemHandlers }
|
||||
)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
|
||||
holder.apply {
|
||||
binding.item = item
|
||||
binding.checked = selectedActiviities.contains(item.activityClassName)
|
||||
binding.title = item.title
|
||||
binding.currentCategoryTitle = item.currentCategoryTitle
|
||||
binding.subtitle = item.className
|
||||
binding.showIcon = false
|
||||
binding.checked = selectedActivities.contains(item.className)
|
||||
binding.executePendingBindings()
|
||||
|
||||
binding.card.setOnClickListener {
|
||||
selectedActivities.toggle(item.className)
|
||||
binding.checked = selectedActivities.contains(item.className)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(val binding: FragmentAddCategoryActivitiesItemBinding): RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
interface ItemHandlers {
|
||||
fun onActivityClicked(activity: AppActivity)
|
||||
}
|
||||
class ViewHolder(val binding: FragmentAddCategoryAppsItemBinding): RecyclerView.ViewHolder(binding.root)
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -13,9 +13,7 @@
|
|||
* 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.apps
|
||||
|
||||
import io.timelimit.android.ui.manage.category.appsandrules.AppAndRuleItem
|
||||
package io.timelimit.android.ui.manage.category.appsandrules
|
||||
|
||||
interface AppAdapterHandlers {
|
||||
fun onAppClicked(app: AppAndRuleItem.AppEntry)
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -27,7 +27,6 @@ import io.timelimit.android.date.DateInTimezone
|
|||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.logic.DummyApps
|
||||
import io.timelimit.android.ui.manage.category.apps.AppAdapterHandlers
|
||||
import io.timelimit.android.ui.manage.category.timelimit_rules.TimeLimitRulesHandlers
|
||||
import io.timelimit.android.util.DayNameUtil
|
||||
import io.timelimit.android.util.TimeTextUtil
|
||||
|
@ -58,7 +57,7 @@ class AppAndRuleAdapter: RecyclerView.Adapter<AppAndRuleAdapter.Holder>() {
|
|||
|
||||
override fun getItemId(position: Int): Long = items[position].let { item ->
|
||||
when (item) {
|
||||
is AppAndRuleItem.AppEntry -> item.packageName.hashCode()
|
||||
is AppAndRuleItem.AppEntry -> item.specifier.hashCode()
|
||||
is AppAndRuleItem.RuleEntry -> item.rule.id.hashCode()
|
||||
else -> item.hashCode()
|
||||
}
|
||||
|
@ -137,16 +136,17 @@ class AppAndRuleAdapter: RecyclerView.Adapter<AppAndRuleAdapter.Holder>() {
|
|||
val binding = holder.itemView.tag as FragmentCategoryAppsItemBinding
|
||||
val context = binding.root.context
|
||||
|
||||
binding.item = item
|
||||
binding.handlers = handlers
|
||||
binding.title = item.title
|
||||
binding.deviceName = item.deviceName
|
||||
binding.subtitle = item.specifier.copy(deviceId = null).encode()
|
||||
binding.card.setOnClickListener { handlers?.onAppClicked(item) }
|
||||
binding.card.setOnLongClickListener { handlers?.onAppLongClicked(item) ?: false }
|
||||
binding.executePendingBindings()
|
||||
|
||||
binding.root.setOnLongClickListener { handlers?.onAppLongClicked(item) ?: false }
|
||||
|
||||
binding.icon.setImageDrawable(
|
||||
DummyApps.getIcon(item.packageNameWithoutActivityName, context) ?:
|
||||
DummyApps.getIcon(item.specifier.packageName, context) ?:
|
||||
DefaultAppLogic.with(context)
|
||||
.platformIntegration.getAppIcon(item.packageNameWithoutActivityName)
|
||||
.platformIntegration.getAppIcon(item.specifier.packageName)
|
||||
)
|
||||
}
|
||||
AppAndRuleItem.AddAppItem -> {/* nothing to do */}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -17,9 +17,10 @@
|
|||
package io.timelimit.android.ui.manage.category.appsandrules
|
||||
|
||||
import io.timelimit.android.data.model.TimeLimitRule
|
||||
import io.timelimit.android.data.model.derived.AppSpecifier
|
||||
|
||||
sealed class AppAndRuleItem {
|
||||
data class AppEntry(val title: String, val packageName: String, val packageNameWithoutActivityName: String): AppAndRuleItem()
|
||||
data class AppEntry(val title: String, val deviceName: String?, val specifier: AppSpecifier): AppAndRuleItem()
|
||||
object AddAppItem: AppAndRuleItem()
|
||||
object ExpandAppsItem: AppAndRuleItem()
|
||||
data class RuleEntry(val rule: TimeLimitRule): AppAndRuleItem()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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,6 +24,7 @@ import io.timelimit.android.data.extensions.getDateLive
|
|||
import io.timelimit.android.data.model.HintsToShow
|
||||
import io.timelimit.android.extensions.takeDistributedElements
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.mergeLiveDataWaitForValues
|
||||
import io.timelimit.android.livedata.switchMap
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.logic.DummyApps
|
||||
|
@ -108,27 +109,30 @@ class AppsAndRulesModel(application: Application): AndroidViewModel(application)
|
|||
|
||||
private val installedApps = database.app().getAllApps()
|
||||
|
||||
private val installedAppsIndexed = installedApps.map { apps -> apps.associateBy { it.packageName } }
|
||||
|
||||
private val deviceNamesIndexedLive = database.device().getDeviceNamesLive().map { items -> items.associateBy { it.id } }
|
||||
|
||||
private val appsOfThisCategory = categoryIdLive.switchMap { categoryId -> database.categoryApp().getCategoryApps(categoryId) }
|
||||
|
||||
private val appsOfCategoryWithNames = installedApps.switchMap { allApps ->
|
||||
appsOfThisCategory.map { apps ->
|
||||
apps.map { categoryApp ->
|
||||
val title = DummyApps.getTitle(categoryApp.packageNameWithoutActivityName, getApplication()) ?:
|
||||
allApps.find { app -> app.packageName == categoryApp.packageNameWithoutActivityName }?.title
|
||||
private val appsOfCategoryWithNames = mergeLiveDataWaitForValues(installedAppsIndexed, appsOfThisCategory, deviceNamesIndexedLive)
|
||||
.map { (allAppsIndexed, appsOfThisCategory, deviceNamesIndexed) ->
|
||||
appsOfThisCategory.map { categoryApp ->
|
||||
val title = DummyApps.getTitle(categoryApp.appSpecifier.packageName, getApplication()) ?:
|
||||
allAppsIndexed[categoryApp.appSpecifier.packageName]?.title
|
||||
|
||||
categoryApp to title
|
||||
AppAndRuleItem.AppEntry(
|
||||
title = title ?: "app not found",
|
||||
specifier = categoryApp.appSpecifier,
|
||||
deviceName = categoryApp.appSpecifier.deviceId?.let { deviceId ->
|
||||
deviceNamesIndexed[deviceId]?.name ?: "removed device"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val appEntries = appsOfCategoryWithNames.map { apps ->
|
||||
apps.map { (app, title) ->
|
||||
if (title != null) {
|
||||
AppAndRuleItem.AppEntry(title, app.packageName, app.packageNameWithoutActivityName)
|
||||
} else {
|
||||
AppAndRuleItem.AppEntry("app not found", app.packageName, app.packageNameWithoutActivityName)
|
||||
}
|
||||
}.sortedBy { it.title.toLowerCase(Locale.US) }
|
||||
apps.sortedBy { it.title.lowercase() }
|
||||
}
|
||||
|
||||
private val fullAppScreenContent = showAllAppsLive.switchMap { showAllApps ->
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -39,6 +39,7 @@ import io.timelimit.android.sync.actions.RemoveCategoryAppsAction
|
|||
import io.timelimit.android.sync.actions.UpdateTimeLimitRuleAction
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.main.getActivityViewModel
|
||||
import io.timelimit.android.ui.manage.category.apps.add.AddAppsParams
|
||||
import io.timelimit.android.ui.manage.category.apps.add.AddCategoryAppsFragment
|
||||
import io.timelimit.android.ui.manage.category.timelimit_rules.edit.EditTimeLimitRuleDialogFragment
|
||||
import io.timelimit.android.ui.manage.category.timelimit_rules.edit.EditTimeLimitRuleDialogFragmentListener
|
||||
|
@ -135,7 +136,7 @@ abstract class CategoryAppsAndRulesFragment: Fragment(), Handlers, EditTimeLimit
|
|||
if (auth.tryDispatchParentAction(
|
||||
RemoveCategoryAppsAction(
|
||||
categoryId = categoryId,
|
||||
packageNames = listOf(app.packageName)
|
||||
packageNames = listOf(app.specifier.encode())
|
||||
)
|
||||
)) {
|
||||
Snackbar.make(requireView(), getString(R.string.category_apps_item_removed_toast, app.title), Snackbar.LENGTH_SHORT)
|
||||
|
@ -143,7 +144,7 @@ abstract class CategoryAppsAndRulesFragment: Fragment(), Handlers, EditTimeLimit
|
|||
auth.tryDispatchParentAction(
|
||||
AddCategoryAppsAction(
|
||||
categoryId = categoryId,
|
||||
packageNames = listOf(app.packageName)
|
||||
packageNames = listOf(app.specifier.encode())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -155,7 +156,7 @@ abstract class CategoryAppsAndRulesFragment: Fragment(), Handlers, EditTimeLimit
|
|||
return if (auth.requestAuthenticationOrReturnTrue()) {
|
||||
AssignAppCategoryDialogFragment.newInstance(
|
||||
childId = childId,
|
||||
appPackageName = app.packageName
|
||||
appPackageName = app.specifier.encode()
|
||||
).show(parentFragmentManager)
|
||||
|
||||
true
|
||||
|
@ -164,11 +165,11 @@ abstract class CategoryAppsAndRulesFragment: Fragment(), Handlers, EditTimeLimit
|
|||
|
||||
override fun onAddAppsClicked() {
|
||||
if (auth.requestAuthenticationOrReturnTrueAllowChild(childId = childId)) {
|
||||
AddCategoryAppsFragment.newInstance(
|
||||
AddCategoryAppsFragment.newInstance(AddAppsParams(
|
||||
childId = childId,
|
||||
categoryId = categoryId,
|
||||
childAddLimitMode = !auth.isParentAuthenticated()
|
||||
).show(parentFragmentManager)
|
||||
isSelfLimitAddingMode = !auth.isParentAuthenticated()
|
||||
)).show(parentFragmentManager)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@ object DuplicateChildActions {
|
|||
|
||||
result.add(AddCategoryAppsAction(
|
||||
categoryId = newCategoryId,
|
||||
packageNames = oldApps.map { it.packageName }
|
||||
packageNames = oldApps.map { it.appSpecifierString }
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -64,13 +64,17 @@ class ChildAppsModel(application: Application): AndroidViewModel(application) {
|
|||
val listContentLive = childAppsLive.switchMap { childApps ->
|
||||
childCategoriesLive.switchMap { categories ->
|
||||
childCategoryAppsLive.switchMap { categoryApps ->
|
||||
// only show items that are not device specific
|
||||
val categoryAppByPackageName = categoryApps
|
||||
.filter { it.appSpecifier.deviceId == null }
|
||||
.associateBy { it.appSpecifier.packageName }
|
||||
|
||||
appFilterLive.ignoreUnchanged().switchMap { appFilter ->
|
||||
val filteredChildApps = childApps.filter { appFilter.matches(it) }
|
||||
|
||||
modeLive.ignoreUnchanged().map { mode ->
|
||||
when (mode!!) {
|
||||
ChildAppsMode.SortByCategory -> {
|
||||
val categoryAppByPackageName = categoryApps.associateBy { it.packageName }
|
||||
val appsByCategoryId = filteredChildApps.groupBy { app ->
|
||||
categoryAppByPackageName[app.packageName]?.categoryId
|
||||
}
|
||||
|
@ -117,7 +121,6 @@ class ChildAppsModel(application: Application): AndroidViewModel(application) {
|
|||
}
|
||||
ChildAppsMode.SortByTitle -> {
|
||||
val categoryById = categories.associateBy { it.id }
|
||||
val categoryAppByPackageName = categoryApps.associateBy { it.packageName }
|
||||
|
||||
filteredChildApps
|
||||
.distinctBy { it.packageName }
|
||||
|
|
|
@ -194,7 +194,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
|
||||
val appsAssignedToTheUser = categoriesOfTheSelectedUser.switchMap { categories ->
|
||||
logic.database.categoryApp().getCategoryApps(categories.map { it.id }).map { categoryApps ->
|
||||
categoryApps.map { it.packageName }.toSet()
|
||||
categoryApps.map { it.appSpecifierString }.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 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
|
||||
|
@ -132,7 +132,8 @@ class SetupDeviceModel(application: Application): AndroidViewModel(application)
|
|||
|
||||
val alreadyAssignedApps = Threads.database.executeAndWait {
|
||||
logic.database.categoryApp().getCategoryAppsByUserIdSync(realUserId)
|
||||
.map { it.packageName }
|
||||
.filter { it.appSpecifier.deviceId == null }
|
||||
.map { it.appSpecifier.packageName }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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/>.
|
||||
-->
|
||||
<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="item"
|
||||
type="io.timelimit.android.data.model.AppActivity" />
|
||||
|
||||
<variable
|
||||
name="currentCategoryTitle"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="handlers"
|
||||
type="io.timelimit.android.ui.manage.category.apps.addactivity.ItemHandlers" />
|
||||
|
||||
<variable
|
||||
name="checked"
|
||||
type="Boolean" />
|
||||
|
||||
<import type="android.view.View" />
|
||||
<import type="android.text.TextUtils" />
|
||||
</data>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:onClick="@{() -> handlers.onActivityClicked(item)}"
|
||||
android:foreground="?selectableItemBackground"
|
||||
app:cardUseCompatPadding="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_weight="1"
|
||||
android:padding="8dp"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
tools:text="Android Settings"
|
||||
android:text="@{item.title}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:visibility="@{TextUtils.isEmpty(currentCategoryTitle) ? View.GONE : View.VISIBLE}"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
tools:text="@string/category_apps_add_dialog_already_assigned_to"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:text="@{@string/category_apps_add_dialog_already_assigned_to(currentCategoryTitle)}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
tools:text="com.android.settings"
|
||||
android:text="@{item.activityClassName}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:visibility="@{safeUnbox(checked) ? View.VISIBLE : View.GONE}"
|
||||
android:tint="?colorPrimary"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@drawable/ic_check_box_black_24dp"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp" />
|
||||
|
||||
<ImageView
|
||||
android:visibility="@{safeUnbox(checked) ? View.GONE : View.VISIBLE}"
|
||||
android:tint="@color/gray"
|
||||
android:layout_margin="8dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@drawable/ic_check_box_outline_blank_black_24dp"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</layout>
|
|
@ -1,5 +1,5 @@
|
|||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
TimeLimit Copyright <C> 2019 - 2022 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.
|
||||
|
@ -71,6 +71,13 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<CheckBox
|
||||
android:checked="false"
|
||||
android:id="@+id/assign_to_this_device_only"
|
||||
android:text="@string/category_apps_add_dialog_assign_current_device_only"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
TimeLimit Copyright <C> 2019 - 2022 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.
|
||||
|
@ -18,28 +18,31 @@
|
|||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<data>
|
||||
<variable
|
||||
name="item"
|
||||
type="io.timelimit.android.data.model.App" />
|
||||
name="title"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="subtitle"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="currentCategoryTitle"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="handlers"
|
||||
type="io.timelimit.android.ui.manage.category.apps.add.AddAppAdapterListener" />
|
||||
|
||||
<variable
|
||||
name="checked"
|
||||
type="Boolean" />
|
||||
|
||||
<variable
|
||||
name="showIcon"
|
||||
type="boolean" />
|
||||
|
||||
<import type="android.view.View" />
|
||||
<import type="android.text.TextUtils" />
|
||||
</data>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:onClick="@{() -> handlers.onAppClicked(item)}"
|
||||
android:onLongClick="@{() -> handlers.onAppLongClicked(item)}"
|
||||
android:id="@+id/card"
|
||||
android:foreground="?selectableItemBackground"
|
||||
app:cardUseCompatPadding="true"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -50,6 +53,7 @@
|
|||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:visibility="@{showIcon ? View.VISIBLE : View.GONE}"
|
||||
android:id="@+id/icon"
|
||||
tools:src="@mipmap/ic_launcher"
|
||||
android:layout_margin="8dp"
|
||||
|
@ -66,7 +70,7 @@
|
|||
<TextView
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
tools:text="Android Settings"
|
||||
android:text="@{item.title}"
|
||||
android:text="@{title}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
|
@ -82,7 +86,7 @@
|
|||
<TextView
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
tools:text="com.android.settings"
|
||||
android:text="@{item.packageName}"
|
||||
android:text="@{subtitle}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
TimeLimit Copyright <C> 2019 - 2022 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.
|
||||
|
@ -18,18 +18,23 @@
|
|||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<data>
|
||||
<variable
|
||||
name="item"
|
||||
type="io.timelimit.android.ui.manage.category.appsandrules.AppAndRuleItem.AppEntry" />
|
||||
name="title"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="handlers"
|
||||
type="io.timelimit.android.ui.manage.category.apps.AppAdapterHandlers" />
|
||||
name="deviceName"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="subtitle"
|
||||
type="String" />
|
||||
|
||||
<import type="android.text.TextUtils" />
|
||||
<import type="android.view.View" />
|
||||
</data>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:onClick="@{() -> handlers.onAppClicked(item)}"
|
||||
android:id="@+id/card"
|
||||
android:foreground="?selectableItemBackground"
|
||||
app:cardUseCompatPadding="true"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -56,14 +61,23 @@
|
|||
<TextView
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
tools:text="Android Settings"
|
||||
android:text="@{item.title}"
|
||||
android:text="@{title}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:textColor="?colorAccent"
|
||||
tools:text="AVD 1"
|
||||
android:text="@{deviceName}"
|
||||
android:visibility="@{TextUtils.isEmpty(deviceName) ? View.GONE : View.VISIBLE}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
tools:text="com.android.settings"
|
||||
android:text="@{item.packageName}"
|
||||
android:text="@{subtitle}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
|
|
|
@ -241,6 +241,7 @@
|
|||
<string name="category_apps_add_dialog_select_all">Alle auswählen</string>
|
||||
<string name="category_apps_add_dialog_show_assigned_to_other_category">Zu anderen Kategorien zugeordnete Apps anzeigen</string>
|
||||
<string name="category_apps_add_dialog_show_from_other_devices">Apps von Geräten, die nicht diesem Kind zugeordnet wurden, anzeigen</string>
|
||||
<string name="category_apps_add_dialog_assign_current_device_only">Nur für dieses Gerät zuweisen</string>
|
||||
<plurals name="category_apps_add_dialog_hidden_entries">
|
||||
<item quantity="one">%d ausgeblendeten Eintrag ausgewählt</item>
|
||||
<item quantity="other">%s ausgeblendete Einträge ausgewählt</item>
|
||||
|
|
|
@ -281,6 +281,7 @@
|
|||
<string name="category_apps_add_dialog_show_sys_apps">Show system Apps</string>
|
||||
<string name="category_apps_add_dialog_show_assigned_to_other_category">Show Apps assigned to other categories</string>
|
||||
<string name="category_apps_add_dialog_show_from_other_devices">Show Apps from devices not assigned to this child</string>
|
||||
<string name="category_apps_add_dialog_assign_current_device_only">Assign for this device only</string>
|
||||
<string name="category_apps_add_dialog_select_all">Select all</string>
|
||||
<plurals name="category_apps_add_dialog_hidden_entries">
|
||||
<item quantity="one">%d hidden entry selected</item>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue