mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +02:00
Improve the widget
This commit is contained in:
parent
baff9a110d
commit
46550588f5
33 changed files with 2843 additions and 217 deletions
1734
app/schemas/io.timelimit.android.data.RoomDatabase/45.json
Normal file
1734
app/schemas/io.timelimit.android.data.RoomDatabase/45.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -112,6 +112,14 @@
|
|||
android:taskAffinity=":update"
|
||||
android:launchMode="singleTop" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.widget.config.WidgetConfigActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- system integration -->
|
||||
|
||||
<receiver android:name=".integration.platform.android.receiver.BootReceiver" android:exported="false">
|
||||
|
|
|
@ -46,6 +46,7 @@ interface Database {
|
|||
fun cryptContainerKeyResult(): CryptContainerKeyResultDao
|
||||
fun deviceKey(): DeviceKeyDao
|
||||
fun u2f(): U2FDao
|
||||
fun widgetCategory(): WidgetCategoryDao
|
||||
|
||||
fun <T> runInTransaction(block: () -> T): T
|
||||
fun <T> runInUnobservedTransaction(block: () -> T): T
|
||||
|
|
|
@ -326,6 +326,13 @@ object DatabaseMigrations {
|
|||
}
|
||||
}
|
||||
|
||||
val MIGRATE_TO_V45 = object: Migration(44, 45) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `widget_category` (`widget_id` INTEGER NOT NULL, `category_id` TEXT NOT NULL, PRIMARY KEY(`widget_id`, `category_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS `index_widget_category_category_id` ON `widget_category` (`category_id`)")
|
||||
}
|
||||
}
|
||||
|
||||
val ALL = arrayOf(
|
||||
MIGRATE_TO_V2,
|
||||
MIGRATE_TO_V3,
|
||||
|
@ -369,6 +376,7 @@ object DatabaseMigrations {
|
|||
MIGRATE_TO_V41,
|
||||
MIGRATE_TO_V42,
|
||||
MIGRATE_TP_V43,
|
||||
MIGRATE_TO_V44
|
||||
MIGRATE_TO_V44,
|
||||
MIGRATE_TO_V45
|
||||
)
|
||||
}
|
||||
|
|
|
@ -58,8 +58,9 @@ import java.util.concurrent.TimeUnit
|
|||
CryptContainerPendingKeyRequest::class,
|
||||
CryptContainerKeyResult::class,
|
||||
DevicePublicKey::class,
|
||||
UserU2FKey::class
|
||||
], version = 44)
|
||||
UserU2FKey::class,
|
||||
WidgetCategory::class
|
||||
], version = 45)
|
||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||
companion object {
|
||||
private val lock = Object()
|
||||
|
|
|
@ -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.data.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import io.timelimit.android.data.model.WidgetCategory
|
||||
|
||||
@Dao
|
||||
interface WidgetCategoryDao {
|
||||
@Query("DELETE FROM widget_category WHERE widget_id = :widgetId")
|
||||
fun deleteByWidgetId(widgetId: Int)
|
||||
|
||||
@Query("DELETE FROM widget_category WHERE widget_id IN (:widgetIds)")
|
||||
fun deleteByWidgetIds(widgetIds: IntArray)
|
||||
|
||||
@Query("DELETE FROM widget_category WHERE widget_id = :widgetId AND category_id IN (:categoryIds)")
|
||||
fun deleteByWidgetIdAndCategoryIds(widgetId: Int, categoryIds: List<String>)
|
||||
|
||||
@Query("DELETE FROM widget_category")
|
||||
fun deleteAll()
|
||||
|
||||
@Query("SELECT * FROM widget_category")
|
||||
fun queryLive(): LiveData<List<WidgetCategory>>
|
||||
|
||||
@Query("SELECT category_id FROM widget_category WHERE widget_id = :widgetId")
|
||||
fun queryByWidgetIdSync(widgetId: Int): List<String>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
fun insert(items: List<WidgetCategory>)
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
|
||||
@Entity(
|
||||
tableName = "widget_category",
|
||||
primaryKeys = ["widget_id", "category_id"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = Category::class,
|
||||
parentColumns = ["id"],
|
||||
childColumns = ["category_id"],
|
||||
onUpdate = ForeignKey.CASCADE,
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
data class WidgetCategory (
|
||||
@ColumnInfo(name = "widget_id")
|
||||
val widgetId: Int,
|
||||
@ColumnInfo(name = "category_id", index = true)
|
||||
val categoryId: String
|
||||
)
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -44,8 +44,13 @@ class BackgroundActionService: Service() {
|
|||
fun prepareRevokeTemporarilyAllowed(context: Context) = Intent(context, BackgroundActionService::class.java)
|
||||
.putExtra(ACTION, ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS)
|
||||
|
||||
fun prepareSwitchToDefaultUser(context: Context) = Intent(context, BackgroundActionService::class.java)
|
||||
.putExtra(ACTION, ACTION_SWITCH_TO_DEFAULT_USER)
|
||||
fun getSwitchToDefaultUserIntent(context: Context) = PendingIntent.getService(
|
||||
context,
|
||||
PendingIntentIds.SWITCH_TO_DEFAULT_USER,
|
||||
Intent(context, BackgroundActionService::class.java)
|
||||
.putExtra(ACTION, ACTION_SWITCH_TO_DEFAULT_USER),
|
||||
PendingIntentIds.PENDING_INTENT_FLAGS
|
||||
)
|
||||
|
||||
fun prepareDismissNotification(context: Context, type: Int, id: String) = Intent(context, BackgroundActionService::class.java)
|
||||
.putExtra(ACTION, ACTION_DISMISS_NOTIFICATION)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -17,7 +17,6 @@ package io.timelimit.android.integration.platform.android
|
|||
|
||||
import android.app.ActivityManager
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
@ -89,12 +88,7 @@ class BackgroundService: Service() {
|
|||
NotificationCompat.Action.Builder(
|
||||
R.drawable.ic_account_circle_black_24dp,
|
||||
context.getString(R.string.manage_device_default_user_switch_btn),
|
||||
PendingIntent.getService(
|
||||
context,
|
||||
PendingIntentIds.SWITCH_TO_DEFAULT_USER,
|
||||
BackgroundActionService.prepareSwitchToDefaultUser(context),
|
||||
PendingIntentIds.PENDING_INTENT_FLAGS
|
||||
)
|
||||
BackgroundActionService.getSwitchToDefaultUserIntent(context)
|
||||
).build()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import io.timelimit.android.sync.SyncUtil
|
|||
import io.timelimit.android.sync.network.api.ServerApi
|
||||
import io.timelimit.android.sync.websocket.NetworkStatusInterface
|
||||
import io.timelimit.android.sync.websocket.WebsocketClientCreator
|
||||
import io.timelimit.android.ui.widget.TimesWidgetProvider
|
||||
|
||||
class AppLogic(
|
||||
val platformIntegration: PlatformIntegration,
|
||||
|
@ -98,6 +99,7 @@ class AppLogic(
|
|||
|
||||
init {
|
||||
WatchdogLogic(this)
|
||||
TimesWidgetProvider.triggerUpdates(context)
|
||||
}
|
||||
|
||||
val suspendAppsLogic = SuspendAppsLogic(this)
|
||||
|
|
|
@ -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.ui.widget
|
||||
|
||||
import io.timelimit.android.data.model.WidgetCategory
|
||||
|
||||
data class TimesWidgetConfig (val categories: List<WidgetCategory>) {
|
||||
val widgetCategoriesByWidgetId by lazy {
|
||||
categories.groupBy { it.widgetId }.mapValues { it.value.map { it.categoryId }.toSet() }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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.widget
|
||||
|
||||
sealed class TimesWidgetContent {
|
||||
object UnconfiguredDevice: TimesWidgetContent()
|
||||
data class NoChildUser(val canSwitchToDefaultUser: Boolean): TimesWidgetContent()
|
||||
data class Categories(
|
||||
val categories: List<Item>,
|
||||
val canSwitchToDefaultUser: Boolean
|
||||
): TimesWidgetContent() {
|
||||
data class Item(
|
||||
val categoryId: String,
|
||||
val categoryName: String,
|
||||
val level: Int,
|
||||
val remainingTimeToday: Long?
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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.widget
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.data.extensions.sortedCategories
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.RealTime
|
||||
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
|
||||
|
||||
object TimesWidgetContentLoader {
|
||||
fun with(logic: AppLogic): LiveData<TimesWidgetContent> {
|
||||
val database = logic.database
|
||||
val realTimeLogic = logic.realTimeLogic
|
||||
val realTime = RealTime.newInstance()
|
||||
val categoryHandlingCache = CategoryHandlingCache()
|
||||
val handler = Threads.mainThreadHandler
|
||||
|
||||
val deviceAndUserRelatedDataLive = database.derivedDataDao().getUserAndDeviceRelatedDataLive()
|
||||
var deviceAndUserRelatedDataLiveLoaded = false
|
||||
|
||||
val batteryStatusLive = logic.platformIntegration.getBatteryStatusLive()
|
||||
|
||||
lateinit var timeModificationListener: () -> Unit
|
||||
lateinit var updateByClockRunnable: Runnable
|
||||
var isActive = false
|
||||
|
||||
val newResult = object: MediatorLiveData<TimesWidgetContent>() {
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
|
||||
isActive = true
|
||||
|
||||
realTimeLogic.registerTimeModificationListener(timeModificationListener)
|
||||
|
||||
// ensure that the next update gets scheduled
|
||||
updateByClockRunnable.run()
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
super.onInactive()
|
||||
|
||||
isActive = true
|
||||
|
||||
realTimeLogic.unregisterTimeModificationListener(timeModificationListener)
|
||||
handler.removeCallbacks(updateByClockRunnable)
|
||||
}
|
||||
}
|
||||
|
||||
fun update() {
|
||||
handler.removeCallbacks(updateByClockRunnable)
|
||||
|
||||
if (!deviceAndUserRelatedDataLiveLoaded) { return }
|
||||
|
||||
val deviceAndUserRelatedData = deviceAndUserRelatedDataLive.value
|
||||
val userRelatedData = deviceAndUserRelatedData?.userRelatedData
|
||||
val canSwitchToDefaultUser = deviceAndUserRelatedData?.deviceRelatedData?.canSwitchToDefaultUser ?: false
|
||||
|
||||
if (deviceAndUserRelatedData == null) {
|
||||
newResult.value = TimesWidgetContent.UnconfiguredDevice
|
||||
|
||||
return
|
||||
} else if (userRelatedData?.user?.type != UserType.Child) {
|
||||
newResult.value = TimesWidgetContent.NoChildUser(
|
||||
canSwitchToDefaultUser = canSwitchToDefaultUser
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
realTimeLogic.getRealTime(realTime)
|
||||
|
||||
categoryHandlingCache.reportStatus(
|
||||
user = userRelatedData,
|
||||
timeInMillis = realTime.timeInMillis,
|
||||
batteryStatus = logic.platformIntegration.getBatteryStatus(),
|
||||
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
|
||||
assumeCurrentDevice = true,
|
||||
currentNetworkId = null, // not relevant here
|
||||
hasPremiumOrLocalMode = false // not relevant here
|
||||
)
|
||||
|
||||
var maxTime = Long.MAX_VALUE
|
||||
|
||||
val categories = userRelatedData.sortedCategories().map { (level, category) ->
|
||||
val handling = categoryHandlingCache.get(categoryId = category.category.id)
|
||||
|
||||
maxTime = maxTime.coerceAtMost(handling.dependsOnMaxTime)
|
||||
|
||||
TimesWidgetContent.Categories.Item(
|
||||
categoryId = category.category.id,
|
||||
categoryName = category.category.title,
|
||||
level = level,
|
||||
remainingTimeToday = handling.remainingTime?.includingExtraTime
|
||||
)
|
||||
}
|
||||
|
||||
newResult.value = TimesWidgetContent.Categories(
|
||||
categories = categories,
|
||||
canSwitchToDefaultUser = canSwitchToDefaultUser
|
||||
)
|
||||
|
||||
if (isActive && maxTime != Long.MAX_VALUE) {
|
||||
val delay = maxTime - realTime.timeInMillis
|
||||
|
||||
handler.postDelayed(updateByClockRunnable, delay)
|
||||
}
|
||||
}
|
||||
|
||||
timeModificationListener = { update() }
|
||||
updateByClockRunnable = Runnable { update() }
|
||||
|
||||
newResult.addSource(deviceAndUserRelatedDataLive) { deviceAndUserRelatedDataLiveLoaded = true; update() }
|
||||
newResult.addSource(batteryStatusLive) { update() }
|
||||
|
||||
return newResult.ignoreUnchanged()
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -15,8 +15,8 @@
|
|||
*/
|
||||
package io.timelimit.android.ui.widget
|
||||
|
||||
data class TimesWidgetItem(
|
||||
val title: String,
|
||||
val level: Int,
|
||||
val remainingTimeToday: Long?
|
||||
)
|
||||
sealed class TimesWidgetItem {
|
||||
data class TextMessage(val textRessourceId: Int): TimesWidgetItem()
|
||||
data class Category(val category: TimesWidgetContent.Categories.Item): TimesWidgetItem()
|
||||
object DefaultUserButton: TimesWidgetItem()
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -15,107 +15,31 @@
|
|||
*/
|
||||
package io.timelimit.android.ui.widget
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.data.extensions.sortedCategories
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.RealTime
|
||||
import io.timelimit.android.logic.blockingreason.CategoryHandlingCache
|
||||
import io.timelimit.android.R
|
||||
|
||||
object TimesWidgetItems {
|
||||
fun with(logic: AppLogic): LiveData<List<TimesWidgetItem>> {
|
||||
val database = logic.database
|
||||
val realTimeLogic = logic.realTimeLogic
|
||||
val categoryHandlingCache = CategoryHandlingCache()
|
||||
val realTime = RealTime.newInstance()
|
||||
val handler = Threads.mainThreadHandler
|
||||
fun with(content: TimesWidgetContent, config: TimesWidgetConfig, appWidgetId: Int): List<TimesWidgetItem> = when (content) {
|
||||
is TimesWidgetContent.UnconfiguredDevice -> listOf(TimesWidgetItem.TextMessage(R.string.widget_msg_unconfigured))
|
||||
is TimesWidgetContent.NoChildUser -> {
|
||||
val base = TimesWidgetItem.TextMessage(R.string.widget_msg_no_child)
|
||||
|
||||
val deviceAndUserRelatedDataLive = database.derivedDataDao().getUserAndDeviceRelatedDataLive()
|
||||
var deviceAndUserRelatedDataLiveLoaded = false
|
||||
if (content.canSwitchToDefaultUser) listOf(base, TimesWidgetItem.DefaultUserButton)
|
||||
else listOf(base)
|
||||
}
|
||||
is TimesWidgetContent.Categories -> {
|
||||
val categoryFilter = config.widgetCategoriesByWidgetId[appWidgetId] ?: emptySet()
|
||||
|
||||
val batteryStatusLive = logic.platformIntegration.getBatteryStatusLive()
|
||||
val categoryItems = if (content.categories.isEmpty()) listOf(TimesWidgetItem.TextMessage(R.string.widget_msg_no_category))
|
||||
else if (categoryFilter.isEmpty()) content.categories.map { TimesWidgetItem.Category(it) }
|
||||
else {
|
||||
val filteredCategories = content.categories.filter { categoryFilter.contains(it.categoryId) }
|
||||
|
||||
lateinit var timeModificationListener: () -> Unit
|
||||
lateinit var updateByClockRunnable: Runnable
|
||||
var isActive = false
|
||||
|
||||
val newResult = object: MediatorLiveData<List<TimesWidgetItem>>() {
|
||||
override fun onActive() {
|
||||
super.onActive()
|
||||
|
||||
isActive = true
|
||||
|
||||
realTimeLogic.registerTimeModificationListener(timeModificationListener)
|
||||
|
||||
// ensure that the next update gets scheduled
|
||||
updateByClockRunnable.run()
|
||||
if (filteredCategories.isEmpty()) listOf(TimesWidgetItem.TextMessage(R.string.widget_msg_no_filtered_category))
|
||||
else filteredCategories.map { TimesWidgetItem.Category(it) }
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
super.onInactive()
|
||||
|
||||
isActive = true
|
||||
|
||||
realTimeLogic.unregisterTimeModificationListener(timeModificationListener)
|
||||
handler.removeCallbacks(updateByClockRunnable)
|
||||
if (content.canSwitchToDefaultUser) categoryItems + TimesWidgetItem.DefaultUserButton
|
||||
else categoryItems
|
||||
}
|
||||
}
|
||||
|
||||
fun update() {
|
||||
handler.removeCallbacks(updateByClockRunnable)
|
||||
|
||||
if (!deviceAndUserRelatedDataLiveLoaded) { return }
|
||||
|
||||
val deviceAndUserRelatedData = deviceAndUserRelatedDataLive.value
|
||||
val userRelatedData = deviceAndUserRelatedData?.userRelatedData
|
||||
|
||||
if (userRelatedData == null) {
|
||||
newResult.value = emptyList(); return
|
||||
}
|
||||
|
||||
realTimeLogic.getRealTime(realTime)
|
||||
|
||||
categoryHandlingCache.reportStatus(
|
||||
user = userRelatedData,
|
||||
timeInMillis = realTime.timeInMillis,
|
||||
batteryStatus = logic.platformIntegration.getBatteryStatus(),
|
||||
shouldTrustTimeTemporarily = realTime.shouldTrustTimeTemporarily,
|
||||
assumeCurrentDevice = true,
|
||||
currentNetworkId = null, // not relevant here
|
||||
hasPremiumOrLocalMode = false // not relevant here
|
||||
)
|
||||
|
||||
var maxTime = Long.MAX_VALUE
|
||||
|
||||
val list = userRelatedData.sortedCategories().map { (level, category) ->
|
||||
val handling = categoryHandlingCache.get(categoryId = category.category.id)
|
||||
|
||||
maxTime = maxTime.coerceAtMost(handling.dependsOnMaxTime)
|
||||
|
||||
TimesWidgetItem(
|
||||
title = category.category.title,
|
||||
level = level,
|
||||
remainingTimeToday = handling.remainingTime?.includingExtraTime
|
||||
)
|
||||
}
|
||||
|
||||
newResult.value = list
|
||||
|
||||
if (isActive && maxTime != Long.MAX_VALUE) {
|
||||
val delay = maxTime - realTime.timeInMillis
|
||||
|
||||
handler.postDelayed(updateByClockRunnable, delay)
|
||||
}
|
||||
}
|
||||
|
||||
timeModificationListener = { update() }
|
||||
updateByClockRunnable = Runnable { update() }
|
||||
|
||||
newResult.addSource(deviceAndUserRelatedDataLive) { deviceAndUserRelatedDataLiveLoaded = true; update() }
|
||||
newResult.addSource(batteryStatusLive) { update() }
|
||||
|
||||
return newResult.ignoreUnchanged()
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -17,13 +17,45 @@ package io.timelimit.android.ui.widget
|
|||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.widget.RemoteViews
|
||||
import androidx.core.content.getSystemService
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.integration.platform.android.BackgroundActionService
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
|
||||
class TimesWidgetProvider: AppWidgetProvider() {
|
||||
companion object {
|
||||
private const val LOG_TAG = "TimesWidgetProvider"
|
||||
|
||||
private fun handleUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
for (appWidgetId in appWidgetIds) {
|
||||
val views = RemoteViews(context.packageName, R.layout.widget_times)
|
||||
|
||||
views.setRemoteAdapter(android.R.id.list, TimesWidgetService.intent(context, appWidgetId))
|
||||
views.setPendingIntentTemplate(android.R.id.list, BackgroundActionService.getSwitchToDefaultUserIntent(context))
|
||||
views.setEmptyView(android.R.id.list, android.R.id.empty)
|
||||
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||
}
|
||||
|
||||
TimesWidgetService.notifyContentChanges(context)
|
||||
}
|
||||
|
||||
fun triggerUpdates(context: Context) {
|
||||
context.getSystemService<AppWidgetManager>()?.also { appWidgetManager ->
|
||||
val appWidgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, TimesWidgetProvider::class.java))
|
||||
|
||||
handleUpdate(context, appWidgetManager, appWidgetIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
super.onReceive(context, intent)
|
||||
|
||||
|
@ -34,13 +66,38 @@ class TimesWidgetProvider: AppWidgetProvider() {
|
|||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
|
||||
for (appWidgetId in appWidgetIds) {
|
||||
val views = RemoteViews(context.packageName, R.layout.widget_times)
|
||||
handleUpdate(context, appWidgetManager, appWidgetIds)
|
||||
}
|
||||
|
||||
views.setRemoteAdapter(android.R.id.list, Intent(context, TimesWidgetService::class.java))
|
||||
views.setEmptyView(android.R.id.list, android.R.id.empty)
|
||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||
super.onDeleted(context, appWidgetIds)
|
||||
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||
val database = DefaultAppLogic.with(context).database
|
||||
|
||||
Threads.database.execute {
|
||||
try {
|
||||
database.widgetCategory().deleteByWidgetIds(appWidgetIds)
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "onDisabled", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDisabled(context: Context) {
|
||||
super.onDisabled(context)
|
||||
|
||||
val database = DefaultAppLogic.with(context).database
|
||||
|
||||
Threads.database.execute {
|
||||
try {
|
||||
database.widgetCategory().deleteAll()
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "onDisabled", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -17,101 +17,150 @@ package io.timelimit.android.ui.widget
|
|||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import android.widget.RemoteViewsService
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.map
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.livedata.mergeLiveDataWaitForValues
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.ui.manage.child.category.CategoryItemLeftPadding
|
||||
import io.timelimit.android.util.TimeTextUtil
|
||||
|
||||
class TimesWidgetService: RemoteViewsService() {
|
||||
private val appWidgetManager: AppWidgetManager by lazy { AppWidgetManager.getInstance(this) }
|
||||
companion object {
|
||||
private const val EXTRA_APP_WIDGET_ID = "appWidgetId"
|
||||
|
||||
private val categoriesLive: LiveData<List<TimesWidgetItem>> by lazy {
|
||||
TimesWidgetItems.with(DefaultAppLogic.with(this))
|
||||
}
|
||||
fun intent(context: Context, appWidgetId: Int) = Intent(context, TimesWidgetService::class.java)
|
||||
.setData(Uri.parse("widget:$appWidgetId"))
|
||||
.putExtra(EXTRA_APP_WIDGET_ID, appWidgetId)
|
||||
|
||||
private var categoriesInput: List<TimesWidgetItem> = emptyList()
|
||||
private var categoriesCurrent: List<TimesWidgetItem> = categoriesInput
|
||||
|
||||
private val categoriesObserver = Observer<List<TimesWidgetItem>> {
|
||||
categoriesInput = it
|
||||
|
||||
val widgetIds = appWidgetManager.getAppWidgetIds(ComponentName(this, TimesWidgetProvider::class.java))
|
||||
fun notifyContentChanges(context: Context) {
|
||||
context.getSystemService<AppWidgetManager>()?.also { appWidgetManager ->
|
||||
val widgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, TimesWidgetProvider::class.java))
|
||||
|
||||
appWidgetManager.notifyAppWidgetViewDataChanged(widgetIds, android.R.id.list)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val content: LiveData<Pair<TimesWidgetContent, TimesWidgetConfig>> by lazy {
|
||||
val logic = DefaultAppLogic.with(this)
|
||||
|
||||
val content = TimesWidgetContentLoader.with(logic)
|
||||
val config = logic.database.widgetCategory().queryLive().map { TimesWidgetConfig(it) }
|
||||
|
||||
mergeLiveDataWaitForValues(content, config)
|
||||
}
|
||||
|
||||
private var observerCounter = 0
|
||||
private var contentInput: Pair<TimesWidgetContent, TimesWidgetConfig>? = null
|
||||
|
||||
private val contentObserver = Observer<Pair<TimesWidgetContent, TimesWidgetConfig>> {
|
||||
contentInput = it
|
||||
|
||||
notifyContentChanges(this)
|
||||
}
|
||||
|
||||
private fun createFactory(appWidgetId: Int) = object : RemoteViewsFactory {
|
||||
private var currentItems: List<TimesWidgetItem> = emptyList()
|
||||
|
||||
init { onDataSetChanged() }
|
||||
|
||||
private val factory = object : RemoteViewsFactory {
|
||||
override fun onCreate() {
|
||||
Threads.mainThreadHandler.post { categoriesLive.observeForever(categoriesObserver) }
|
||||
Threads.mainThreadHandler.post {
|
||||
if (observerCounter < 0) throw IllegalStateException()
|
||||
else if (observerCounter == 0) content.observeForever(contentObserver)
|
||||
|
||||
observerCounter++
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Threads.mainThreadHandler.post { categoriesLive.removeObserver(categoriesObserver) }
|
||||
Threads.mainThreadHandler.post {
|
||||
if (observerCounter <= 0) throw IllegalStateException()
|
||||
else if (observerCounter == 1) content.removeObserver(contentObserver)
|
||||
|
||||
observerCounter--
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDataSetChanged() {
|
||||
categoriesCurrent = categoriesInput
|
||||
currentItems = contentInput?.let { TimesWidgetItems.with(it.first, it.second, appWidgetId) } ?: emptyList()
|
||||
}
|
||||
|
||||
override fun getCount(): Int = categoriesCurrent.size
|
||||
override fun getCount(): Int = currentItems.size
|
||||
|
||||
override fun getViewAt(position: Int): RemoteViews {
|
||||
if (position >= categoriesCurrent.size) {
|
||||
return RemoteViews(packageName, R.layout.widget_times_item)
|
||||
if (position >= currentItems.size) {
|
||||
return RemoteViews(packageName, R.layout.widget_times_category_item)
|
||||
}
|
||||
|
||||
val category = categoriesCurrent[position]
|
||||
val result = RemoteViews(packageName, R.layout.widget_times_item)
|
||||
|
||||
result.setTextViewText(R.id.title, category.title)
|
||||
result.setTextViewText(
|
||||
R.id.subtitle,
|
||||
if (category.remainingTimeToday == null)
|
||||
getString(R.string.manage_child_category_no_time_limits)
|
||||
else
|
||||
TimeTextUtil.remaining(category.remainingTimeToday.toInt(), this@TimesWidgetService)
|
||||
)
|
||||
|
||||
result.setViewPadding(
|
||||
R.id.widgetInnerContainer,
|
||||
// not much space here => / 2
|
||||
CategoryItemLeftPadding.calculate(category.level, this@TimesWidgetService) / 2,
|
||||
0, 0, 0
|
||||
)
|
||||
fun createCategoryItem(title: String?, subtitle: String, paddingLeft: Int) = RemoteViews(packageName, R.layout.widget_times_category_item).also { result ->
|
||||
result.setTextViewText(R.id.title, title ?: "")
|
||||
result.setTextViewText(R.id.subtitle, subtitle)
|
||||
|
||||
result.setViewPadding(R.id.widgetInnerContainer, paddingLeft, 0, 0, 0)
|
||||
result.setViewVisibility(R.id.title, if (title != null) View.VISIBLE else View.GONE)
|
||||
result.setViewVisibility(R.id.topPadding, if (position == 0) View.VISIBLE else View.GONE)
|
||||
result.setViewVisibility(R.id.bottomPadding, if (position == count - 1) View.VISIBLE else View.GONE)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
override fun getLoadingView(): RemoteViews? {
|
||||
return null
|
||||
val item = currentItems[position]
|
||||
|
||||
return when (item) {
|
||||
is TimesWidgetItem.Category -> item.category.let { category ->
|
||||
createCategoryItem(
|
||||
title = if (category.remainingTimeToday == null)
|
||||
getString(R.string.manage_child_category_no_time_limits_short)
|
||||
else {
|
||||
val remainingTimeToday = category.remainingTimeToday.coerceAtLeast(0) / (1000 * 60)
|
||||
val minutes = remainingTimeToday % 60
|
||||
val hours = remainingTimeToday / 60
|
||||
|
||||
if (hours == 0L) "$minutes m"
|
||||
else "$hours h $minutes m"
|
||||
},
|
||||
subtitle = category.categoryName,
|
||||
// not much space here => / 2
|
||||
paddingLeft = CategoryItemLeftPadding.calculate(category.level, this@TimesWidgetService) / 2
|
||||
)
|
||||
}
|
||||
is TimesWidgetItem.TextMessage -> createCategoryItem(null, getString(item.textRessourceId), 0)
|
||||
is TimesWidgetItem.DefaultUserButton -> RemoteViews(packageName, R.layout.widget_times_button).also { result ->
|
||||
result.setTextViewText(R.id.button, getString(R.string.manage_device_default_user_switch_btn))
|
||||
result.setOnClickFillInIntent(R.id.button, Intent())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getViewTypeCount(): Int {
|
||||
return 1
|
||||
}
|
||||
override fun getLoadingView(): RemoteViews? = null
|
||||
|
||||
override fun getViewTypeCount(): Int = 2
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
if (position >= categoriesCurrent.size) {
|
||||
if (position >= currentItems.size) {
|
||||
return -(position.toLong())
|
||||
}
|
||||
|
||||
return categoriesCurrent[position].hashCode().toLong()
|
||||
val item = currentItems[position]
|
||||
|
||||
return when (item) {
|
||||
is TimesWidgetItem.Category -> item.category.categoryId.hashCode()
|
||||
else -> item.hashCode()
|
||||
}.toLong()
|
||||
}
|
||||
|
||||
override fun hasStableIds(): Boolean {
|
||||
return true
|
||||
}
|
||||
override fun hasStableIds(): Boolean = true
|
||||
}
|
||||
|
||||
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = factory
|
||||
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = createFactory(
|
||||
intent.getIntExtra(EXTRA_APP_WIDGET_ID, 0)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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.widget.config
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import io.timelimit.android.R
|
||||
|
||||
class UnconfiguredDialogFragment: DialogFragment() {
|
||||
companion object {
|
||||
const val DIALOG_TAG = "UnconfiguredDialogFragment"
|
||||
}
|
||||
|
||||
private val model: WidgetConfigModel by activityViewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
model.state.observe(this) {
|
||||
if (!(it is WidgetConfigModel.State.Unconfigured)) dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = AlertDialog.Builder(requireContext(), theme)
|
||||
.setMessage(R.string.widget_msg_unconfigured)
|
||||
.setPositiveButton(R.string.generic_ok) { _, _ -> model.userCancel() }
|
||||
.create()
|
||||
|
||||
override fun onCancel(dialog: DialogInterface) {
|
||||
super.onCancel(dialog)
|
||||
|
||||
model.userCancel()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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.widget.config
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
|
||||
class WidgetConfigActivity: FragmentActivity() {
|
||||
private val model: WidgetConfigModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (model.state.value == WidgetConfigModel.State.WaitingForInit) {
|
||||
model.init(
|
||||
intent?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||
?: AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
)
|
||||
}
|
||||
|
||||
model.state.observe(this) { state ->
|
||||
when (state) {
|
||||
is WidgetConfigModel.State.WaitingForInit -> {/* ignore */}
|
||||
is WidgetConfigModel.State.Working -> {/* ignore */}
|
||||
is WidgetConfigModel.State.Unconfigured -> {
|
||||
if (supportFragmentManager.findFragmentByTag(UnconfiguredDialogFragment.DIALOG_TAG) == null) {
|
||||
UnconfiguredDialogFragment().showSafe(supportFragmentManager, UnconfiguredDialogFragment.DIALOG_TAG)
|
||||
}
|
||||
}
|
||||
is WidgetConfigModel.State.ShowModeSelection -> {
|
||||
if (supportFragmentManager.findFragmentByTag(WidgetConfigModeDialogFragment.DIALOG_TAG) == null) {
|
||||
WidgetConfigModeDialogFragment().showSafe(supportFragmentManager, WidgetConfigModeDialogFragment.DIALOG_TAG)
|
||||
}
|
||||
}
|
||||
is WidgetConfigModel.State.ShowCategorySelection -> {
|
||||
if (supportFragmentManager.findFragmentByTag(WidgetConfigFilterDialogFragment.DIALOG_TAG) == null) {
|
||||
WidgetConfigFilterDialogFragment().showSafe(supportFragmentManager, WidgetConfigFilterDialogFragment.DIALOG_TAG)
|
||||
}
|
||||
}
|
||||
is WidgetConfigModel.State.Done -> {
|
||||
setResult(
|
||||
RESULT_OK,
|
||||
Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, state.appWidgetId)
|
||||
)
|
||||
|
||||
finish()
|
||||
}
|
||||
is WidgetConfigModel.State.ErrorCancel -> {
|
||||
Toast.makeText(this, R.string.error_general, Toast.LENGTH_SHORT).show()
|
||||
|
||||
finish()
|
||||
}
|
||||
is WidgetConfigModel.State.UserCancel -> finish()
|
||||
}
|
||||
}
|
||||
|
||||
setResult(RESULT_CANCELED)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* 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.widget.config
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import io.timelimit.android.R
|
||||
|
||||
class WidgetConfigFilterDialogFragment: DialogFragment() {
|
||||
companion object {
|
||||
private const val STATE_CATEGORY_IDS = "categoryIds"
|
||||
|
||||
const val DIALOG_TAG = "WidgetConfigFilterDialogFragment"
|
||||
}
|
||||
|
||||
private val model: WidgetConfigModel by activityViewModels()
|
||||
private val selectedCategoryIds = mutableSetOf<String>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
model.state.value?.also { state ->
|
||||
if (state is WidgetConfigModel.State.ShowCategorySelection) {
|
||||
selectedCategoryIds.clear()
|
||||
selectedCategoryIds.addAll(state.selectedFilterCategories)
|
||||
}
|
||||
}
|
||||
|
||||
savedInstanceState?.also {
|
||||
selectedCategoryIds.clear()
|
||||
selectedCategoryIds.addAll(it.getStringArray(STATE_CATEGORY_IDS) ?: emptyArray())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putStringArray(STATE_CATEGORY_IDS, selectedCategoryIds.toTypedArray())
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val state = model.state.value
|
||||
|
||||
if (!(state is WidgetConfigModel.State.ShowCategorySelection)) return super.onCreateDialog(savedInstanceState)
|
||||
|
||||
return AlertDialog.Builder(requireContext(), theme)
|
||||
.setMultiChoiceItems(
|
||||
state.categories.map { it.title }.toTypedArray(),
|
||||
state.categories.map { selectedCategoryIds.contains(it.id) }.toBooleanArray()
|
||||
) { _, index, checked ->
|
||||
val categoryId = state.categories[index].id
|
||||
|
||||
if (checked) selectedCategoryIds.add(categoryId) else selectedCategoryIds.remove(categoryId)
|
||||
}
|
||||
.setPositiveButton(R.string.wiazrd_next) { _, _ ->
|
||||
if (selectedCategoryIds.isEmpty()) {
|
||||
Toast.makeText(requireContext(), R.string.widget_config_error_filter_empty, Toast.LENGTH_SHORT).show()
|
||||
|
||||
model.userCancel()
|
||||
} else model.selectFilterItems(selectedCategoryIds)
|
||||
}
|
||||
.create()
|
||||
}
|
||||
|
||||
override fun onCancel(dialog: DialogInterface) {
|
||||
super.onCancel(dialog)
|
||||
|
||||
model.userCancel()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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.widget.config
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import io.timelimit.android.R
|
||||
|
||||
class WidgetConfigModeDialogFragment: DialogFragment() {
|
||||
companion object {
|
||||
private const val STATE_SELECTION = "selection"
|
||||
|
||||
const val DIALOG_TAG = "WidgetConfigModeDialogFragment"
|
||||
}
|
||||
|
||||
private val model: WidgetConfigModel by activityViewModels()
|
||||
private var selection = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
model.state.value?.also {
|
||||
if (it is WidgetConfigModel.State.ShowModeSelection) {
|
||||
selection = if (it.selectedFilterCategories.isEmpty()) 0 else 1
|
||||
}
|
||||
}
|
||||
|
||||
savedInstanceState?.also { selection = it.getInt(STATE_SELECTION) }
|
||||
|
||||
model.state.observe(this) {
|
||||
if (!(it is WidgetConfigModel.State.ShowModeSelection)) dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
outState.putInt(STATE_SELECTION, selection)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = AlertDialog.Builder(requireContext(), theme)
|
||||
.setSingleChoiceItems(
|
||||
arrayOf(
|
||||
getString(R.string.widget_config_mode_all),
|
||||
getString(R.string.widget_config_mode_filter)
|
||||
),
|
||||
selection
|
||||
) { _, selectedItemIndex -> selection = selectedItemIndex }
|
||||
.setPositiveButton(R.string.wiazrd_next) { _, _ ->
|
||||
if (selection == 0) model.selectModeAll()
|
||||
else model.selectModeFilter()
|
||||
}
|
||||
.create()
|
||||
|
||||
override fun onCancel(dialog: DialogInterface) {
|
||||
super.onCancel(dialog)
|
||||
|
||||
model.userCancel()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* 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.widget.config
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.data.extensions.sortedCategories
|
||||
import io.timelimit.android.data.model.Category
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.data.model.WidgetCategory
|
||||
import io.timelimit.android.livedata.castDown
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class WidgetConfigModel(application: Application): AndroidViewModel(application) {
|
||||
companion object {
|
||||
private const val LOG_TAG = "WidgetConfigModel"
|
||||
}
|
||||
|
||||
private val stateInternal = MutableLiveData<State>().apply { value = State.WaitingForInit }
|
||||
private val database = DefaultAppLogic.with(application).database
|
||||
|
||||
val state = stateInternal.castDown()
|
||||
|
||||
fun init(appWidgetId: Int) {
|
||||
if (state.value != State.WaitingForInit) return
|
||||
stateInternal.value = State.Working
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val (deviceAndUserRelatedData, selectedFilterCategories) = Threads.database.executeAndWait {
|
||||
database.runInTransaction {
|
||||
val deviceAndUserRelatedData = database.derivedDataDao().getUserAndDeviceRelatedDataSync()
|
||||
val selectedFilterCategories = database.widgetCategory().queryByWidgetIdSync(appWidgetId).toSet()
|
||||
|
||||
Pair(deviceAndUserRelatedData, selectedFilterCategories)
|
||||
}
|
||||
}
|
||||
|
||||
val isNoChildUser = deviceAndUserRelatedData?.userRelatedData?.user?.type != UserType.Child
|
||||
val currentUserCategories = deviceAndUserRelatedData?.userRelatedData?.sortedCategories()?.map { it.second.category }
|
||||
|
||||
if (currentUserCategories == null || isNoChildUser) {
|
||||
stateInternal.value = State.Unconfigured
|
||||
|
||||
return@launch
|
||||
}
|
||||
|
||||
stateInternal.value = State.ShowModeSelection(appWidgetId, selectedFilterCategories, currentUserCategories)
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "selectModeAll", ex)
|
||||
}
|
||||
|
||||
stateInternal.value = State.ErrorCancel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectModeAll() {
|
||||
val oldState = state.value
|
||||
if (!(oldState is State.ShowModeSelection)) return
|
||||
stateInternal.value = State.Working
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
Threads.database.executeAndWait {
|
||||
database.widgetCategory().deleteByWidgetId(oldState.appWidgetId)
|
||||
}
|
||||
|
||||
stateInternal.value = State.Done(appWidgetId = oldState.appWidgetId)
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "selectModeAll", ex)
|
||||
}
|
||||
|
||||
stateInternal.value = State.ErrorCancel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectModeFilter() {
|
||||
val oldState = state.value
|
||||
if (!(oldState is State.ShowModeSelection)) return
|
||||
stateInternal.value = State.Working
|
||||
|
||||
stateInternal.value = State.ShowCategorySelection(
|
||||
appWidgetId = oldState.appWidgetId,
|
||||
selectedFilterCategories = oldState.selectedFilterCategories,
|
||||
categories = oldState.categories
|
||||
)
|
||||
}
|
||||
|
||||
fun selectFilterItems(selectedCategoryIds: Set<String>) {
|
||||
val oldState = state.value
|
||||
if (!(oldState is State.ShowCategorySelection)) return
|
||||
stateInternal.value = State.Working
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
Threads.database.executeAndWait {
|
||||
val userAndDeviceRelatedData = database.derivedDataDao().getUserAndDeviceRelatedDataSync()
|
||||
val currentCategoryIds = userAndDeviceRelatedData!!.userRelatedData!!.categoryById.keys
|
||||
|
||||
val categoriesToRemove = currentCategoryIds - selectedCategoryIds
|
||||
val categoriesToAdd = selectedCategoryIds.filter { currentCategoryIds.contains(it) }
|
||||
|
||||
if (categoriesToRemove.isNotEmpty()) {
|
||||
database.widgetCategory().deleteByWidgetIdAndCategoryIds(
|
||||
oldState.appWidgetId, categoriesToRemove.toList()
|
||||
)
|
||||
}
|
||||
|
||||
if (categoriesToAdd.isNotEmpty()) {
|
||||
database.widgetCategory().insert(
|
||||
categoriesToAdd.toList().map { WidgetCategory(oldState.appWidgetId, it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
stateInternal.value = State.Done(appWidgetId = oldState.appWidgetId)
|
||||
} catch (ex: Exception) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOG_TAG, "selectModeAll", ex)
|
||||
}
|
||||
|
||||
stateInternal.value = State.ErrorCancel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun userCancel() {
|
||||
stateInternal.value = State.UserCancel
|
||||
}
|
||||
|
||||
sealed class State {
|
||||
object WaitingForInit: State()
|
||||
object Working: State()
|
||||
data class ShowModeSelection(val appWidgetId: Int, val selectedFilterCategories: Set<String>, val categories: List<Category>): State()
|
||||
data class ShowCategorySelection(val appWidgetId: Int, val selectedFilterCategories: Set<String>, val categories: List<Category>): State()
|
||||
data class Done(val appWidgetId: Int): State()
|
||||
object Unconfigured: State()
|
||||
object UserCancel: State()
|
||||
object ErrorCancel: State()
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 24 KiB |
Binary file not shown.
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 23 KiB |
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
@ -17,7 +17,7 @@
|
|||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/widget_background">
|
||||
android:background="@color/widgetBackground">
|
||||
|
||||
<ListView
|
||||
android:divider="@color/transparent"
|
||||
|
@ -26,13 +26,11 @@
|
|||
android:layout_height="match_parent"
|
||||
android:listSelector="@android:color/transparent"/>
|
||||
|
||||
<TextView
|
||||
<ProgressBar
|
||||
android:id="@android:id/empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="@string/widget_empty_view"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:textColor="@color/white"/>
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
</FrameLayout>
|
33
app/src/main/res/layout/widget_times_button.xml
Normal file
33
app/src/main/res/layout/widget_times_button.xml
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
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/>.
|
||||
-->
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:background="@color/widget_background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button"
|
||||
tools:text="Action"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</FrameLayout>
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
@ -35,20 +35,21 @@
|
|||
android:layout_height="12dp" />
|
||||
|
||||
<TextView
|
||||
tools:text="Erlaubte Apps"
|
||||
tools:text="3 m"
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:textColor="@color/white"/>
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/widgetText"/>
|
||||
|
||||
<TextView
|
||||
tools:text="wenige Minuten verbleibend"
|
||||
tools:text="Erlaubte Apps"
|
||||
android:id="@+id/subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:textAppearanceSmall"
|
||||
android:textColor="@color/white"/>
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:textColor="@color/widgetText"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/bottomPadding"
|
58
app/src/main/res/layout/widget_times_preview.xml
Normal file
58
app/src/main/res/layout/widget_times_preview.xml
Normal file
|
@ -0,0 +1,58 @@
|
|||
<!--
|
||||
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/>.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
android:background="@color/widgetBackground">
|
||||
|
||||
<TextView
|
||||
tools:ignore="HardcodedText"
|
||||
android:text="no limit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/widgetText"/>
|
||||
|
||||
<TextView
|
||||
android:paddingBottom="8dp"
|
||||
android:text="@string/setup_category_allowed"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:textColor="@color/widgetText"/>
|
||||
|
||||
<TextView
|
||||
tools:ignore="HardcodedText"
|
||||
android:text="30 m"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:textAppearanceLarge"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/widgetText"/>
|
||||
|
||||
<TextView
|
||||
android:text="@string/setup_category_games"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:textAppearanceMedium"
|
||||
android:textColor="@color/widgetText"/>
|
||||
|
||||
</LinearLayout>
|
|
@ -769,6 +769,7 @@
|
|||
<string name="manage_child_tab_other">erweiterte Einstellungen</string>
|
||||
|
||||
<string name="manage_child_category_no_time_limits">Keine Zeitbegrenzung</string>
|
||||
<string name="manage_child_category_no_time_limits_short">keine Begrenzung</string>
|
||||
|
||||
<string name="manage_child_categories_intro_title">Kategorien</string>
|
||||
<string name="manage_child_categories_intro_text">
|
||||
|
@ -1608,10 +1609,13 @@
|
|||
|
||||
<string name="util_day_from_to">%s bis %s</string>
|
||||
|
||||
<string name="widget_empty_view">Wenn dieses Gerät einem Kind zugeordnet
|
||||
wird und dieses Kind Kategorien hat, dann sollten diese Kategorien
|
||||
hier angezeigt werden.
|
||||
</string>
|
||||
<string name="widget_msg_unconfigured">Dieses Widget funktioniert nur auf beschränkten Geräten</string>
|
||||
<string name="widget_msg_no_child">Momentan hat dieses Gerät keinen Kind-Benutzer</string>
|
||||
<string name="widget_msg_no_category">Das Kind hat keine Kategorien</string>
|
||||
<string name="widget_msg_no_filtered_category">Das Kind hat keine für das Widget ausgewählten Kategorien</string>
|
||||
<string name="widget_config_mode_all">alle Kategorien anzeigen</string>
|
||||
<string name="widget_config_mode_filter">sichtbare Kategorien filtern</string>
|
||||
<string name="widget_config_error_filter_empty">Sie müssen mindestens eine Kategorie auswählen</string>
|
||||
|
||||
<string name="wiazrd_next">Weiter</string>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
@ -23,4 +23,7 @@
|
|||
<color name="orange_text">#ff6f00</color>
|
||||
<color name="text_green">#4caf50</color>
|
||||
<color name="text_red">#f44</color>
|
||||
|
||||
<color name="widgetBackground">#000000</color>
|
||||
<color name="widgetText">#ffffff</color>
|
||||
</resources>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
@ -17,7 +17,8 @@
|
|||
<color name="colorPrimary">#009688</color>
|
||||
<color name="colorPrimaryDark">#00796B</color>
|
||||
<color name="colorAccent">#1976d2</color>
|
||||
<color name="widget_background">#33000000</color>
|
||||
<color name="widgetBackground">#ffffff</color>
|
||||
<color name="widgetText">#000000</color>
|
||||
|
||||
<color name="transparent">#00000000</color>
|
||||
<color name="gray">#9E9E9E</color>
|
||||
|
|
|
@ -821,6 +821,7 @@
|
|||
<string name="manage_child_tab_other">Advanced settings</string>
|
||||
|
||||
<string name="manage_child_category_no_time_limits">no time limit</string>
|
||||
<string name="manage_child_category_no_time_limits_short">no limit</string>
|
||||
|
||||
<string name="manage_child_categories_intro_title">Categories</string>
|
||||
<string name="manage_child_categories_intro_text">
|
||||
|
@ -1656,10 +1657,13 @@
|
|||
|
||||
<string name="util_day_from_to">%s to %s</string>
|
||||
|
||||
<string name="widget_empty_view">If this device is assigned to a child
|
||||
and this child has got categories, then the categories should
|
||||
appear here.
|
||||
</string>
|
||||
<string name="widget_msg_unconfigured">This Widget only works on limited devices</string>
|
||||
<string name="widget_msg_no_child">This device is not used by a child right now</string>
|
||||
<string name="widget_msg_no_category">This child does not have any categories</string>
|
||||
<string name="widget_msg_no_filtered_category">For this child, no visible categories were chosen for this widget</string>
|
||||
<string name="widget_config_mode_all">Show all Categories</string>
|
||||
<string name="widget_config_mode_filter">Filter visible Categories</string>
|
||||
<string name="widget_config_error_filter_empty">You must select at least one category</string>
|
||||
|
||||
<string name="wiazrd_next">Next</string>
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||
TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation version 3 of the License.
|
||||
|
@ -19,4 +20,7 @@
|
|||
android:updatePeriodMillis="0"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:previewImage="@drawable/widget_preview"
|
||||
android:configure="io.timelimit.android.ui.widget.config.WidgetConfigActivity"
|
||||
android:widgetFeatures="reconfigurable|configuration_optional"
|
||||
android:previewLayout="@layout/widget_times_preview"
|
||||
android:widgetCategory="home_screen" />
|
Loading…
Add table
Add a link
Reference in a new issue