mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +02:00
Add option to assign all system apps without category
This commit is contained in:
parent
c40b2298d4
commit
1821e697cf
22 changed files with 119 additions and 30 deletions
|
@ -31,6 +31,7 @@ abstract class PlatformIntegration(
|
|||
abstract fun getLocalAppActivities(deviceId: String): Collection<AppActivity>
|
||||
abstract fun getLocalAppTitle(packageName: String): String?
|
||||
abstract fun getAppIcon(packageName: String): Drawable?
|
||||
abstract fun isSystemImageApp(packageName: String): Boolean
|
||||
abstract fun getLauncherAppPackageName(): String?
|
||||
abstract fun getCurrentProtectionLevel(): ProtectionLevel
|
||||
abstract fun getForegroundAppPermissionStatus(): RuntimePermissionStatus
|
||||
|
|
|
@ -34,6 +34,7 @@ import android.provider.Settings
|
|||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.widget.Toast
|
||||
import androidx.collection.LruCache
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
|
@ -116,6 +117,18 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
|
|||
return AndroidIntegrationApps.getAppIcon(packageName, context)
|
||||
}
|
||||
|
||||
private val isSystemImageAppCache = object: LruCache<String, Boolean>(8) {
|
||||
override fun create(key: String): Boolean? = try {
|
||||
val appInfo: ApplicationInfo = context.packageManager.getApplicationInfo(key, 0)
|
||||
|
||||
appInfo.flags and ApplicationInfo.FLAG_SYSTEM == ApplicationInfo.FLAG_SYSTEM
|
||||
} catch (ex: PackageManager.NameNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun isSystemImageApp(packageName: String): Boolean = isSystemImageAppCache.get(packageName) ?: false
|
||||
|
||||
override fun getLauncherAppPackageName(): String? {
|
||||
return Intent(Intent.ACTION_MAIN)
|
||||
.addCategory(Intent.CATEGORY_HOME)
|
||||
|
|
|
@ -18,8 +18,6 @@ package io.timelimit.android.integration.platform.android
|
|||
import android.annotation.TargetApi
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.service.notification.NotificationListenerService
|
||||
import android.service.notification.StatusBarNotification
|
||||
|
@ -153,16 +151,19 @@ class NotificationListener: NotificationListenerService() {
|
|||
return if (deviceAndUserRelatedData?.userRelatedData?.user?.type != UserType.Child) {
|
||||
BlockingReason.None
|
||||
} else {
|
||||
val isSystemImageApp = appLogic.platformIntegration.isSystemImageApp(sbn.packageName)
|
||||
|
||||
val appHandling = AppBaseHandling.calculate(
|
||||
foregroundAppPackageName = sbn.packageName,
|
||||
foregroundAppActivityName = null,
|
||||
pauseCounting = false,
|
||||
pauseForegroundAppBackgroundLoop = false,
|
||||
userRelatedData = deviceAndUserRelatedData.userRelatedData,
|
||||
deviceRelatedData = deviceAndUserRelatedData.deviceRelatedData
|
||||
deviceRelatedData = deviceAndUserRelatedData.deviceRelatedData,
|
||||
isSystemImageApp = isSystemImageApp
|
||||
)
|
||||
|
||||
if (appHandling is AppBaseHandling.BlockDueToNoCategory && !isSystemApp(sbn.packageName)) {
|
||||
if (appHandling is AppBaseHandling.BlockDueToNoCategory && !isSystemImageApp) {
|
||||
BlockingReason.NotPartOfAnCategory
|
||||
} else if (appHandling is AppBaseHandling.UseCategories) {
|
||||
val time = RealTime.newInstance()
|
||||
|
@ -201,14 +202,4 @@ class NotificationListener: NotificationListenerService() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSystemApp(packageName: String): Boolean {
|
||||
try {
|
||||
val appInfo = packageManager.getApplicationInfo(packageName, 0)
|
||||
|
||||
return appInfo.flags and ApplicationInfo.FLAG_SYSTEM == ApplicationInfo.FLAG_SYSTEM
|
||||
} catch (ex: PackageManager.NameNotFoundException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -55,6 +55,8 @@ class DummyIntegration(
|
|||
return null
|
||||
}
|
||||
|
||||
override fun isSystemImageApp(packageName: String): Boolean = false
|
||||
|
||||
override fun getLauncherAppPackageName(): String? = null
|
||||
|
||||
override fun getCurrentProtectionLevel(): ProtectionLevel {
|
||||
|
|
|
@ -55,7 +55,8 @@ object AppAffectedByPrimaryDeviceUtil {
|
|||
deviceRelatedData = deviceAndUserRelatedData.deviceRelatedData,
|
||||
userRelatedData = deviceAndUserRelatedData.userRelatedData,
|
||||
pauseCounting = false,
|
||||
pauseForegroundAppBackgroundLoop = false
|
||||
pauseForegroundAppBackgroundLoop = false,
|
||||
isSystemImageApp = logic.platformIntegration.isSystemImageApp(currentApp.packageName)
|
||||
)
|
||||
|
||||
if (!(handling is AppBaseHandling.UseCategories)) {
|
||||
|
|
|
@ -294,7 +294,8 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
pauseForegroundAppBackgroundLoop = pauseForegroundAppBackgroundLoop,
|
||||
userRelatedData = userRelatedData,
|
||||
deviceRelatedData = deviceRelatedData,
|
||||
pauseCounting = !isScreenOn
|
||||
pauseCounting = !isScreenOn,
|
||||
isSystemImageApp = appLogic.platformIntegration.isSystemImageApp(app.packageName)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -304,7 +305,8 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
|
|||
pauseForegroundAppBackgroundLoop = false,
|
||||
userRelatedData = userRelatedData,
|
||||
deviceRelatedData = deviceRelatedData,
|
||||
pauseCounting = false
|
||||
pauseCounting = false,
|
||||
isSystemImageApp = audioPlaybackPackageName?.let { appLogic.platformIntegration.isSystemImageApp(it) } ?: false
|
||||
)
|
||||
|
||||
val allAppsBaseHandlings = foregroundAppWithBaseHandlings.map { it.second } + listOf(backgroundAppBaseHandling)
|
||||
|
|
47
app/src/main/java/io/timelimit/android/logic/DummyApps.kt
Normal file
47
app/src/main/java/io/timelimit/android/logic/DummyApps.kt
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation version 3 of the License.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.data.model.AppRecommendation
|
||||
|
||||
object DummyApps {
|
||||
const val NOT_ASSIGNED_SYSTEM_IMAGE_APP = ".dummy.system_image"
|
||||
|
||||
fun getTitle(packageName: String, context: Context): String? = when (packageName) {
|
||||
NOT_ASSIGNED_SYSTEM_IMAGE_APP -> context.getString(R.string.dummy_app_unassigned_system_image_app)
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun getIcon(packageName: String, context: Context): Drawable? = when (packageName) {
|
||||
NOT_ASSIGNED_SYSTEM_IMAGE_APP -> ContextCompat.getDrawable(context, R.mipmap.ic_system_app)
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun getApps(deviceId: String, context: Context): List<App> = listOf(
|
||||
App(
|
||||
deviceId = deviceId,
|
||||
packageName = NOT_ASSIGNED_SYSTEM_IMAGE_APP,
|
||||
title = getTitle(NOT_ASSIGNED_SYSTEM_IMAGE_APP, context)!!,
|
||||
isLaunchable = false,
|
||||
recommendation = AppRecommendation.None
|
||||
)
|
||||
)
|
||||
}
|
|
@ -171,6 +171,7 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
|
|||
|
||||
private fun getAppsWithCategories(packageNames: List<String>, data: UserRelatedData, blockingAtActivityLevel: Boolean): Map<String, Set<String>> {
|
||||
val categoryForUnassignedApps = data.categoryById[data.user.categoryForNotAssignedApps]
|
||||
val categoryForOtherSystemApps = data.findCategoryApp(DummyApps.NOT_ASSIGNED_SYSTEM_IMAGE_APP)?.categoryId?.let { data.categoryById[it] }
|
||||
|
||||
if (blockingAtActivityLevel) {
|
||||
val categoriesByPackageName = data.categoryApps.groupBy { it.packageNameWithoutActivityName }
|
||||
|
@ -183,7 +184,9 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
|
|||
val isMainAppIncluded = categoriesItems?.find { !it.specifiesActivity } != null
|
||||
|
||||
if (!isMainAppIncluded) {
|
||||
if (categoryForUnassignedApps != null) {
|
||||
if (categoryForOtherSystemApps != null && appLogic.platformIntegration.isSystemImageApp(packageName)) {
|
||||
categories.add(categoryForOtherSystemApps.category.id)
|
||||
} else if (categoryForUnassignedApps != null) {
|
||||
categories.add(categoryForUnassignedApps.category.id)
|
||||
}
|
||||
}
|
||||
|
@ -198,7 +201,10 @@ class SuspendAppsLogic(private val appLogic: AppLogic): Observer {
|
|||
val result = mutableMapOf<String, Set<String>>()
|
||||
|
||||
packageNames.forEach { packageName ->
|
||||
val category = categoryByPackageName[packageName]?.categoryId ?: categoryForUnassignedApps?.category?.id
|
||||
val category = categoryByPackageName[packageName]?.categoryId ?: run {
|
||||
if (categoryForOtherSystemApps != null && appLogic.platformIntegration.isSystemImageApp(packageName))
|
||||
categoryForOtherSystemApps.category.id else categoryForUnassignedApps?.category?.id
|
||||
}
|
||||
|
||||
result[packageName] = if (category != null) setOf(category) else emptySet()
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import io.timelimit.android.data.model.derived.DeviceRelatedData
|
|||
import io.timelimit.android.data.model.derived.UserRelatedData
|
||||
import io.timelimit.android.integration.platform.android.AndroidIntegrationApps
|
||||
import io.timelimit.android.logic.BlockingLevel
|
||||
import io.timelimit.android.logic.DummyApps
|
||||
|
||||
sealed class AppBaseHandling {
|
||||
object Idle: AppBaseHandling()
|
||||
|
@ -49,7 +50,8 @@ sealed class AppBaseHandling {
|
|||
pauseForegroundAppBackgroundLoop: Boolean,
|
||||
pauseCounting: Boolean,
|
||||
userRelatedData: UserRelatedData,
|
||||
deviceRelatedData: DeviceRelatedData
|
||||
deviceRelatedData: DeviceRelatedData,
|
||||
isSystemImageApp: Boolean
|
||||
): AppBaseHandling {
|
||||
if (pauseForegroundAppBackgroundLoop) {
|
||||
return PauseLogic
|
||||
|
@ -71,7 +73,9 @@ sealed class AppBaseHandling {
|
|||
} else if (foregroundAppPackageName != null) {
|
||||
val appCategory = run {
|
||||
val tryActivityLevelBlocking = deviceRelatedData.deviceEntry.enableActivityLevelBlocking && foregroundAppActivityName != null
|
||||
val appLevelCategory = userRelatedData.findCategoryApp(foregroundAppPackageName)
|
||||
val appLevelCategory = userRelatedData.findCategoryApp(foregroundAppPackageName) ?: run {
|
||||
if (isSystemImageApp) userRelatedData.findCategoryApp(DummyApps.NOT_ASSIGNED_SYSTEM_IMAGE_APP) else null
|
||||
}
|
||||
|
||||
(if (tryActivityLevelBlocking) {
|
||||
userRelatedData.findCategoryApp("$foregroundAppPackageName:$foregroundAppActivityName")
|
||||
|
|
|
@ -96,7 +96,8 @@ class LockModel(application: Application): AndroidViewModel(application) {
|
|||
deviceRelatedData = deviceAndUserRelatedData.deviceRelatedData,
|
||||
userRelatedData = deviceAndUserRelatedData.userRelatedData,
|
||||
pauseForegroundAppBackgroundLoop = false,
|
||||
pauseCounting = false
|
||||
pauseCounting = false,
|
||||
isSystemImageApp = logic.platformIntegration.isSystemImageApp(packageName)
|
||||
)
|
||||
|
||||
val needsNetworkId = appBaseHandling.needsNetworkId()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -21,6 +21,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import io.timelimit.android.data.model.App
|
||||
import io.timelimit.android.databinding.FragmentAddCategoryAppsItemBinding
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.logic.DummyApps
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class AddAppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
||||
|
@ -61,6 +62,7 @@ class AddAppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
|||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
val context = holder.itemView.context
|
||||
|
||||
holder.apply {
|
||||
binding.item = item
|
||||
|
@ -70,7 +72,8 @@ class AddAppAdapter: RecyclerView.Adapter<ViewHolder>() {
|
|||
binding.executePendingBindings()
|
||||
|
||||
binding.icon.setImageDrawable(
|
||||
DefaultAppLogic.with(holder.itemView.context)
|
||||
DummyApps.getIcon(item.packageName, context) ?:
|
||||
DefaultAppLogic.with(context)
|
||||
.platformIntegration.getAppIcon(item.packageName)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ import io.timelimit.android.livedata.liveDataFromValue
|
|||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.switchMap
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.logic.DummyApps
|
||||
import io.timelimit.android.sync.actions.AddCategoryAppsAction
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.main.getActivityViewModel
|
||||
|
@ -147,6 +148,8 @@ class AddCategoryAppsFragment : DialogFragment() {
|
|||
|
||||
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)
|
||||
|
|
|
@ -25,6 +25,7 @@ import io.timelimit.android.data.model.UsedTimeItem
|
|||
import io.timelimit.android.databinding.*
|
||||
import io.timelimit.android.extensions.MinuteOfDay
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.logic.DummyApps
|
||||
import io.timelimit.android.ui.manage.category.apps.AppAdapterHandlers
|
||||
import io.timelimit.android.ui.manage.category.timelimit_rules.TimeLimitRulesHandlers
|
||||
import io.timelimit.android.util.JoinUtil
|
||||
|
@ -133,13 +134,15 @@ class AppAndRuleAdapter: RecyclerView.Adapter<AppAndRuleAdapter.Holder>() {
|
|||
when (item) {
|
||||
is AppAndRuleItem.AppEntry -> {
|
||||
val binding = holder.itemView.tag as FragmentCategoryAppsItemBinding
|
||||
val context = binding.root.context
|
||||
|
||||
binding.item = item
|
||||
binding.handlers = handlers
|
||||
binding.executePendingBindings()
|
||||
|
||||
binding.icon.setImageDrawable(
|
||||
DefaultAppLogic.with(binding.root.context)
|
||||
DummyApps.getIcon(item.packageNameWithoutActivityName, context) ?:
|
||||
DefaultAppLogic.with(context)
|
||||
.platformIntegration.getAppIcon(item.packageNameWithoutActivityName)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import io.timelimit.android.extensions.takeDistributedElements
|
|||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.switchMap
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.logic.DummyApps
|
||||
import java.util.*
|
||||
|
||||
class AppsAndRulesModel(application: Application): AndroidViewModel(application) {
|
||||
|
@ -114,15 +115,18 @@ class AppsAndRulesModel(application: Application): AndroidViewModel(application)
|
|||
private val appsOfCategoryWithNames = installedApps.switchMap { allApps ->
|
||||
appsOfThisCategory.map { apps ->
|
||||
apps.map { categoryApp ->
|
||||
categoryApp to allApps.find { app -> app.packageName == categoryApp.packageNameWithoutActivityName }
|
||||
val title = DummyApps.getTitle(categoryApp.packageNameWithoutActivityName, getApplication()) ?:
|
||||
allApps.find { app -> app.packageName == categoryApp.packageNameWithoutActivityName }?.title
|
||||
|
||||
categoryApp to title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val appEntries = appsOfCategoryWithNames.map { apps ->
|
||||
apps.map { (app, appEntry) ->
|
||||
if (appEntry != null) {
|
||||
AppAndRuleItem.AppEntry(appEntry.title, app.packageName, app.packageNameWithoutActivityName)
|
||||
apps.map { (app, title) ->
|
||||
if (title != null) {
|
||||
AppAndRuleItem.AppEntry(title, app.packageName, app.packageNameWithoutActivityName)
|
||||
} else {
|
||||
AppAndRuleItem.AppEntry("app not found", app.packageName, app.packageNameWithoutActivityName)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import io.timelimit.android.databinding.FragmentChildAppsItemBinding
|
|||
import io.timelimit.android.databinding.GenericBigListHeaderBinding
|
||||
import io.timelimit.android.databinding.GenericListHeaderBinding
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.logic.DummyApps
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class ChildAppsAdapter: RecyclerView.Adapter<ChildAppsHolder>() {
|
||||
|
@ -101,10 +102,13 @@ class ChildAppsAdapter: RecyclerView.Adapter<ChildAppsHolder>() {
|
|||
is ChildAppsApp -> {
|
||||
holder as AppHolder
|
||||
|
||||
val context = holder.binding.root.context
|
||||
|
||||
holder.binding.item = item.app
|
||||
holder.binding.currentCategoryTitle = item.shownCategoryName
|
||||
holder.binding.icon.setImageDrawable(
|
||||
DefaultAppLogic.with(holder.binding.root.context).platformIntegration.getAppIcon(item.app.packageName)
|
||||
DummyApps.getIcon(item.app.packageName, context) ?:
|
||||
DefaultAppLogic.with(context).platformIntegration.getAppIcon(item.app.packageName)
|
||||
)
|
||||
holder.binding.handlers = handlers
|
||||
holder.binding.executePendingBindings()
|
||||
|
|
BIN
app/src/main/res/mipmap-hdpi/ic_system_app.png
Normal file
BIN
app/src/main/res/mipmap-hdpi/ic_system_app.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.7 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_system_app.png
Normal file
BIN
app/src/main/res/mipmap-mdpi/ic_system_app.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_system_app.png
Normal file
BIN
app/src/main/res/mipmap-xhdpi/ic_system_app.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.1 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_system_app.png
Normal file
BIN
app/src/main/res/mipmap-xxhdpi/ic_system_app.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_system_app.png
Normal file
BIN
app/src/main/res/mipmap-xxxhdpi/ic_system_app.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
|
@ -1514,4 +1514,6 @@
|
|||
<string name="task_review_text">%1$s hat angegeben, %2$s erledigt zu haben. Ist das richtig?</string>
|
||||
<string name="task_review_category">Dafür wird es %1$s Extrazeit für %2$s geben</string>
|
||||
<string name="task_review_last_grant">Diese Aufgabe wurde zuletzt bestätigt am %s</string>
|
||||
|
||||
<string name="dummy_app_unassigned_system_image_app">nicht zugeordnete Apps von der Systempartition</string>
|
||||
</resources>
|
||||
|
|
|
@ -1567,4 +1567,6 @@
|
|||
<string name="task_review_text">%1$s said that %2$s was finished. Is this correct?</string>
|
||||
<string name="task_review_category">This will grant %1$s extra time for %2$s</string>
|
||||
<string name="task_review_last_grant">This task was confirmed last time at %s</string>
|
||||
|
||||
<string name="dummy_app_unassigned_system_image_app">not assigned Apps from the system image</string>
|
||||
</resources>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue