Add optional translucency to the Widget

This commit is contained in:
Jonas Lochmann 2022-09-12 02:00:00 +02:00
parent 435a29c958
commit a7e2131638
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
17 changed files with 2133 additions and 27 deletions

File diff suppressed because it is too large Load diff

View file

@ -47,6 +47,7 @@ interface Database {
fun deviceKey(): DeviceKeyDao
fun u2f(): U2FDao
fun widgetCategory(): WidgetCategoryDao
fun widgetConfig(): WidgetConfigDao
fun <T> runInTransaction(block: () -> T): T
fun <T> runInUnobservedTransaction(block: () -> T): T

View file

@ -333,6 +333,12 @@ object DatabaseMigrations {
}
}
val MIGRATE_TO_V46 = object: Migration(45, 46) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `widget_config` (`widget_id` INTEGER NOT NULL, `translucent` INTEGER NOT NULL, PRIMARY KEY(`widget_id`))")
}
}
val ALL = arrayOf(
MIGRATE_TO_V2,
MIGRATE_TO_V3,
@ -377,6 +383,7 @@ object DatabaseMigrations {
MIGRATE_TO_V42,
MIGRATE_TP_V43,
MIGRATE_TO_V44,
MIGRATE_TO_V45
MIGRATE_TO_V45,
MIGRATE_TO_V46
)
}

View file

@ -59,8 +59,9 @@ import java.util.concurrent.TimeUnit
CryptContainerKeyResult::class,
DevicePublicKey::class,
UserU2FKey::class,
WidgetCategory::class
], version = 45)
WidgetCategory::class,
WidgetConfig::class
], version = 46)
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
companion object {
private val lock = Object()

View file

@ -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.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import io.timelimit.android.data.model.WidgetConfig
@Dao
interface WidgetConfigDao {
@Query("SELECT * FROM widget_config")
fun queryAll(): List<WidgetConfig>
@Query("SELECT * FROM widget_config WHERE widget_id = :widgetId")
fun queryByWidgetId(widgetId: Int): WidgetConfig?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun upsert(config: WidgetConfig)
@Query("DELETE FROM widget_config WHERE widget_id IN (:widgetIds)")
fun deleteByWidgetIds(widgetIds: IntArray)
@Query("DELETE FROM widget_config")
fun deleteAll()
}

View file

@ -0,0 +1,30 @@
/*
* 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.PrimaryKey
@Entity(
tableName = "widget_config"
)
data class WidgetConfig (
@ColumnInfo(name = "widget_id")
@PrimaryKey
val widgetId: Int,
val translucent: Boolean
)

View file

@ -26,6 +26,8 @@ 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.coroutines.executeAndWait
import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.integration.platform.android.BackgroundActionService
import io.timelimit.android.logic.DefaultAppLogic
@ -34,24 +36,48 @@ class TimesWidgetProvider: AppWidgetProvider() {
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)
runAsync {
val configs = Threads.database.executeAndWait {
try {
DefaultAppLogic.with(context).database.widgetConfig()
.queryAll()
.associateBy { it.widgetId }
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "could not query database", ex)
}
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)
emptyMap()
}
}
appWidgetManager.updateAppWidget(appWidgetId, views)
for (appWidgetId in appWidgetIds) {
val config = configs[appWidgetId]
val translucent = config?.translucent ?: false
val views = RemoteViews(
context.packageName,
if (translucent) R.layout.widget_times_translucent
else R.layout.widget_times
)
views.setRemoteAdapter(android.R.id.list, TimesWidgetService.intent(context, appWidgetId, translucent))
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)
}
TimesWidgetService.notifyContentChanges(context)
}
fun triggerUpdates(context: Context) {
fun triggerUpdates(context: Context, appWidgetIds: IntArray? = null) {
context.getSystemService<AppWidgetManager>()?.also { appWidgetManager ->
val appWidgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, TimesWidgetProvider::class.java))
val usedAppWidgetIds = appWidgetIds
?: appWidgetManager.getAppWidgetIds(ComponentName(context, TimesWidgetProvider::class.java))
handleUpdate(context, appWidgetManager, appWidgetIds)
handleUpdate(context, appWidgetManager, usedAppWidgetIds)
}
}
}
@ -76,7 +102,10 @@ class TimesWidgetProvider: AppWidgetProvider() {
Threads.database.execute {
try {
database.widgetCategory().deleteByWidgetIds(appWidgetIds)
database.runInTransaction {
database.widgetCategory().deleteByWidgetIds(appWidgetIds)
database.widgetConfig().deleteByWidgetIds(appWidgetIds)
}
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "onDisabled", ex)
@ -92,7 +121,10 @@ class TimesWidgetProvider: AppWidgetProvider() {
Threads.database.execute {
try {
database.widgetCategory().deleteAll()
database.runInTransaction {
database.widgetCategory().deleteAll()
database.widgetConfig().deleteAll()
}
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "onDisabled", ex)

View file

@ -36,10 +36,12 @@ import io.timelimit.android.ui.manage.child.category.CategoryItemLeftPadding
class TimesWidgetService: RemoteViewsService() {
companion object {
private const val EXTRA_APP_WIDGET_ID = "appWidgetId"
private const val EXTRA_TRANSLUCENT = "translucent"
fun intent(context: Context, appWidgetId: Int) = Intent(context, TimesWidgetService::class.java)
.setData(Uri.parse("widget:$appWidgetId"))
fun intent(context: Context, appWidgetId: Int, translucent: Boolean) = Intent(context, TimesWidgetService::class.java)
.setData(Uri.parse("widget:$appWidgetId:$translucent"))
.putExtra(EXTRA_APP_WIDGET_ID, appWidgetId)
.putExtra(EXTRA_TRANSLUCENT, translucent)
fun notifyContentChanges(context: Context) {
context.getSystemService<AppWidgetManager>()?.also { appWidgetManager ->
@ -68,7 +70,7 @@ class TimesWidgetService: RemoteViewsService() {
notifyContentChanges(this)
}
private fun createFactory(appWidgetId: Int) = object : RemoteViewsFactory {
private fun createFactory(appWidgetId: Int, translucent: Boolean) = object : RemoteViewsFactory {
private var currentItems: List<TimesWidgetItem> = emptyList()
init { onDataSetChanged() }
@ -98,11 +100,14 @@ class TimesWidgetService: RemoteViewsService() {
override fun getCount(): Int = currentItems.size
override fun getViewAt(position: Int): RemoteViews {
val categoryItemView = if (translucent) R.layout.widget_times_category_item_translucent
else R.layout.widget_times_category_item
if (position >= currentItems.size) {
return RemoteViews(packageName, R.layout.widget_times_category_item)
return RemoteViews(packageName, categoryItemView)
}
fun createCategoryItem(title: String?, subtitle: String, paddingLeft: Int) = RemoteViews(packageName, R.layout.widget_times_category_item).also { result ->
fun createCategoryItem(title: String?, subtitle: String, paddingLeft: Int) = RemoteViews(packageName, categoryItemView).also { result ->
result.setTextViewText(R.id.title, title ?: "")
result.setTextViewText(R.id.subtitle, subtitle)
@ -161,6 +166,7 @@ class TimesWidgetService: RemoteViewsService() {
}
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = createFactory(
intent.getIntExtra(EXTRA_APP_WIDGET_ID, 0)
intent.getIntExtra(EXTRA_APP_WIDGET_ID, 0),
intent.getBooleanExtra(EXTRA_TRANSLUCENT, false)
)
}

View file

@ -56,6 +56,11 @@ class WidgetConfigActivity: FragmentActivity() {
WidgetConfigFilterDialogFragment().showSafe(supportFragmentManager, WidgetConfigFilterDialogFragment.DIALOG_TAG)
}
}
is WidgetConfigModel.State.ShowOtherOptions -> {
if (supportFragmentManager.findFragmentByTag(WidgetConfigOtherDialogFragment.DIALOG_TAG) == null) {
WidgetConfigOtherDialogFragment().showSafe(supportFragmentManager, WidgetConfigOtherDialogFragment.DIALOG_TAG)
}
}
is WidgetConfigModel.State.Done -> {
setResult(
RESULT_OK,

View file

@ -27,8 +27,10 @@ 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.data.model.WidgetConfig
import io.timelimit.android.livedata.castDown
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.widget.TimesWidgetProvider
import kotlinx.coroutines.launch
class WidgetConfigModel(application: Application): AndroidViewModel(application) {
@ -83,11 +85,16 @@ class WidgetConfigModel(application: Application): AndroidViewModel(application)
viewModelScope.launch {
try {
Threads.database.executeAndWait {
val currentConfig = Threads.database.executeAndWait {
database.widgetCategory().deleteByWidgetId(oldState.appWidgetId)
database.widgetConfig().queryByWidgetId(oldState.appWidgetId)
}
stateInternal.value = State.Done(appWidgetId = oldState.appWidgetId)
stateInternal.value = State.ShowOtherOptions(
appWidgetId = oldState.appWidgetId,
translucent = currentConfig?.translucent ?: false
)
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "selectModeAll", ex)
@ -117,7 +124,7 @@ class WidgetConfigModel(application: Application): AndroidViewModel(application)
viewModelScope.launch {
try {
Threads.database.executeAndWait {
val currentConfig = Threads.database.executeAndWait {
val userAndDeviceRelatedData = database.derivedDataDao().getUserAndDeviceRelatedDataSync()
val currentCategoryIds = userAndDeviceRelatedData!!.userRelatedData!!.categoryById.keys
@ -135,9 +142,14 @@ class WidgetConfigModel(application: Application): AndroidViewModel(application)
categoriesToAdd.toList().map { WidgetCategory(oldState.appWidgetId, it) }
)
}
database.widgetConfig().queryByWidgetId(oldState.appWidgetId)
}
stateInternal.value = State.Done(appWidgetId = oldState.appWidgetId)
stateInternal.value = State.ShowOtherOptions(
appWidgetId = oldState.appWidgetId,
translucent = currentConfig?.translucent ?: false
)
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "selectModeAll", ex)
@ -148,6 +160,38 @@ class WidgetConfigModel(application: Application): AndroidViewModel(application)
}
}
fun selectOtherOptions(translucent: Boolean) {
val oldState = state.value
if (!(oldState is State.ShowOtherOptions)) return
stateInternal.value = State.Working
viewModelScope.launch {
try {
Threads.database.executeAndWait {
database.widgetConfig().upsert(
WidgetConfig(
widgetId = oldState.appWidgetId,
translucent = translucent
)
)
}
TimesWidgetProvider.triggerUpdates(
context = getApplication(),
appWidgetIds = intArrayOf(oldState.appWidgetId)
)
stateInternal.value = State.Done(oldState.appWidgetId)
} catch (ex: Exception) {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "selectOtherOptions", ex)
}
stateInternal.value = State.ErrorCancel
}
}
}
fun userCancel() {
stateInternal.value = State.UserCancel
}
@ -157,6 +201,7 @@ class WidgetConfigModel(application: Application): AndroidViewModel(application)
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 ShowOtherOptions(val appWidgetId: Int, val translucent: Boolean): State()
data class Done(val appWidgetId: Int): State()
object Unconfigured: State()
object UserCancel: State()

View file

@ -0,0 +1,79 @@
/*
* 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 WidgetConfigOtherDialogFragment: DialogFragment() {
companion object {
private const val STATE_TRANSLUCENT = "translucent"
const val DIALOG_TAG = "WidgetConfigOtherDialogFragment"
}
private val model: WidgetConfigModel by activityViewModels()
private var translucent = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
model.state.value?.also {
if (it is WidgetConfigModel.State.ShowOtherOptions) {
translucent = it.translucent
}
}
savedInstanceState?.also { translucent = it.getBoolean(STATE_TRANSLUCENT) }
model.state.observe(this) {
if (!(it is WidgetConfigModel.State.ShowOtherOptions)) dismissAllowingStateLoss()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(STATE_TRANSLUCENT, translucent)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = AlertDialog.Builder(requireContext(), theme)
.setMultiChoiceItems(
arrayOf(
getString(R.string.widget_config_other_translucent)
),
booleanArrayOf(
translucent
)
) { _, _, checked ->
translucent = checked
}
.setPositiveButton(R.string.wiazrd_next) { _, _ ->
model.selectOtherOptions(translucent)
}
.create()
override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)
model.userCancel()
}
}

View file

@ -16,7 +16,7 @@
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:background="@color/widget_background"
tools:background="@color/widgetBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="4dp"

View file

@ -0,0 +1,60 @@
<?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/widgetBackgroundTranslucent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<LinearLayout
android:id="@+id/widgetInnerContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:id="@+id/topPadding"
android:layout_width="match_parent"
android:layout_height="12dp" />
<TextView
tools:text="3 m"
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:textAppearanceLarge"
android:textStyle="bold"
android:textColor="@color/widgetTextTranslucent"/>
<TextView
tools:text="Erlaubte Apps"
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:textAppearanceMedium"
android:textColor="@color/widgetTextTranslucent"/>
<FrameLayout
android:id="@+id/bottomPadding"
android:layout_width="match_parent"
android:layout_height="12dp" />
</LinearLayout>
</FrameLayout>

View file

@ -0,0 +1,36 @@
<!--
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/widgetBackgroundTranslucent">
<ListView
android:divider="@color/transparent"
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:listSelector="@android:color/transparent"/>
<ProgressBar
android:id="@android:id/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:attr/progressBarStyleLarge"
android:layout_gravity="center" />
</FrameLayout>

View file

@ -1618,6 +1618,7 @@
<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="widget_config_other_translucent">Transparenz aktivieren</string>
<string name="wiazrd_next">Weiter</string>

View file

@ -18,7 +18,9 @@
<color name="colorPrimaryDark">#00796B</color>
<color name="colorAccent">#1976d2</color>
<color name="widgetBackground">#ffffff</color>
<color name="widgetBackgroundTranslucent">#33000000</color>
<color name="widgetText">#000000</color>
<color name="widgetTextTranslucent">#ffffff</color>
<color name="transparent">#00000000</color>
<color name="gray">#9E9E9E</color>

View file

@ -1666,6 +1666,7 @@
<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="widget_config_other_translucent">Enable translucency</string>
<string name="wiazrd_next">Next</string>