mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 17:59:51 +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:taskAffinity=":update"
|
||||||
android:launchMode="singleTop" />
|
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 -->
|
<!-- system integration -->
|
||||||
|
|
||||||
<receiver android:name=".integration.platform.android.receiver.BootReceiver" android:exported="false">
|
<receiver android:name=".integration.platform.android.receiver.BootReceiver" android:exported="false">
|
||||||
|
|
|
@ -46,6 +46,7 @@ interface Database {
|
||||||
fun cryptContainerKeyResult(): CryptContainerKeyResultDao
|
fun cryptContainerKeyResult(): CryptContainerKeyResultDao
|
||||||
fun deviceKey(): DeviceKeyDao
|
fun deviceKey(): DeviceKeyDao
|
||||||
fun u2f(): U2FDao
|
fun u2f(): U2FDao
|
||||||
|
fun widgetCategory(): WidgetCategoryDao
|
||||||
|
|
||||||
fun <T> runInTransaction(block: () -> T): T
|
fun <T> runInTransaction(block: () -> T): T
|
||||||
fun <T> runInUnobservedTransaction(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(
|
val ALL = arrayOf(
|
||||||
MIGRATE_TO_V2,
|
MIGRATE_TO_V2,
|
||||||
MIGRATE_TO_V3,
|
MIGRATE_TO_V3,
|
||||||
|
@ -369,6 +376,7 @@ object DatabaseMigrations {
|
||||||
MIGRATE_TO_V41,
|
MIGRATE_TO_V41,
|
||||||
MIGRATE_TO_V42,
|
MIGRATE_TO_V42,
|
||||||
MIGRATE_TP_V43,
|
MIGRATE_TP_V43,
|
||||||
MIGRATE_TO_V44
|
MIGRATE_TO_V44,
|
||||||
|
MIGRATE_TO_V45
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,8 +58,9 @@ import java.util.concurrent.TimeUnit
|
||||||
CryptContainerPendingKeyRequest::class,
|
CryptContainerPendingKeyRequest::class,
|
||||||
CryptContainerKeyResult::class,
|
CryptContainerKeyResult::class,
|
||||||
DevicePublicKey::class,
|
DevicePublicKey::class,
|
||||||
UserU2FKey::class
|
UserU2FKey::class,
|
||||||
], version = 44)
|
WidgetCategory::class
|
||||||
|
], version = 45)
|
||||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||||
companion object {
|
companion object {
|
||||||
private val lock = 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
|
* 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
|
||||||
|
@ -44,8 +44,13 @@ class BackgroundActionService: Service() {
|
||||||
fun prepareRevokeTemporarilyAllowed(context: Context) = Intent(context, BackgroundActionService::class.java)
|
fun prepareRevokeTemporarilyAllowed(context: Context) = Intent(context, BackgroundActionService::class.java)
|
||||||
.putExtra(ACTION, ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS)
|
.putExtra(ACTION, ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS)
|
||||||
|
|
||||||
fun prepareSwitchToDefaultUser(context: Context) = Intent(context, BackgroundActionService::class.java)
|
fun getSwitchToDefaultUserIntent(context: Context) = PendingIntent.getService(
|
||||||
.putExtra(ACTION, ACTION_SWITCH_TO_DEFAULT_USER)
|
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)
|
fun prepareDismissNotification(context: Context, type: Int, id: String) = Intent(context, BackgroundActionService::class.java)
|
||||||
.putExtra(ACTION, ACTION_DISMISS_NOTIFICATION)
|
.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
|
* 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,7 +17,6 @@ package io.timelimit.android.integration.platform.android
|
||||||
|
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -89,12 +88,7 @@ class BackgroundService: Service() {
|
||||||
NotificationCompat.Action.Builder(
|
NotificationCompat.Action.Builder(
|
||||||
R.drawable.ic_account_circle_black_24dp,
|
R.drawable.ic_account_circle_black_24dp,
|
||||||
context.getString(R.string.manage_device_default_user_switch_btn),
|
context.getString(R.string.manage_device_default_user_switch_btn),
|
||||||
PendingIntent.getService(
|
BackgroundActionService.getSwitchToDefaultUserIntent(context)
|
||||||
context,
|
|
||||||
PendingIntentIds.SWITCH_TO_DEFAULT_USER,
|
|
||||||
BackgroundActionService.prepareSwitchToDefaultUser(context),
|
|
||||||
PendingIntentIds.PENDING_INTENT_FLAGS
|
|
||||||
)
|
|
||||||
).build()
|
).build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import io.timelimit.android.sync.SyncUtil
|
||||||
import io.timelimit.android.sync.network.api.ServerApi
|
import io.timelimit.android.sync.network.api.ServerApi
|
||||||
import io.timelimit.android.sync.websocket.NetworkStatusInterface
|
import io.timelimit.android.sync.websocket.NetworkStatusInterface
|
||||||
import io.timelimit.android.sync.websocket.WebsocketClientCreator
|
import io.timelimit.android.sync.websocket.WebsocketClientCreator
|
||||||
|
import io.timelimit.android.ui.widget.TimesWidgetProvider
|
||||||
|
|
||||||
class AppLogic(
|
class AppLogic(
|
||||||
val platformIntegration: PlatformIntegration,
|
val platformIntegration: PlatformIntegration,
|
||||||
|
@ -98,6 +99,7 @@ class AppLogic(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
WatchdogLogic(this)
|
WatchdogLogic(this)
|
||||||
|
TimesWidgetProvider.triggerUpdates(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
val suspendAppsLogic = SuspendAppsLogic(this)
|
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
|
* 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,8 +15,8 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.ui.widget
|
package io.timelimit.android.ui.widget
|
||||||
|
|
||||||
data class TimesWidgetItem(
|
sealed class TimesWidgetItem {
|
||||||
val title: String,
|
data class TextMessage(val textRessourceId: Int): TimesWidgetItem()
|
||||||
val level: Int,
|
data class Category(val category: TimesWidgetContent.Categories.Item): TimesWidgetItem()
|
||||||
val remainingTimeToday: Long?
|
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
|
* 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,107 +15,31 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.ui.widget
|
package io.timelimit.android.ui.widget
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import io.timelimit.android.R
|
||||||
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
|
|
||||||
|
|
||||||
object TimesWidgetItems {
|
object TimesWidgetItems {
|
||||||
fun with(logic: AppLogic): LiveData<List<TimesWidgetItem>> {
|
fun with(content: TimesWidgetContent, config: TimesWidgetConfig, appWidgetId: Int): List<TimesWidgetItem> = when (content) {
|
||||||
val database = logic.database
|
is TimesWidgetContent.UnconfiguredDevice -> listOf(TimesWidgetItem.TextMessage(R.string.widget_msg_unconfigured))
|
||||||
val realTimeLogic = logic.realTimeLogic
|
is TimesWidgetContent.NoChildUser -> {
|
||||||
val categoryHandlingCache = CategoryHandlingCache()
|
val base = TimesWidgetItem.TextMessage(R.string.widget_msg_no_child)
|
||||||
val realTime = RealTime.newInstance()
|
|
||||||
val handler = Threads.mainThreadHandler
|
|
||||||
|
|
||||||
val deviceAndUserRelatedDataLive = database.derivedDataDao().getUserAndDeviceRelatedDataLive()
|
if (content.canSwitchToDefaultUser) listOf(base, TimesWidgetItem.DefaultUserButton)
|
||||||
var deviceAndUserRelatedDataLiveLoaded = false
|
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
|
if (filteredCategories.isEmpty()) listOf(TimesWidgetItem.TextMessage(R.string.widget_msg_no_filtered_category))
|
||||||
lateinit var updateByClockRunnable: Runnable
|
else filteredCategories.map { TimesWidgetItem.Category(it) }
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInactive() {
|
if (content.canSwitchToDefaultUser) categoryItems + TimesWidgetItem.DefaultUserButton
|
||||||
super.onInactive()
|
else categoryItems
|
||||||
|
|
||||||
isActive = true
|
|
||||||
|
|
||||||
realTimeLogic.unregisterTimeModificationListener(timeModificationListener)
|
|
||||||
handler.removeCallbacks(updateByClockRunnable)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
* 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,13 +17,45 @@ package io.timelimit.android.ui.widget
|
||||||
|
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
import android.appwidget.AppWidgetProvider
|
import android.appwidget.AppWidgetProvider
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.R
|
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
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
|
||||||
class TimesWidgetProvider: AppWidgetProvider() {
|
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?) {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
super.onReceive(context, intent)
|
super.onReceive(context, intent)
|
||||||
|
|
||||||
|
@ -34,13 +66,38 @@ class TimesWidgetProvider: AppWidgetProvider() {
|
||||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||||
|
|
||||||
for (appWidgetId in appWidgetIds) {
|
handleUpdate(context, appWidgetManager, appWidgetIds)
|
||||||
val views = RemoteViews(context.packageName, R.layout.widget_times)
|
}
|
||||||
|
|
||||||
views.setRemoteAdapter(android.R.id.list, Intent(context, TimesWidgetService::class.java))
|
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||||
views.setEmptyView(android.R.id.list, android.R.id.empty)
|
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
|
* 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,101 +17,150 @@ package io.timelimit.android.ui.widget
|
||||||
|
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import android.widget.RemoteViewsService
|
import android.widget.RemoteViewsService
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.map
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.async.Threads
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.livedata.mergeLiveDataWaitForValues
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.ui.manage.child.category.CategoryItemLeftPadding
|
import io.timelimit.android.ui.manage.child.category.CategoryItemLeftPadding
|
||||||
import io.timelimit.android.util.TimeTextUtil
|
|
||||||
|
|
||||||
class TimesWidgetService: RemoteViewsService() {
|
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 {
|
fun intent(context: Context, appWidgetId: Int) = Intent(context, TimesWidgetService::class.java)
|
||||||
TimesWidgetItems.with(DefaultAppLogic.with(this))
|
.setData(Uri.parse("widget:$appWidgetId"))
|
||||||
}
|
.putExtra(EXTRA_APP_WIDGET_ID, appWidgetId)
|
||||||
|
|
||||||
private var categoriesInput: List<TimesWidgetItem> = emptyList()
|
fun notifyContentChanges(context: Context) {
|
||||||
private var categoriesCurrent: List<TimesWidgetItem> = categoriesInput
|
context.getSystemService<AppWidgetManager>()?.also { appWidgetManager ->
|
||||||
|
val widgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, TimesWidgetProvider::class.java))
|
||||||
private val categoriesObserver = Observer<List<TimesWidgetItem>> {
|
|
||||||
categoriesInput = it
|
|
||||||
|
|
||||||
val widgetIds = appWidgetManager.getAppWidgetIds(ComponentName(this, TimesWidgetProvider::class.java))
|
|
||||||
|
|
||||||
appWidgetManager.notifyAppWidgetViewDataChanged(widgetIds, android.R.id.list)
|
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() {
|
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() {
|
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() {
|
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 {
|
override fun getViewAt(position: Int): RemoteViews {
|
||||||
if (position >= categoriesCurrent.size) {
|
if (position >= currentItems.size) {
|
||||||
return RemoteViews(packageName, R.layout.widget_times_item)
|
return RemoteViews(packageName, R.layout.widget_times_category_item)
|
||||||
}
|
}
|
||||||
|
|
||||||
val category = categoriesCurrent[position]
|
fun createCategoryItem(title: String?, subtitle: String, paddingLeft: Int) = RemoteViews(packageName, R.layout.widget_times_category_item).also { result ->
|
||||||
val result = RemoteViews(packageName, R.layout.widget_times_item)
|
result.setTextViewText(R.id.title, title ?: "")
|
||||||
|
result.setTextViewText(R.id.subtitle, subtitle)
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
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.topPadding, if (position == 0) View.VISIBLE else View.GONE)
|
||||||
result.setViewVisibility(R.id.bottomPadding, if (position == count - 1) 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? {
|
val item = currentItems[position]
|
||||||
return null
|
|
||||||
|
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 {
|
override fun getLoadingView(): RemoteViews? = null
|
||||||
return 1
|
|
||||||
}
|
override fun getViewTypeCount(): Int = 2
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
override fun getItemId(position: Int): Long {
|
||||||
if (position >= categoriesCurrent.size) {
|
if (position >= currentItems.size) {
|
||||||
return -(position.toLong())
|
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 {
|
override fun hasStableIds(): Boolean = true
|
||||||
return 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
|
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.
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@color/widget_background">
|
android:background="@color/widgetBackground">
|
||||||
|
|
||||||
<ListView
|
<ListView
|
||||||
android:divider="@color/transparent"
|
android:divider="@color/transparent"
|
||||||
|
@ -26,13 +26,11 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:listSelector="@android:color/transparent"/>
|
android:listSelector="@android:color/transparent"/>
|
||||||
|
|
||||||
<TextView
|
<ProgressBar
|
||||||
android:id="@android:id/empty"
|
android:id="@android:id/empty"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
style="?android:attr/progressBarStyleLarge"
|
||||||
android:text="@string/widget_empty_view"
|
android:layout_gravity="center" />
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:textColor="@color/white"/>
|
|
||||||
|
|
||||||
</FrameLayout>
|
</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"?>
|
<?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
|
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.
|
||||||
|
@ -35,20 +35,21 @@
|
||||||
android:layout_height="12dp" />
|
android:layout_height="12dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
tools:text="Erlaubte Apps"
|
tools:text="3 m"
|
||||||
android:id="@+id/title"
|
android:id="@+id/title"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
android:textAppearance="?android:textAppearanceLarge"
|
||||||
android:textColor="@color/white"/>
|
android:textStyle="bold"
|
||||||
|
android:textColor="@color/widgetText"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
tools:text="wenige Minuten verbleibend"
|
tools:text="Erlaubte Apps"
|
||||||
android:id="@+id/subtitle"
|
android:id="@+id/subtitle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textAppearance="?android:textAppearanceSmall"
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
android:textColor="@color/white"/>
|
android:textColor="@color/widgetText"/>
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/bottomPadding"
|
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_tab_other">erweiterte Einstellungen</string>
|
||||||
|
|
||||||
<string name="manage_child_category_no_time_limits">Keine Zeitbegrenzung</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_title">Kategorien</string>
|
||||||
<string name="manage_child_categories_intro_text">
|
<string name="manage_child_categories_intro_text">
|
||||||
|
@ -1608,10 +1609,13 @@
|
||||||
|
|
||||||
<string name="util_day_from_to">%s bis %s</string>
|
<string name="util_day_from_to">%s bis %s</string>
|
||||||
|
|
||||||
<string name="widget_empty_view">Wenn dieses Gerät einem Kind zugeordnet
|
<string name="widget_msg_unconfigured">Dieses Widget funktioniert nur auf beschränkten Geräten</string>
|
||||||
wird und dieses Kind Kategorien hat, dann sollten diese Kategorien
|
<string name="widget_msg_no_child">Momentan hat dieses Gerät keinen Kind-Benutzer</string>
|
||||||
hier angezeigt werden.
|
<string name="widget_msg_no_category">Das Kind hat keine Kategorien</string>
|
||||||
</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>
|
<string name="wiazrd_next">Weiter</string>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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
|
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.
|
||||||
|
@ -23,4 +23,7 @@
|
||||||
<color name="orange_text">#ff6f00</color>
|
<color name="orange_text">#ff6f00</color>
|
||||||
<color name="text_green">#4caf50</color>
|
<color name="text_green">#4caf50</color>
|
||||||
<color name="text_red">#f44</color>
|
<color name="text_red">#f44</color>
|
||||||
|
|
||||||
|
<color name="widgetBackground">#000000</color>
|
||||||
|
<color name="widgetText">#ffffff</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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
|
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.
|
||||||
|
@ -17,7 +17,8 @@
|
||||||
<color name="colorPrimary">#009688</color>
|
<color name="colorPrimary">#009688</color>
|
||||||
<color name="colorPrimaryDark">#00796B</color>
|
<color name="colorPrimaryDark">#00796B</color>
|
||||||
<color name="colorAccent">#1976d2</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="transparent">#00000000</color>
|
||||||
<color name="gray">#9E9E9E</color>
|
<color name="gray">#9E9E9E</color>
|
||||||
|
|
|
@ -821,6 +821,7 @@
|
||||||
<string name="manage_child_tab_other">Advanced settings</string>
|
<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">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_title">Categories</string>
|
||||||
<string name="manage_child_categories_intro_text">
|
<string name="manage_child_categories_intro_text">
|
||||||
|
@ -1656,10 +1657,13 @@
|
||||||
|
|
||||||
<string name="util_day_from_to">%s to %s</string>
|
<string name="util_day_from_to">%s to %s</string>
|
||||||
|
|
||||||
<string name="widget_empty_view">If this device is assigned to a child
|
<string name="widget_msg_unconfigured">This Widget only works on limited devices</string>
|
||||||
and this child has got categories, then the categories should
|
<string name="widget_msg_no_child">This device is not used by a child right now</string>
|
||||||
appear here.
|
<string name="widget_msg_no_category">This child does not have any categories</string>
|
||||||
</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>
|
<string name="wiazrd_next">Next</string>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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
|
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.
|
||||||
|
@ -19,4 +20,7 @@
|
||||||
android:updatePeriodMillis="0"
|
android:updatePeriodMillis="0"
|
||||||
android:resizeMode="horizontal|vertical"
|
android:resizeMode="horizontal|vertical"
|
||||||
android:previewImage="@drawable/widget_preview"
|
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" />
|
android:widgetCategory="home_screen" />
|
Loading…
Add table
Add a link
Reference in a new issue