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
|
"notNull": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "packageName",
|
"fieldPath": "appSpecifierString",
|
||||||
"columnName": "package_name",
|
"columnName": "package_name",
|
||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": true
|
"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
|
package io.timelimit.android.data.dao
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
@ -6,6 +21,7 @@ import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import io.timelimit.android.data.model.AppActivity
|
import io.timelimit.android.data.model.AppActivity
|
||||||
|
import io.timelimit.android.data.model.AppActivityTitleAndClassNameItem
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface AppActivityDao {
|
interface AppActivityDao {
|
||||||
|
@ -15,8 +31,8 @@ interface AppActivityDao {
|
||||||
@Query("SELECT * FROM app_activity WHERE device_id IN (:deviceIds)")
|
@Query("SELECT * FROM app_activity WHERE device_id IN (:deviceIds)")
|
||||||
fun getAppActivitiesByDeviceIds(deviceIds: List<String>): LiveData<List<AppActivity>>
|
fun getAppActivitiesByDeviceIds(deviceIds: List<String>): LiveData<List<AppActivity>>
|
||||||
|
|
||||||
@Query("SELECT * FROM app_activity WHERE app_package_name = :packageName")
|
@Query("SELECT DISTINCT activity_class_name, activity_title FROM app_activity WHERE app_package_name = :packageName")
|
||||||
fun getAppActivitiesByPackageName(packageName: String): LiveData<List<AppActivity>>
|
fun getAppActivitiesByPackageName(packageName: String): LiveData<List<AppActivityTitleAndClassNameItem>>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun addAppActivitySync(item: AppActivity)
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -17,9 +17,7 @@ package io.timelimit.android.data.dao
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import io.timelimit.android.data.model.Device
|
import io.timelimit.android.data.model.*
|
||||||
import io.timelimit.android.data.model.NetworkTime
|
|
||||||
import io.timelimit.android.data.model.NetworkTimeAdapter
|
|
||||||
import io.timelimit.android.integration.platform.NewPermissionStatusConverter
|
import io.timelimit.android.integration.platform.NewPermissionStatusConverter
|
||||||
import io.timelimit.android.integration.platform.ProtectionLevelConverter
|
import io.timelimit.android.integration.platform.ProtectionLevelConverter
|
||||||
import io.timelimit.android.integration.platform.RuntimePermissionStatusConverter
|
import io.timelimit.android.integration.platform.RuntimePermissionStatusConverter
|
||||||
|
@ -41,6 +39,9 @@ abstract class DeviceDao {
|
||||||
@Query("SELECT * FROM device ORDER BY id")
|
@Query("SELECT * FROM device ORDER BY id")
|
||||||
abstract fun getAllDevicesLive(): LiveData<List<Device>>
|
abstract fun getAllDevicesLive(): LiveData<List<Device>>
|
||||||
|
|
||||||
|
@Query("SELECT id, name FROM device")
|
||||||
|
abstract fun getDeviceNamesLive(): LiveData<List<DeviceName>>
|
||||||
|
|
||||||
@Query("SELECT * FROM device")
|
@Query("SELECT * FROM device")
|
||||||
abstract fun getAllDevicesSync(): List<Device>
|
abstract fun getAllDevicesSync(): List<Device>
|
||||||
|
|
||||||
|
@ -68,6 +69,9 @@ abstract class DeviceDao {
|
||||||
@Query("SELECT * FROM device WHERE current_user_id = :userId")
|
@Query("SELECT * FROM device WHERE current_user_id = :userId")
|
||||||
abstract fun getDevicesByUserId(userId: String): LiveData<List<Device>>
|
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 = \"\"")
|
@Query("UPDATE device SET apps_version = \"\"")
|
||||||
abstract fun deleteAllInstalledAppsVersions()
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -21,13 +21,14 @@ import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import io.timelimit.android.data.IdGenerator
|
import io.timelimit.android.data.IdGenerator
|
||||||
import io.timelimit.android.data.JsonSerializable
|
import io.timelimit.android.data.JsonSerializable
|
||||||
|
import io.timelimit.android.data.model.derived.AppSpecifier
|
||||||
|
|
||||||
@Entity(primaryKeys = ["category_id", "package_name"], tableName = "category_app")
|
@Entity(primaryKeys = ["category_id", "package_name"], tableName = "category_app")
|
||||||
data class CategoryApp(
|
data class CategoryApp(
|
||||||
@ColumnInfo(index = true, name = "category_id")
|
@ColumnInfo(index = true, name = "category_id")
|
||||||
val categoryId: String,
|
val categoryId: String,
|
||||||
@ColumnInfo(index = true, name = "package_name")
|
@ColumnInfo(index = true, name = "package_name")
|
||||||
val packageName: String
|
val appSpecifierString: String // originally a packageName, but can contain more than that
|
||||||
): JsonSerializable {
|
): JsonSerializable {
|
||||||
companion object {
|
companion object {
|
||||||
private const val CATEGORY_ID = "c"
|
private const val CATEGORY_ID = "c"
|
||||||
|
@ -51,26 +52,18 @@ data class CategoryApp(
|
||||||
|
|
||||||
return CategoryApp(
|
return CategoryApp(
|
||||||
categoryId = categoryId!!,
|
categoryId = categoryId!!,
|
||||||
packageName = packageName!!
|
appSpecifierString = packageName!!
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@delegate:Transient
|
@delegate:Transient
|
||||||
val packageNameWithoutActivityName: String by lazy {
|
val appSpecifier: AppSpecifier by lazy { AppSpecifier.decode(appSpecifierString) }
|
||||||
if (specifiesActivity)
|
|
||||||
packageName.substring(0, packageName.indexOf(":"))
|
|
||||||
else
|
|
||||||
packageName
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transient
|
|
||||||
val specifiesActivity = packageName.contains(":")
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
IdGenerator.assertIdValid(categoryId)
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
|
||||||
if (packageName.isEmpty()) {
|
if (appSpecifierString.isEmpty()) {
|
||||||
throw IllegalArgumentException()
|
throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,7 +72,7 @@ data class CategoryApp(
|
||||||
writer.beginObject()
|
writer.beginObject()
|
||||||
|
|
||||||
writer.name(CATEGORY_ID).value(categoryId)
|
writer.name(CATEGORY_ID).value(categoryId)
|
||||||
writer.name(PACKAGE_NAME).value(packageName)
|
writer.name(PACKAGE_NAME).value(appSpecifierString)
|
||||||
|
|
||||||
writer.endObject()
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -34,7 +34,7 @@ data class UserRelatedData(
|
||||||
val categoryApps: List<CategoryApp>
|
val categoryApps: List<CategoryApp>
|
||||||
): Observer {
|
): Observer {
|
||||||
companion object {
|
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(
|
private val relatedTables = arrayOf(
|
||||||
Table.User, Table.Category, Table.TimeLimitRule,
|
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
|
// notFoundCategoryApp is a workaround because the lru cache does not support null
|
||||||
private val categoryAppLruCache = object: LruCache<String, CategoryApp>(8) {
|
private val categoryAppLruCache = object: LruCache<String, CategoryApp>(8) {
|
||||||
override fun create(key: String): CategoryApp {
|
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? {
|
private fun findCategoryApp(appSpecifier: AppSpecifier): CategoryApp? {
|
||||||
val item = categoryAppLruCache[packageName]
|
val item = categoryAppLruCache[appSpecifier.encode()]
|
||||||
|
|
||||||
// important: strict equality/ same object instance
|
// important: strict equality/ same object instance
|
||||||
if (item === notFoundCategoryApp) {
|
if (item === notFoundCategoryApp) {
|
||||||
|
@ -85,6 +85,15 @@ data class UserRelatedData(
|
||||||
return item
|
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 userInvalidated = false
|
||||||
private var categoriesInvalidated = false
|
private var categoriesInvalidated = false
|
||||||
|
|
|
@ -223,7 +223,7 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
||||||
.map {
|
.map {
|
||||||
CategoryApp(
|
CategoryApp(
|
||||||
categoryId = allowedAppsCategoryId,
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -152,7 +152,7 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
|
||||||
lastDefaultCategory = defaultCategory
|
lastDefaultCategory = defaultCategory
|
||||||
|
|
||||||
val installedApps = appLogic.platformIntegration.getLocalAppPackageNames()
|
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>()
|
val appsToBlock = mutableListOf<String>()
|
||||||
|
|
||||||
installedApps.forEach { packageName ->
|
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 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) {
|
if (blockingAtActivityLevel) {
|
||||||
val categoriesByPackageName = data.categoryApps.groupBy { it.packageNameWithoutActivityName }
|
val categoriesByPackageName = effectiveCategoryApps.groupBy { it.appSpecifier.packageName }
|
||||||
|
|
||||||
val result = mutableMapOf<String, Set<String>>()
|
val result = mutableMapOf<String, Set<String>>()
|
||||||
|
|
||||||
packageNames.forEach { packageName ->
|
packageNames.forEach { packageName ->
|
||||||
val categoriesItems = categoriesByPackageName[packageName]
|
val categoriesItems = categoriesByPackageName[packageName]
|
||||||
val categories = (categoriesItems?.map { it.categoryId }?.toSet() ?: emptySet()).toMutableSet()
|
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 (!isMainAppIncluded) {
|
||||||
if (categoryForOtherSystemApps != null && appLogic.platformIntegration.isSystemImageApp(packageName)) {
|
if (categoryForOtherSystemApps != null && appLogic.platformIntegration.isSystemImageApp(packageName)) {
|
||||||
|
@ -196,7 +209,9 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
|
||||||
|
|
||||||
return result
|
return result
|
||||||
} else {
|
} 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>>()
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -73,15 +73,31 @@ sealed class AppBaseHandling {
|
||||||
} else if (foregroundAppPackageName != null) {
|
} else if (foregroundAppPackageName != null) {
|
||||||
val appCategory = run {
|
val appCategory = run {
|
||||||
val tryActivityLevelBlocking = deviceRelatedData.deviceEntry.enableActivityLevelBlocking && foregroundAppActivityName != null
|
val tryActivityLevelBlocking = deviceRelatedData.deviceEntry.enableActivityLevelBlocking && foregroundAppActivityName != null
|
||||||
val appLevelCategory = userRelatedData.findCategoryApp(foregroundAppPackageName) ?: run {
|
val appLevelCategory = userRelatedData.findCategoryAppTryDeviceSpecificFirst(
|
||||||
if (isSystemImageApp) userRelatedData.findCategoryApp(DummyApps.NOT_ASSIGNED_SYSTEM_IMAGE_APP) else null
|
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) {
|
if (tryActivityLevelBlocking) {
|
||||||
userRelatedData.findCategoryApp("$foregroundAppPackageName:$foregroundAppActivityName")
|
val activityLevelCategory = userRelatedData.findCategoryAppTryDeviceSpecificFirst(
|
||||||
} else {
|
packageName = foregroundAppPackageName,
|
||||||
null
|
activityName = foregroundAppActivityName,
|
||||||
}) ?: appLevelCategory
|
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]
|
val startCategory = userRelatedData.categoryById[appCategory?.categoryId]
|
||||||
|
@ -95,8 +111,7 @@ sealed class AppBaseHandling {
|
||||||
return UseCategories(
|
return UseCategories(
|
||||||
categoryIds = categoryIds,
|
categoryIds = categoryIds,
|
||||||
shouldCount = !pauseCounting,
|
shouldCount = !pauseCounting,
|
||||||
level = when (appCategory?.specifiesActivity) {
|
level = when (appCategory == null || appCategory.appSpecifier.activityName != null) {
|
||||||
null -> BlockingLevel.Activity // occurs when using a default category
|
|
||||||
true -> BlockingLevel.Activity
|
true -> BlockingLevel.Activity
|
||||||
false -> BlockingLevel.App
|
false -> BlockingLevel.App
|
||||||
},
|
},
|
||||||
|
|
|
@ -458,7 +458,7 @@ object ApplyServerDataStatus {
|
||||||
database.categoryApp().addCategoryAppsSync(item.assignedApps.map {
|
database.categoryApp().addCategoryAppsSync(item.assignedApps.map {
|
||||||
CategoryApp(
|
CategoryApp(
|
||||||
categoryId = item.categoryId,
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -53,6 +53,10 @@ object LocalDatabaseParentActionDispatcher {
|
||||||
val allCategoriesOfChild = database.category().getCategoriesByChildIdSync(categoryEntry.childId)
|
val allCategoriesOfChild = database.category().getCategoriesByChildIdSync(categoryEntry.childId)
|
||||||
|
|
||||||
if (fromChildSelfLimitAddChildUserId != null) {
|
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 parentCategoriesOfTargetCategory = allCategoriesOfChild.getCategoryWithParentCategories(action.categoryId)
|
||||||
val userEntry = database.user().getUserByIdSync(fromChildSelfLimitAddChildUserId) ?: throw RuntimeException("user not found")
|
val userEntry = database.user().getUserByIdSync(fromChildSelfLimitAddChildUserId) ?: throw RuntimeException("user not found")
|
||||||
val validatedDefaultCategoryId = (allCategoriesOfChild.find {
|
val validatedDefaultCategoryId = (allCategoriesOfChild.find {
|
||||||
|
@ -99,7 +103,7 @@ object LocalDatabaseParentActionDispatcher {
|
||||||
action.packageNames.map {
|
action.packageNames.map {
|
||||||
CategoryApp(
|
CategoryApp(
|
||||||
categoryId = action.categoryId,
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,39 +18,23 @@ package io.timelimit.android.ui.manage.category.apps.add
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.timelimit.android.data.model.App
|
|
||||||
import io.timelimit.android.databinding.FragmentAddCategoryAppsItemBinding
|
import io.timelimit.android.databinding.FragmentAddCategoryAppsItemBinding
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.logic.DummyApps
|
import io.timelimit.android.logic.DummyApps
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
class AddAppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
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 listener: AddAppAdapterListener? = null
|
||||||
var categoryTitleByPackageName: Map<String, String> by Delegates.observable(emptyMap()) { _, _, _ -> notifyDataSetChanged() }
|
|
||||||
var selectedApps: Set<String> by Delegates.observable(emptySet()) { _, _, _ -> notifyDataSetChanged() }
|
var selectedApps: Set<String> by Delegates.observable(emptySet()) { _, _, _ -> notifyDataSetChanged() }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setHasStableIds(true)
|
setHasStableIds(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getItem(position: Int): App {
|
private fun getItem(position: Int): AddAppListItem = data[position]
|
||||||
return data!![position]
|
override fun getItemId(position: Int): Long = getItem(position).hashCode().toLong()
|
||||||
}
|
override fun getItemCount(): Int = data.size
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||||
FragmentAddCategoryAppsItemBinding.inflate(
|
FragmentAddCategoryAppsItemBinding.inflate(
|
||||||
|
@ -65,10 +49,13 @@ class AddAppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
||||||
val context = holder.itemView.context
|
val context = holder.itemView.context
|
||||||
|
|
||||||
holder.apply {
|
holder.apply {
|
||||||
binding.item = item
|
binding.title = item.title
|
||||||
|
binding.subtitle = item.packageName
|
||||||
|
binding.currentCategoryTitle = item.currentCategoryName
|
||||||
binding.checked = selectedApps.contains(item.packageName)
|
binding.checked = selectedApps.contains(item.packageName)
|
||||||
binding.currentCategoryTitle = categoryTitleByPackageName[item.packageName]
|
binding.card.setOnClickListener { listener?.onAppClicked(item) }
|
||||||
binding.handlers = listener
|
binding.card.setOnLongClickListener { listener?.onAppLongClicked(item) ?: false }
|
||||||
|
binding.showIcon = true
|
||||||
binding.executePendingBindings()
|
binding.executePendingBindings()
|
||||||
|
|
||||||
binding.icon.setImageDrawable(
|
binding.icon.setImageDrawable(
|
||||||
|
@ -83,6 +70,6 @@ class AddAppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
||||||
class ViewHolder(val binding: FragmentAddCategoryAppsItemBinding): RecyclerView.ViewHolder(binding.root)
|
class ViewHolder(val binding: FragmentAddCategoryAppsItemBinding): RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
interface AddAppAdapterListener {
|
interface AddAppAdapterListener {
|
||||||
fun onAppClicked(app: App)
|
fun onAppClicked(app: AddAppListItem)
|
||||||
fun onAppLongClicked(app: App): Boolean
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.ui.manage.category.apps.add
|
package io.timelimit.android.ui.manage.category.apps.add
|
||||||
|
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -27,24 +26,18 @@ import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
import com.google.android.material.appbar.AppBarLayout
|
||||||
import io.timelimit.android.R
|
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.databinding.FragmentAddCategoryAppsBinding
|
||||||
import io.timelimit.android.extensions.showSafe
|
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.sync.actions.AddCategoryAppsAction
|
||||||
import io.timelimit.android.ui.main.ActivityViewModel
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
import io.timelimit.android.ui.main.getActivityViewModel
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
|
import io.timelimit.android.ui.manage.category.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.manage.category.apps.addactivity.AddAppActivitiesDialogFragment
|
||||||
import io.timelimit.android.ui.view.AppFilterView
|
import io.timelimit.android.ui.view.AppFilterView
|
||||||
|
|
||||||
|
@ -53,23 +46,18 @@ class AddCategoryAppsFragment : DialogFragment() {
|
||||||
private const val DIALOG_TAG = "x"
|
private const val DIALOG_TAG = "x"
|
||||||
private const val STATUS_PACKAGE_NAMES = "d"
|
private const val STATUS_PACKAGE_NAMES = "d"
|
||||||
private const val STATUS_EDUCATED = "e"
|
private const val STATUS_EDUCATED = "e"
|
||||||
private const val PARAM_CHILD_ID = "childId"
|
private const val PARAMS = "params"
|
||||||
private const val PARAM_CATEGORY_ID = "categoryId"
|
|
||||||
private const val PARAM_CHILD_ADD_LIMIT_MODE = "addLimitMode"
|
|
||||||
|
|
||||||
fun newInstance(childId: String, categoryId: String, childAddLimitMode: Boolean) = AddCategoryAppsFragment().apply {
|
fun newInstance(params: AddAppsParams) = AddCategoryAppsFragment().apply {
|
||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply { putParcelable(PARAMS, params) }
|
||||||
putString(PARAM_CHILD_ID, childId)
|
|
||||||
putString(PARAM_CATEGORY_ID, categoryId)
|
|
||||||
putBoolean(PARAM_CHILD_ADD_LIMIT_MODE, childAddLimitMode)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val database: Database by lazy { DefaultAppLogic.with(requireContext()).database }
|
|
||||||
private val auth: ActivityViewModel by lazy { getActivityViewModel(requireActivity()) }
|
private val auth: ActivityViewModel by lazy { getActivityViewModel(requireActivity()) }
|
||||||
private val adapter = AddAppAdapter()
|
private val adapter = AddAppAdapter()
|
||||||
private var didEducateAboutAddingAssignedApps = false
|
private var didEducateAboutAddingAssignedApps = false
|
||||||
|
private val baseModel: AddAppsOrActivitiesModel by viewModels()
|
||||||
|
private val model: AddAppsModel by viewModels()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -89,167 +77,43 @@ class AddCategoryAppsFragment : DialogFragment() {
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val binding = FragmentAddCategoryAppsBinding.inflate(LayoutInflater.from(context))
|
val binding = FragmentAddCategoryAppsBinding.inflate(LayoutInflater.from(context))
|
||||||
val childId = requireArguments().getString(PARAM_CHILD_ID)!!
|
val params = requireArguments().getParcelable<AddAppsParams>(PARAMS)!!
|
||||||
val categoryId = requireArguments().getString(PARAM_CATEGORY_ID)!!
|
|
||||||
val childAddLimitMode = requireArguments().getBoolean(PARAM_CHILD_ADD_LIMIT_MODE)
|
|
||||||
|
|
||||||
auth.authenticatedUserOrChild.observe(this, Observer {
|
baseModel.init(params)
|
||||||
val parentAuthValid = it?.second?.type == UserType.Parent
|
baseModel.isAuthValid(auth).observe(this) { if (!it) dismissAllowingStateLoss() }
|
||||||
val childAuthValid = it?.second?.id == childId && childAddLimitMode
|
|
||||||
val authValid = parentAuthValid || childAuthValid
|
|
||||||
|
|
||||||
if (!authValid) {
|
model.init(params)
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
val filter = AppFilterView.getFilterLive(binding.filter)
|
model.showAppsFromOtherDevicesChecked.value = binding.showAppsFromUnassignedDevices.isChecked
|
||||||
val isLocalMode = database.config().getDeviceAuthTokenAsync().map { it.isEmpty() }
|
model.showAppsFromOtherCategories.value = binding.showOtherCategoriesApps.isChecked
|
||||||
val showAppsFromOtherDevicesChecked = MutableLiveData<Boolean>().apply {
|
model.assignToThisDeviceOnly.value = binding.assignToThisDeviceOnly.isChecked
|
||||||
value = binding.showAppsFromUnassignedDevices.isChecked
|
|
||||||
}
|
|
||||||
val realShowAppsFromAllDevices = isLocalMode.switchMap { localMode ->
|
|
||||||
if (localMode) {
|
|
||||||
liveDataFromNonNullValue(true)
|
|
||||||
} else {
|
|
||||||
showAppsFromOtherDevicesChecked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.showAppsFromUnassignedDevices.setOnCheckedChangeListener { _, isChecked ->
|
binding.showAppsFromUnassignedDevices.setOnCheckedChangeListener { _, isChecked ->
|
||||||
showAppsFromOtherDevicesChecked.value = isChecked
|
model.showAppsFromOtherDevicesChecked.value = isChecked
|
||||||
}
|
}
|
||||||
|
|
||||||
isLocalMode.observe(this, Observer {
|
binding.assignToThisDeviceOnly.setOnCheckedChangeListener { _, isChecked ->
|
||||||
binding.showAppsFromUnassignedDevices.visibility = if (it) View.GONE else View.VISIBLE
|
model.assignToThisDeviceOnly.value = isChecked
|
||||||
})
|
}
|
||||||
|
|
||||||
val showAppsFromOtherCategories = MutableLiveData<Boolean>().apply { value = binding.showOtherCategoriesApps.isChecked }
|
AppFilterView.getFilterLive(binding.filter).observe(this) { model.filter.value = it }
|
||||||
binding.showOtherCategoriesApps.setOnCheckedChangeListener { _, isChecked -> showAppsFromOtherCategories.value = isChecked }
|
|
||||||
|
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.layoutManager = LinearLayoutManager(context)
|
||||||
binding.recycler.adapter = adapter
|
binding.recycler.adapter = adapter
|
||||||
|
|
||||||
val childDeviceIds = database.device().getDevicesByUserId(childId)
|
model.listItems.observe(this, Observer {
|
||||||
.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 {
|
|
||||||
val selectedPackageNames = adapter.selectedApps
|
val selectedPackageNames = adapter.selectedApps
|
||||||
val visiblePackageNames = it.map { it.packageName }.toSet()
|
val visiblePackageNames = it.map { it.packageName }.toSet()
|
||||||
val hiddenSelectedPackageNames = selectedPackageNames.toMutableSet().apply { removeAll(visiblePackageNames) }.size
|
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)
|
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 {
|
binding.someOptionsDisabledDueToChildAuthentication = params.isSelfLimitAddingMode
|
||||||
adapter.categoryTitleByPackageName = it
|
|
||||||
})
|
|
||||||
|
|
||||||
binding.someOptionsDisabledDueToChildAuthentication = childAddLimitMode
|
model.deviceIdLive.observe(this) {/* keep loaded */}
|
||||||
|
|
||||||
binding.addAppsButton.setOnClickListener {
|
binding.addAppsButton.setOnClickListener {
|
||||||
val packageNames = adapter.selectedApps.toList()
|
val packageNames = adapter.selectedApps.toList()
|
||||||
|
|
||||||
if (packageNames.isNotEmpty()) {
|
if (packageNames.isNotEmpty()) {
|
||||||
|
val deviceSpecific = binding.assignToThisDeviceOnly.isChecked && !params.isSelfLimitAddingMode
|
||||||
|
val deviceId = model.deviceIdLive.value
|
||||||
|
|
||||||
|
if (deviceSpecific && deviceId == null) return@setOnClickListener
|
||||||
|
|
||||||
auth.tryDispatchParentAction(
|
auth.tryDispatchParentAction(
|
||||||
action = AddCategoryAppsAction(
|
action = AddCategoryAppsAction(
|
||||||
categoryId = categoryId,
|
categoryId = params.categoryId,
|
||||||
packageNames = packageNames
|
packageNames = if (deviceSpecific) packageNames.map { "$it@$deviceId" } else packageNames
|
||||||
),
|
),
|
||||||
allowAsChild = childAddLimitMode
|
allowAsChild = params.isSelfLimitAddingMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.cancelButton.setOnClickListener {
|
binding.cancelButton.setOnClickListener { dismiss() }
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.selectAllButton.setOnClickListener {
|
binding.selectAllButton.setOnClickListener {
|
||||||
adapter.selectedApps = adapter.selectedApps + (adapter.data?.map { it.packageName }?.toSet() ?: emptySet())
|
adapter.selectedApps = adapter.selectedApps + (adapter.data?.map { it.packageName }?.toSet() ?: emptySet())
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter.listener = object: AddAppAdapterListener {
|
adapter.listener = object: AddAppAdapterListener {
|
||||||
override fun onAppClicked(app: App) {
|
override fun onAppClicked(app: AddAppListItem) {
|
||||||
if (adapter.selectedApps.contains(app.packageName)) {
|
if (adapter.selectedApps.contains(app.packageName)) {
|
||||||
adapter.selectedApps = adapter.selectedApps - setOf(app.packageName)
|
adapter.selectedApps = adapter.selectedApps - setOf(app.packageName)
|
||||||
} else {
|
} else {
|
||||||
if (!didEducateAboutAddingAssignedApps) {
|
if (!didEducateAboutAddingAssignedApps) {
|
||||||
if (adapter.categoryTitleByPackageName[app.packageName] != null) {
|
if (app.currentCategoryName != null) {
|
||||||
didEducateAboutAddingAssignedApps = true
|
didEducateAboutAddingAssignedApps = true
|
||||||
|
|
||||||
AddAlreadyAssignedAppsInfoDialog().show(fragmentManager!!)
|
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()) {
|
return if (adapter.selectedApps.isEmpty()) {
|
||||||
AddAppActivitiesDialogFragment.newInstance(
|
AddAppActivitiesDialogFragment.newInstance(AddActivitiesParams(
|
||||||
childId = childId,
|
base = params,
|
||||||
categoryId = categoryId,
|
packageName = app.packageName
|
||||||
packageName = app.packageName,
|
)).show(parentFragmentManager)
|
||||||
childAddLimitMode = childAddLimitMode
|
|
||||||
).show(parentFragmentManager)
|
|
||||||
|
|
||||||
dismissAllowingStateLoss()
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -21,173 +21,100 @@ import android.view.LayoutInflater
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import io.timelimit.android.R
|
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.databinding.FragmentAddCategoryActivitiesBinding
|
||||||
import io.timelimit.android.extensions.addOnTextChangedListener
|
import io.timelimit.android.extensions.addOnTextChangedListener
|
||||||
import io.timelimit.android.extensions.showSafe
|
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.sync.actions.AddCategoryAppsAction
|
||||||
import io.timelimit.android.ui.main.getActivityViewModel
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
|
import io.timelimit.android.ui.manage.category.apps.AddAppsOrActivitiesModel
|
||||||
|
|
||||||
class AddAppActivitiesDialogFragment: DialogFragment() {
|
class AddAppActivitiesDialogFragment: DialogFragment() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val DIALOG_TAG = "AddAppActivitiesDialogFragment"
|
private const val DIALOG_TAG = "AddAppActivitiesDialogFragment"
|
||||||
private const val CHILD_ID = "childId"
|
private const val PARAMS = "params"
|
||||||
private const val CATEGORY_ID = "categoryId"
|
|
||||||
private const val PACKAGE_NAME = "packageName"
|
|
||||||
private const val SELECTED_ACTIVITIES = "selectedActivities"
|
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 {
|
fun newInstance(params: AddActivitiesParams) = AddAppActivitiesDialogFragment().apply {
|
||||||
arguments = Bundle().apply {
|
arguments = Bundle().apply { putParcelable(PARAMS, params) }
|
||||||
putString(CHILD_ID, childId)
|
|
||||||
putString(CATEGORY_ID, categoryId)
|
|
||||||
putString(PACKAGE_NAME, packageName)
|
|
||||||
putBoolean(CHILD_ADD_LIMIT_MODE, childAddLimitMode)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val adapter = AddAppActivityAdapter()
|
private val adapter = AddAppActivityAdapter()
|
||||||
|
private val baseModel: AddAppsOrActivitiesModel by viewModels()
|
||||||
|
private val model: AddActivitiesModel by viewModels()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
adapter.selectedActiviities.clear()
|
adapter.selectedActivities.clear()
|
||||||
savedInstanceState.getStringArray(SELECTED_ACTIVITIES)!!.forEach { adapter.selectedActiviities.add(it) }
|
savedInstanceState.getStringArray(SELECTED_ACTIVITIES)!!.forEach { adapter.selectedActivities.add(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
outState.putStringArray(SELECTED_ACTIVITIES, adapter.selectedActiviities.toTypedArray())
|
outState.putStringArray(SELECTED_ACTIVITIES, adapter.selectedActivities.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val appPackageName = arguments!!.getString(PACKAGE_NAME)!!
|
val params = requireArguments().getParcelable<AddActivitiesParams>(PARAMS)!!
|
||||||
val categoryId = arguments!!.getString(CATEGORY_ID)!!
|
val auth = getActivityViewModel(requireActivity())
|
||||||
val childId = arguments!!.getString(CHILD_ID)!!
|
val binding = FragmentAddCategoryActivitiesBinding.inflate(LayoutInflater.from(context))
|
||||||
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() }
|
|
||||||
|
|
||||||
auth.authenticatedUserOrChild.observe(this, Observer {
|
baseModel.init(params.base)
|
||||||
val parentAuthenticated = it?.second?.type == UserType.Parent
|
baseModel.isAuthValid(auth).observe(this) { if (!it) dismissAllowingStateLoss() }
|
||||||
val childAuthenticated = it?.second?.id == childId && childAddLimitMode
|
|
||||||
val anyoneAuthenticated = parentAuthenticated || childAuthenticated
|
|
||||||
|
|
||||||
if (!anyoneAuthenticated) {
|
model.init(params)
|
||||||
dismissAllowingStateLoss()
|
model.searchTerm.value = binding.search.text.toString()
|
||||||
}
|
binding.search.addOnTextChangedListener { model.searchTerm.value = binding.search.text.toString() }
|
||||||
})
|
|
||||||
|
|
||||||
val logic = DefaultAppLogic.with(context!!)
|
binding.recycler.layoutManager = LinearLayoutManager(requireContext())
|
||||||
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.adapter = adapter
|
binding.recycler.adapter = adapter
|
||||||
|
|
||||||
filteredActivities.observe(this, Observer { list ->
|
model.filteredActivities.observe(this) { list ->
|
||||||
val selectedActivities = adapter.selectedActiviities
|
val selectedActivities = adapter.selectedActivities
|
||||||
val visibleActivities = list.map { it.activityClassName }
|
val visibleActivities = list.map { it.className }
|
||||||
val hiddenSelectedActivities = selectedActivities.toMutableSet().apply { removeAll(visibleActivities) }.size
|
val hiddenSelectedActivities = selectedActivities.toMutableSet().apply { removeAll(visibleActivities) }.size
|
||||||
|
|
||||||
adapter.data = list
|
adapter.data = list
|
||||||
|
|
||||||
binding.hiddenEntries = if (hiddenSelectedActivities == 0)
|
binding.hiddenEntries = if (hiddenSelectedActivities == 0)
|
||||||
null
|
null
|
||||||
else
|
else
|
||||||
resources.getQuantityString(R.plurals.category_apps_add_dialog_hidden_entries, hiddenSelectedActivities, hiddenSelectedActivities)
|
resources.getQuantityString(R.plurals.category_apps_add_dialog_hidden_entries, hiddenSelectedActivities, hiddenSelectedActivities)
|
||||||
})
|
}
|
||||||
|
|
||||||
val emptyViewText = allActivitiesLive.switchMap { all ->
|
model.emptyViewText.observe(this) {
|
||||||
shownActivities.switchMap { shown ->
|
binding.emptyViewText = when (it!!) {
|
||||||
filteredActivities.map { filtered ->
|
AddActivitiesModel.EmptyViewText.None -> null
|
||||||
if (filtered.isNotEmpty())
|
AddActivitiesModel.EmptyViewText.EmptyShown -> getString(R.string.category_apps_add_activity_empty_shown)
|
||||||
null
|
AddActivitiesModel.EmptyViewText.EmptyFiltered -> getString(R.string.category_apps_add_activity_empty_filtered)
|
||||||
else if (all.isNotEmpty())
|
AddActivitiesModel.EmptyViewText.EmptyUnfiltered -> getString(R.string.category_apps_add_activity_empty_unfiltered)
|
||||||
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.someOptionsDisabledDueToChildAuthentication = params.base.isSelfLimitAddingMode
|
||||||
binding.emptyViewText = it
|
|
||||||
})
|
|
||||||
|
|
||||||
binding.someOptionsDisabledDueToChildAuthentication = childAddLimitMode
|
|
||||||
|
|
||||||
binding.cancelButton.setOnClickListener { dismissAllowingStateLoss() }
|
binding.cancelButton.setOnClickListener { dismissAllowingStateLoss() }
|
||||||
binding.addActivitiesButton.setOnClickListener {
|
binding.addActivitiesButton.setOnClickListener {
|
||||||
if (adapter.selectedActiviities.isNotEmpty()) {
|
if (adapter.selectedActivities.isNotEmpty()) {
|
||||||
auth.tryDispatchParentAction(
|
auth.tryDispatchParentAction(
|
||||||
action = AddCategoryAppsAction(
|
action = AddCategoryAppsAction(
|
||||||
categoryId = categoryId,
|
categoryId = params.base.categoryId,
|
||||||
packageNames = adapter.selectedActiviities.toList().map { "$appPackageName:$it" }
|
packageNames = adapter.selectedActivities.toList().map { "${params.packageName}:$it" }
|
||||||
),
|
),
|
||||||
allowAsChild = childAddLimitMode
|
allowAsChild = params.base.isSelfLimitAddingMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissAllowingStateLoss()
|
dismissAllowingStateLoss()
|
||||||
}
|
}
|
||||||
|
|
||||||
return AlertDialog.Builder(context!!, R.style.AppTheme)
|
return AlertDialog.Builder(requireContext(), R.style.AppTheme)
|
||||||
.setView(binding.root)
|
.setView(binding.root)
|
||||||
.create()
|
.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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,58 +18,54 @@ package io.timelimit.android.ui.manage.category.apps.addactivity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.timelimit.android.data.model.AppActivity
|
import io.timelimit.android.databinding.FragmentAddCategoryAppsItemBinding
|
||||||
import io.timelimit.android.databinding.FragmentAddCategoryActivitiesItemBinding
|
|
||||||
import io.timelimit.android.extensions.toggle
|
import io.timelimit.android.extensions.toggle
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
class AddAppActivityAdapter: RecyclerView.Adapter<ViewHolder>() {
|
class AddAppActivityAdapter: RecyclerView.Adapter<ViewHolder>() {
|
||||||
var data: List<AppActivity>? by Delegates.observable(null as List<AppActivity>?) { _, _, _ -> notifyDataSetChanged() }
|
var data: List<AddActivityListItem>? by Delegates.observable(emptyList()) { _, _, _ -> notifyDataSetChanged() }
|
||||||
val selectedActiviities = mutableSetOf<String>()
|
|
||||||
|
|
||||||
private val itemHandlers = object: ItemHandlers {
|
val selectedActivities = mutableSetOf<String>()
|
||||||
override fun onActivityClicked(activity: AppActivity) {
|
|
||||||
selectedActiviities.toggle(activity.activityClassName)
|
|
||||||
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setHasStableIds(true)
|
setHasStableIds(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getItem(position: Int): AppActivity {
|
private fun getItem(position: Int): AddActivityListItem {
|
||||||
return data!![position]
|
return data!![position]
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
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 getItemCount(): Int = this.data?.size ?: 0
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
|
||||||
FragmentAddCategoryActivitiesItemBinding.inflate(
|
FragmentAddCategoryAppsItemBinding.inflate(
|
||||||
LayoutInflater.from(parent.context),
|
LayoutInflater.from(parent.context),
|
||||||
parent,
|
parent,
|
||||||
false
|
false
|
||||||
).apply { handlers = itemHandlers }
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
val item = getItem(position)
|
val item = getItem(position)
|
||||||
|
|
||||||
holder.apply {
|
holder.apply {
|
||||||
binding.item = item
|
binding.title = item.title
|
||||||
binding.checked = selectedActiviities.contains(item.activityClassName)
|
binding.currentCategoryTitle = item.currentCategoryTitle
|
||||||
|
binding.subtitle = item.className
|
||||||
|
binding.showIcon = false
|
||||||
|
binding.checked = selectedActivities.contains(item.className)
|
||||||
binding.executePendingBindings()
|
binding.executePendingBindings()
|
||||||
|
|
||||||
|
binding.card.setOnClickListener {
|
||||||
|
selectedActivities.toggle(item.className)
|
||||||
|
binding.checked = selectedActivities.contains(item.className)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ViewHolder(val binding: FragmentAddCategoryActivitiesItemBinding): RecyclerView.ViewHolder(binding.root)
|
class ViewHolder(val binding: FragmentAddCategoryAppsItemBinding): RecyclerView.ViewHolder(binding.root)
|
||||||
|
|
||||||
interface ItemHandlers {
|
|
||||||
fun onActivityClicked(activity: 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,9 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.ui.manage.category.apps
|
package io.timelimit.android.ui.manage.category.appsandrules
|
||||||
|
|
||||||
import io.timelimit.android.ui.manage.category.appsandrules.AppAndRuleItem
|
|
||||||
|
|
||||||
interface AppAdapterHandlers {
|
interface AppAdapterHandlers {
|
||||||
fun onAppClicked(app: AppAndRuleItem.AppEntry)
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -27,7 +27,6 @@ import io.timelimit.android.date.DateInTimezone
|
||||||
import io.timelimit.android.extensions.MinuteOfDay
|
import io.timelimit.android.extensions.MinuteOfDay
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.logic.DummyApps
|
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.ui.manage.category.timelimit_rules.TimeLimitRulesHandlers
|
||||||
import io.timelimit.android.util.DayNameUtil
|
import io.timelimit.android.util.DayNameUtil
|
||||||
import io.timelimit.android.util.TimeTextUtil
|
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 ->
|
override fun getItemId(position: Int): Long = items[position].let { item ->
|
||||||
when (item) {
|
when (item) {
|
||||||
is AppAndRuleItem.AppEntry -> item.packageName.hashCode()
|
is AppAndRuleItem.AppEntry -> item.specifier.hashCode()
|
||||||
is AppAndRuleItem.RuleEntry -> item.rule.id.hashCode()
|
is AppAndRuleItem.RuleEntry -> item.rule.id.hashCode()
|
||||||
else -> item.hashCode()
|
else -> item.hashCode()
|
||||||
}
|
}
|
||||||
|
@ -137,16 +136,17 @@ class AppAndRuleAdapter: RecyclerView.Adapter<AppAndRuleAdapter.Holder>() {
|
||||||
val binding = holder.itemView.tag as FragmentCategoryAppsItemBinding
|
val binding = holder.itemView.tag as FragmentCategoryAppsItemBinding
|
||||||
val context = binding.root.context
|
val context = binding.root.context
|
||||||
|
|
||||||
binding.item = item
|
binding.title = item.title
|
||||||
binding.handlers = handlers
|
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.executePendingBindings()
|
||||||
|
|
||||||
binding.root.setOnLongClickListener { handlers?.onAppLongClicked(item) ?: false }
|
|
||||||
|
|
||||||
binding.icon.setImageDrawable(
|
binding.icon.setImageDrawable(
|
||||||
DummyApps.getIcon(item.packageNameWithoutActivityName, context) ?:
|
DummyApps.getIcon(item.specifier.packageName, context) ?:
|
||||||
DefaultAppLogic.with(context)
|
DefaultAppLogic.with(context)
|
||||||
.platformIntegration.getAppIcon(item.packageNameWithoutActivityName)
|
.platformIntegration.getAppIcon(item.specifier.packageName)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AppAndRuleItem.AddAppItem -> {/* nothing to do */}
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -17,9 +17,10 @@
|
||||||
package io.timelimit.android.ui.manage.category.appsandrules
|
package io.timelimit.android.ui.manage.category.appsandrules
|
||||||
|
|
||||||
import io.timelimit.android.data.model.TimeLimitRule
|
import io.timelimit.android.data.model.TimeLimitRule
|
||||||
|
import io.timelimit.android.data.model.derived.AppSpecifier
|
||||||
|
|
||||||
sealed class AppAndRuleItem {
|
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 AddAppItem: AppAndRuleItem()
|
||||||
object ExpandAppsItem: AppAndRuleItem()
|
object ExpandAppsItem: AppAndRuleItem()
|
||||||
data class RuleEntry(val rule: TimeLimitRule): 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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -24,6 +24,7 @@ import io.timelimit.android.data.extensions.getDateLive
|
||||||
import io.timelimit.android.data.model.HintsToShow
|
import io.timelimit.android.data.model.HintsToShow
|
||||||
import io.timelimit.android.extensions.takeDistributedElements
|
import io.timelimit.android.extensions.takeDistributedElements
|
||||||
import io.timelimit.android.livedata.map
|
import io.timelimit.android.livedata.map
|
||||||
|
import io.timelimit.android.livedata.mergeLiveDataWaitForValues
|
||||||
import io.timelimit.android.livedata.switchMap
|
import io.timelimit.android.livedata.switchMap
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.logic.DummyApps
|
import io.timelimit.android.logic.DummyApps
|
||||||
|
@ -108,27 +109,30 @@ class AppsAndRulesModel(application: Application): AndroidViewModel(application)
|
||||||
|
|
||||||
private val installedApps = database.app().getAllApps()
|
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 appsOfThisCategory = categoryIdLive.switchMap { categoryId -> database.categoryApp().getCategoryApps(categoryId) }
|
||||||
|
|
||||||
private val appsOfCategoryWithNames = installedApps.switchMap { allApps ->
|
private val appsOfCategoryWithNames = mergeLiveDataWaitForValues(installedAppsIndexed, appsOfThisCategory, deviceNamesIndexedLive)
|
||||||
appsOfThisCategory.map { apps ->
|
.map { (allAppsIndexed, appsOfThisCategory, deviceNamesIndexed) ->
|
||||||
apps.map { categoryApp ->
|
appsOfThisCategory.map { categoryApp ->
|
||||||
val title = DummyApps.getTitle(categoryApp.packageNameWithoutActivityName, getApplication()) ?:
|
val title = DummyApps.getTitle(categoryApp.appSpecifier.packageName, getApplication()) ?:
|
||||||
allApps.find { app -> app.packageName == categoryApp.packageNameWithoutActivityName }?.title
|
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 ->
|
private val appEntries = appsOfCategoryWithNames.map { apps ->
|
||||||
apps.map { (app, title) ->
|
apps.sortedBy { it.title.lowercase() }
|
||||||
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) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val fullAppScreenContent = showAllAppsLive.switchMap { showAllApps ->
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -39,6 +39,7 @@ import io.timelimit.android.sync.actions.RemoveCategoryAppsAction
|
||||||
import io.timelimit.android.sync.actions.UpdateTimeLimitRuleAction
|
import io.timelimit.android.sync.actions.UpdateTimeLimitRuleAction
|
||||||
import io.timelimit.android.ui.main.ActivityViewModel
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
import io.timelimit.android.ui.main.getActivityViewModel
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
|
import io.timelimit.android.ui.manage.category.apps.add.AddAppsParams
|
||||||
import io.timelimit.android.ui.manage.category.apps.add.AddCategoryAppsFragment
|
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.EditTimeLimitRuleDialogFragment
|
||||||
import io.timelimit.android.ui.manage.category.timelimit_rules.edit.EditTimeLimitRuleDialogFragmentListener
|
import io.timelimit.android.ui.manage.category.timelimit_rules.edit.EditTimeLimitRuleDialogFragmentListener
|
||||||
|
@ -135,7 +136,7 @@ abstract class CategoryAppsAndRulesFragment: Fragment(), Handlers, EditTimeLimit
|
||||||
if (auth.tryDispatchParentAction(
|
if (auth.tryDispatchParentAction(
|
||||||
RemoveCategoryAppsAction(
|
RemoveCategoryAppsAction(
|
||||||
categoryId = categoryId,
|
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)
|
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(
|
auth.tryDispatchParentAction(
|
||||||
AddCategoryAppsAction(
|
AddCategoryAppsAction(
|
||||||
categoryId = categoryId,
|
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()) {
|
return if (auth.requestAuthenticationOrReturnTrue()) {
|
||||||
AssignAppCategoryDialogFragment.newInstance(
|
AssignAppCategoryDialogFragment.newInstance(
|
||||||
childId = childId,
|
childId = childId,
|
||||||
appPackageName = app.packageName
|
appPackageName = app.specifier.encode()
|
||||||
).show(parentFragmentManager)
|
).show(parentFragmentManager)
|
||||||
|
|
||||||
true
|
true
|
||||||
|
@ -164,11 +165,11 @@ abstract class CategoryAppsAndRulesFragment: Fragment(), Handlers, EditTimeLimit
|
||||||
|
|
||||||
override fun onAddAppsClicked() {
|
override fun onAddAppsClicked() {
|
||||||
if (auth.requestAuthenticationOrReturnTrueAllowChild(childId = childId)) {
|
if (auth.requestAuthenticationOrReturnTrueAllowChild(childId = childId)) {
|
||||||
AddCategoryAppsFragment.newInstance(
|
AddCategoryAppsFragment.newInstance(AddAppsParams(
|
||||||
childId = childId,
|
childId = childId,
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
childAddLimitMode = !auth.isParentAuthenticated()
|
isSelfLimitAddingMode = !auth.isParentAuthenticated()
|
||||||
).show(parentFragmentManager)
|
)).show(parentFragmentManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -124,7 +124,7 @@ object DuplicateChildActions {
|
||||||
|
|
||||||
result.add(AddCategoryAppsAction(
|
result.add(AddCategoryAppsAction(
|
||||||
categoryId = newCategoryId,
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -64,13 +64,17 @@ class ChildAppsModel(application: Application): AndroidViewModel(application) {
|
||||||
val listContentLive = childAppsLive.switchMap { childApps ->
|
val listContentLive = childAppsLive.switchMap { childApps ->
|
||||||
childCategoriesLive.switchMap { categories ->
|
childCategoriesLive.switchMap { categories ->
|
||||||
childCategoryAppsLive.switchMap { categoryApps ->
|
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 ->
|
appFilterLive.ignoreUnchanged().switchMap { appFilter ->
|
||||||
val filteredChildApps = childApps.filter { appFilter.matches(it) }
|
val filteredChildApps = childApps.filter { appFilter.matches(it) }
|
||||||
|
|
||||||
modeLive.ignoreUnchanged().map { mode ->
|
modeLive.ignoreUnchanged().map { mode ->
|
||||||
when (mode!!) {
|
when (mode!!) {
|
||||||
ChildAppsMode.SortByCategory -> {
|
ChildAppsMode.SortByCategory -> {
|
||||||
val categoryAppByPackageName = categoryApps.associateBy { it.packageName }
|
|
||||||
val appsByCategoryId = filteredChildApps.groupBy { app ->
|
val appsByCategoryId = filteredChildApps.groupBy { app ->
|
||||||
categoryAppByPackageName[app.packageName]?.categoryId
|
categoryAppByPackageName[app.packageName]?.categoryId
|
||||||
}
|
}
|
||||||
|
@ -117,7 +121,6 @@ class ChildAppsModel(application: Application): AndroidViewModel(application) {
|
||||||
}
|
}
|
||||||
ChildAppsMode.SortByTitle -> {
|
ChildAppsMode.SortByTitle -> {
|
||||||
val categoryById = categories.associateBy { it.id }
|
val categoryById = categories.associateBy { it.id }
|
||||||
val categoryAppByPackageName = categoryApps.associateBy { it.packageName }
|
|
||||||
|
|
||||||
filteredChildApps
|
filteredChildApps
|
||||||
.distinctBy { it.packageName }
|
.distinctBy { it.packageName }
|
||||||
|
|
|
@ -194,7 +194,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
|
|
||||||
val appsAssignedToTheUser = categoriesOfTheSelectedUser.switchMap { categories ->
|
val appsAssignedToTheUser = categoriesOfTheSelectedUser.switchMap { categories ->
|
||||||
logic.database.categoryApp().getCategoryApps(categories.map { it.id }).map { categoryApps ->
|
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
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -132,7 +132,8 @@ class SetupDeviceModel(application: Application): AndroidViewModel(application)
|
||||||
|
|
||||||
val alreadyAssignedApps = Threads.database.executeAndWait {
|
val alreadyAssignedApps = Threads.database.executeAndWait {
|
||||||
logic.database.categoryApp().getCategoryAppsByUserIdSync(realUserId)
|
logic.database.categoryApp().getCategoryAppsByUserIdSync(realUserId)
|
||||||
.map { it.packageName }
|
.filter { it.appSpecifier.deviceId == null }
|
||||||
|
.map { it.appSpecifier.packageName }
|
||||||
.toSet()
|
.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
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -71,6 +71,13 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
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>
|
</LinearLayout>
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -18,28 +18,31 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<data>
|
<data>
|
||||||
<variable
|
<variable
|
||||||
name="item"
|
name="title"
|
||||||
type="io.timelimit.android.data.model.App" />
|
type="String" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="subtitle"
|
||||||
|
type="String" />
|
||||||
|
|
||||||
<variable
|
<variable
|
||||||
name="currentCategoryTitle"
|
name="currentCategoryTitle"
|
||||||
type="String" />
|
type="String" />
|
||||||
|
|
||||||
<variable
|
|
||||||
name="handlers"
|
|
||||||
type="io.timelimit.android.ui.manage.category.apps.add.AddAppAdapterListener" />
|
|
||||||
|
|
||||||
<variable
|
<variable
|
||||||
name="checked"
|
name="checked"
|
||||||
type="Boolean" />
|
type="Boolean" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="showIcon"
|
||||||
|
type="boolean" />
|
||||||
|
|
||||||
<import type="android.view.View" />
|
<import type="android.view.View" />
|
||||||
<import type="android.text.TextUtils" />
|
<import type="android.text.TextUtils" />
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
android:onClick="@{() -> handlers.onAppClicked(item)}"
|
android:id="@+id/card"
|
||||||
android:onLongClick="@{() -> handlers.onAppLongClicked(item)}"
|
|
||||||
android:foreground="?selectableItemBackground"
|
android:foreground="?selectableItemBackground"
|
||||||
app:cardUseCompatPadding="true"
|
app:cardUseCompatPadding="true"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -50,6 +53,7 @@
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
android:visibility="@{showIcon ? View.VISIBLE : View.GONE}"
|
||||||
android:id="@+id/icon"
|
android:id="@+id/icon"
|
||||||
tools:src="@mipmap/ic_launcher"
|
tools:src="@mipmap/ic_launcher"
|
||||||
android:layout_margin="8dp"
|
android:layout_margin="8dp"
|
||||||
|
@ -66,7 +70,7 @@
|
||||||
<TextView
|
<TextView
|
||||||
android:textAppearance="?android:textAppearanceLarge"
|
android:textAppearance="?android:textAppearanceLarge"
|
||||||
tools:text="Android Settings"
|
tools:text="Android Settings"
|
||||||
android:text="@{item.title}"
|
android:text="@{title}"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
@ -82,7 +86,7 @@
|
||||||
<TextView
|
<TextView
|
||||||
android:textAppearance="?android:textAppearanceSmall"
|
android:textAppearance="?android:textAppearanceSmall"
|
||||||
tools:text="com.android.settings"
|
tools:text="com.android.settings"
|
||||||
android:text="@{item.packageName}"
|
android:text="@{subtitle}"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
the Free Software Foundation version 3 of the License.
|
the Free Software Foundation version 3 of the License.
|
||||||
|
@ -18,18 +18,23 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<data>
|
<data>
|
||||||
<variable
|
<variable
|
||||||
name="item"
|
name="title"
|
||||||
type="io.timelimit.android.ui.manage.category.appsandrules.AppAndRuleItem.AppEntry" />
|
type="String" />
|
||||||
|
|
||||||
<variable
|
<variable
|
||||||
name="handlers"
|
name="deviceName"
|
||||||
type="io.timelimit.android.ui.manage.category.apps.AppAdapterHandlers" />
|
type="String" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="subtitle"
|
||||||
|
type="String" />
|
||||||
|
|
||||||
|
<import type="android.text.TextUtils" />
|
||||||
<import type="android.view.View" />
|
<import type="android.view.View" />
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
android:onClick="@{() -> handlers.onAppClicked(item)}"
|
android:id="@+id/card"
|
||||||
android:foreground="?selectableItemBackground"
|
android:foreground="?selectableItemBackground"
|
||||||
app:cardUseCompatPadding="true"
|
app:cardUseCompatPadding="true"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -56,14 +61,23 @@
|
||||||
<TextView
|
<TextView
|
||||||
android:textAppearance="?android:textAppearanceLarge"
|
android:textAppearance="?android:textAppearanceLarge"
|
||||||
tools:text="Android Settings"
|
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_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:textAppearance="?android:textAppearanceSmall"
|
android:textAppearance="?android:textAppearanceSmall"
|
||||||
tools:text="com.android.settings"
|
tools:text="com.android.settings"
|
||||||
android:text="@{item.packageName}"
|
android:text="@{subtitle}"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -241,6 +241,7 @@
|
||||||
<string name="category_apps_add_dialog_select_all">Alle auswählen</string>
|
<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_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_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">
|
<plurals name="category_apps_add_dialog_hidden_entries">
|
||||||
<item quantity="one">%d ausgeblendeten Eintrag ausgewählt</item>
|
<item quantity="one">%d ausgeblendeten Eintrag ausgewählt</item>
|
||||||
<item quantity="other">%s ausgeblendete Einträge 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_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_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_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>
|
<string name="category_apps_add_dialog_select_all">Select all</string>
|
||||||
<plurals name="category_apps_add_dialog_hidden_entries">
|
<plurals name="category_apps_add_dialog_hidden_entries">
|
||||||
<item quantity="one">%d hidden entry selected</item>
|
<item quantity="one">%d hidden entry selected</item>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue