"Updates" widget for Galaxy Z Flip5 cover screen (#9892)

This commit is contained in:
Ivan Iskandar 2023-09-02 20:37:25 +07:00 committed by GitHub
parent dbc7fe4d54
commit 816d7815e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 322 additions and 189 deletions

View file

@ -141,20 +141,6 @@
android:name=".data.notification.NotificationReceiver" android:name=".data.notification.NotificationReceiver"
android:exported="false" /> android:exported="false" />
<receiver
android:name="tachiyomi.presentation.widget.UpdatesGridGlanceReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
android:label="@string/label_recent_updates">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/updates_grid_glance_widget_info" />
</receiver>
<service <service
android:name=".data.download.DownloadService" android:name=".data.download.DownloadService"
android:exported="false" /> android:exported="false" />

View file

@ -1,2 +1,39 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest /> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<receiver
android:name="tachiyomi.presentation.widget.UpdatesGridGlanceReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
android:label="@string/label_recent_updates">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/updates_grid_homescreen_widget_info" />
</receiver>
<receiver
android:name="tachiyomi.presentation.widget.UpdatesGridCoverScreenGlanceReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
android:label="@string/label_recent_updates">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/updates_grid_lockscreen_widget_info" />
<meta-data
android:name="com.samsung.android.appwidget.provider"
android:resource="@xml/updates_grid_samsung_cover_widget_info" />
<meta-data
android:name="com.samsung.android.sdk.subscreen.widget.support_visibility_callback"
android:value="true" />
</receiver>
</application>
</manifest>

View file

@ -0,0 +1,153 @@
package tachiyomi.presentation.widget
import android.app.Application
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.Dp
import androidx.core.graphics.drawable.toBitmap
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.ImageProvider
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.glance.appwidget.SizeMode
import androidx.glance.appwidget.appWidgetBackground
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.unit.ColorProvider
import coil.executeBlocking
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.size.Precision
import coil.size.Scale
import coil.transform.RoundedCornersTransformation
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.system.dpToPx
import kotlinx.coroutines.flow.map
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.manga.model.MangaCover
import tachiyomi.domain.updates.interactor.GetUpdates
import tachiyomi.domain.updates.model.UpdatesWithRelations
import tachiyomi.presentation.widget.components.CoverHeight
import tachiyomi.presentation.widget.components.CoverWidth
import tachiyomi.presentation.widget.components.LockedWidget
import tachiyomi.presentation.widget.components.UpdatesWidget
import tachiyomi.presentation.widget.util.appWidgetBackgroundRadius
import tachiyomi.presentation.widget.util.calculateRowAndColumnCount
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Calendar
import java.util.Date
abstract class BaseUpdatesGridGlanceWidget(
private val context: Context = Injekt.get<Application>(),
private val getUpdates: GetUpdates = Injekt.get(),
private val preferences: SecurityPreferences = Injekt.get(),
) : GlanceAppWidget() {
override val sizeMode = SizeMode.Exact
abstract val foreground: ColorProvider
abstract val background: ImageProvider
abstract val topPadding: Dp
abstract val bottomPadding: Dp
override suspend fun provideGlance(context: Context, id: GlanceId) {
val locked = preferences.useAuthenticator().get()
val containerModifier = GlanceModifier
.fillMaxSize()
.background(background)
.appWidgetBackground()
.padding(top = topPadding, bottom = bottomPadding)
.appWidgetBackgroundRadius()
val manager = GlanceAppWidgetManager(context)
val ids = manager.getGlanceIds(javaClass)
val (rowCount, columnCount) = ids
.flatMap { manager.getAppWidgetSizes(it) }
.maxBy { it.height.value * it.width.value }
.calculateRowAndColumnCount(topPadding, bottomPadding)
provideContent {
// If app lock enabled, don't do anything
if (locked) {
LockedWidget(
foreground = foreground,
modifier = containerModifier,
)
return@provideContent
}
val flow = remember {
getUpdates
.subscribe(false, DateLimit.timeInMillis)
.map { rawData ->
rawData.prepareData(rowCount, columnCount)
}
}
val data by flow.collectAsState(initial = null)
UpdatesWidget(
data = data,
modifier = containerModifier,
contentColor = foreground,
topPadding = topPadding,
bottomPadding = bottomPadding,
)
}
}
private suspend fun List<UpdatesWithRelations>.prepareData(
rowCount: Int,
columnCount: Int,
): List<Pair<Long, Bitmap?>> {
// Resize to cover size
val widthPx = CoverWidth.value.toInt().dpToPx
val heightPx = CoverHeight.value.toInt().dpToPx
val roundPx = context.resources.getDimension(R.dimen.appwidget_inner_radius)
return withIOContext {
this@prepareData
.distinctBy { it.mangaId }
.take(rowCount * columnCount)
.map { updatesView ->
val request = ImageRequest.Builder(context)
.data(
MangaCover(
mangaId = updatesView.mangaId,
sourceId = updatesView.sourceId,
isMangaFavorite = true,
url = updatesView.coverData.url,
lastModified = updatesView.coverData.lastModified,
),
)
.memoryCachePolicy(CachePolicy.DISABLED)
.precision(Precision.EXACT)
.size(widthPx, heightPx)
.scale(Scale.FILL)
.let {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
it.transformations(RoundedCornersTransformation(roundPx))
} else {
it // Handled by system
}
}
.build()
Pair(updatesView.mangaId, context.imageLoader.executeBlocking(request).drawable?.toBitmap())
}
}
}
companion object {
val DateLimit: Calendar
get() = Calendar.getInstance().apply {
time = Date()
add(Calendar.MONTH, -3)
}
}
}

View file

@ -19,7 +19,7 @@ class TachiyomiWidgetManager(
fun Context.init(scope: LifecycleCoroutineScope) { fun Context.init(scope: LifecycleCoroutineScope) {
combine( combine(
getUpdates.subscribe(read = false, after = UpdatesGridGlanceWidget.DateLimit.timeInMillis), getUpdates.subscribe(read = false, after = BaseUpdatesGridGlanceWidget.DateLimit.timeInMillis),
securityPreferences.useAuthenticator().changes(), securityPreferences.useAuthenticator().changes(),
transform = { a, _ -> a }, transform = { a, _ -> a },
) )
@ -27,6 +27,7 @@ class TachiyomiWidgetManager(
.onEach { .onEach {
try { try {
UpdatesGridGlanceWidget().updateAll(this) UpdatesGridGlanceWidget().updateAll(this)
UpdatesGridCoverScreenGlanceWidget().updateAll(this)
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to update widget" } logcat(LogPriority.ERROR, e) { "Failed to update widget" }
} }

View file

@ -0,0 +1,9 @@
package tachiyomi.presentation.widget
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
class UpdatesGridCoverScreenGlanceReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget
get() = UpdatesGridCoverScreenGlanceWidget()
}

View file

@ -0,0 +1,13 @@
package tachiyomi.presentation.widget
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.glance.ImageProvider
import androidx.glance.unit.ColorProvider
class UpdatesGridCoverScreenGlanceWidget : BaseUpdatesGridGlanceWidget() {
override val foreground = ColorProvider(Color.White)
override val background = ImageProvider(R.drawable.appwidget_coverscreen_background)
override val topPadding = 0.dp
override val bottomPadding = 24.dp
}

View file

@ -4,5 +4,6 @@ import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver import androidx.glance.appwidget.GlanceAppWidgetReceiver
class UpdatesGridGlanceReceiver : GlanceAppWidgetReceiver() { class UpdatesGridGlanceReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = UpdatesGridGlanceWidget() override val glanceAppWidget: GlanceAppWidget
get() = UpdatesGridGlanceWidget()
} }

View file

@ -1,133 +1,12 @@
package tachiyomi.presentation.widget package tachiyomi.presentation.widget
import android.app.Application import androidx.compose.ui.unit.dp
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import androidx.core.graphics.drawable.toBitmap
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.ImageProvider import androidx.glance.ImageProvider
import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.unit.ColorProvider
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.glance.appwidget.SizeMode
import androidx.glance.appwidget.appWidgetBackground
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.layout.fillMaxSize
import coil.executeBlocking
import coil.imageLoader
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.size.Precision
import coil.size.Scale
import coil.transform.RoundedCornersTransformation
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.system.dpToPx
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.manga.model.MangaCover
import tachiyomi.domain.updates.interactor.GetUpdates
import tachiyomi.domain.updates.model.UpdatesWithRelations
import tachiyomi.presentation.widget.components.CoverHeight
import tachiyomi.presentation.widget.components.CoverWidth
import tachiyomi.presentation.widget.components.LockedWidget
import tachiyomi.presentation.widget.components.UpdatesWidget
import tachiyomi.presentation.widget.util.appWidgetBackgroundRadius
import tachiyomi.presentation.widget.util.calculateRowAndColumnCount
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Calendar
import java.util.Date
class UpdatesGridGlanceWidget( class UpdatesGridGlanceWidget : BaseUpdatesGridGlanceWidget() {
private val context: Context = Injekt.get<Application>(), override val foreground = ColorProvider(R.color.appwidget_on_secondary_container)
private val getUpdates: GetUpdates = Injekt.get(), override val background = ImageProvider(R.drawable.appwidget_background)
private val preferences: SecurityPreferences = Injekt.get(), override val topPadding = 0.dp
) : GlanceAppWidget() { override val bottomPadding = 0.dp
private var data: List<Pair<Long, Bitmap?>>? = null
override val sizeMode = SizeMode.Exact
override suspend fun provideGlance(context: Context, id: GlanceId) {
val locked = preferences.useAuthenticator().get()
if (!locked) loadData()
provideContent {
// If app lock enabled, don't do anything
if (locked) {
LockedWidget()
return@provideContent
} }
UpdatesWidget(data)
}
}
private suspend fun loadData() {
val manager = GlanceAppWidgetManager(context)
val ids = manager.getGlanceIds(this@UpdatesGridGlanceWidget::class.java)
if (ids.isEmpty()) return
withIOContext {
val updates = getUpdates.await(
read = false,
after = DateLimit.timeInMillis,
)
val (rowCount, columnCount) = ids
.flatMap { manager.getAppWidgetSizes(it) }
.maxBy { it.height.value * it.width.value }
.calculateRowAndColumnCount()
data = prepareList(updates, rowCount * columnCount)
}
}
private fun prepareList(processList: List<UpdatesWithRelations>, take: Int): List<Pair<Long, Bitmap?>> {
// Resize to cover size
val widthPx = CoverWidth.value.toInt().dpToPx
val heightPx = CoverHeight.value.toInt().dpToPx
val roundPx = context.resources.getDimension(R.dimen.appwidget_inner_radius)
return processList
.distinctBy { it.mangaId }
.take(take)
.map { updatesView ->
val request = ImageRequest.Builder(context)
.data(
MangaCover(
mangaId = updatesView.mangaId,
sourceId = updatesView.sourceId,
isMangaFavorite = true,
url = updatesView.coverData.url,
lastModified = updatesView.coverData.lastModified,
),
)
.memoryCachePolicy(CachePolicy.DISABLED)
.precision(Precision.EXACT)
.size(widthPx, heightPx)
.scale(Scale.FILL)
.let {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
it.transformations(RoundedCornersTransformation(roundPx))
} else {
it // Handled by system
}
}
.build()
Pair(updatesView.mangaId, context.imageLoader.executeBlocking(request).drawable?.toBitmap())
}
}
companion object {
val DateLimit: Calendar
get() = Calendar.getInstance().apply {
time = Date()
add(Calendar.MONTH, -3)
}
}
}
val ContainerModifier = GlanceModifier
.fillMaxSize()
.background(ImageProvider(R.drawable.appwidget_background))
.appWidgetBackground()
.appWidgetBackgroundRadius()

View file

@ -16,26 +16,27 @@ import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider import androidx.glance.unit.ColorProvider
import tachiyomi.core.Constants import tachiyomi.core.Constants
import tachiyomi.presentation.widget.ContainerModifier
import tachiyomi.presentation.widget.R import tachiyomi.presentation.widget.R
import tachiyomi.presentation.widget.util.stringResource import tachiyomi.presentation.widget.util.stringResource
@Composable @Composable
fun LockedWidget() { fun LockedWidget(
foreground: ColorProvider,
modifier: GlanceModifier = GlanceModifier,
) {
val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply { val intent = Intent(LocalContext.current, Class.forName(Constants.MAIN_ACTIVITY)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
} }
Box( Box(
modifier = GlanceModifier modifier = modifier
.clickable(actionStartActivity(intent)) .clickable(actionStartActivity(intent))
.then(ContainerModifier)
.padding(8.dp), .padding(8.dp),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Text( Text(
text = stringResource(R.string.appwidget_unavailable_locked), text = stringResource(R.string.appwidget_unavailable_locked),
style = TextStyle( style = TextStyle(
color = ColorProvider(R.color.appwidget_on_secondary_container), color = foreground,
fontSize = 12.sp, fontSize = 12.sp,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
), ),

View file

@ -3,6 +3,7 @@ package tachiyomi.presentation.widget.components
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.glance.GlanceModifier import androidx.glance.GlanceModifier
import androidx.glance.LocalContext import androidx.glance.LocalContext
@ -14,28 +15,43 @@ import androidx.glance.layout.Alignment
import androidx.glance.layout.Box import androidx.glance.layout.Box
import androidx.glance.layout.Column import androidx.glance.layout.Column
import androidx.glance.layout.Row import androidx.glance.layout.Row
import androidx.glance.layout.fillMaxHeight
import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.padding import androidx.glance.layout.padding
import androidx.glance.text.Text import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
import tachiyomi.core.Constants import tachiyomi.core.Constants
import tachiyomi.presentation.widget.ContainerModifier
import tachiyomi.presentation.widget.R import tachiyomi.presentation.widget.R
import tachiyomi.presentation.widget.util.calculateRowAndColumnCount import tachiyomi.presentation.widget.util.calculateRowAndColumnCount
import tachiyomi.presentation.widget.util.stringResource import tachiyomi.presentation.widget.util.stringResource
@Composable @Composable
fun UpdatesWidget(data: List<Pair<Long, Bitmap?>>?) { fun UpdatesWidget(
val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount() data: List<Pair<Long, Bitmap?>>?,
modifier: GlanceModifier = GlanceModifier,
contentColor: ColorProvider,
topPadding: Dp,
bottomPadding: Dp,
) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier,
) {
if (data == null) {
CircularProgressIndicator(color = contentColor)
} else if (data.isEmpty()) {
Text(
text = stringResource(R.string.information_no_recent),
style = TextStyle(color = contentColor),
)
} else {
val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount(topPadding, bottomPadding)
Column( Column(
modifier = ContainerModifier, modifier = GlanceModifier.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
if (data == null) {
CircularProgressIndicator()
} else if (data.isEmpty()) {
Text(text = stringResource(R.string.information_no_recent))
} else {
(0..<rowCount).forEach { i -> (0..<rowCount).forEach { i ->
val coverRow = (0..<columnCount).mapNotNull { j -> val coverRow = (0..<columnCount).mapNotNull { j ->
data.getOrNull(j + (i * columnCount)) data.getOrNull(j + (i * columnCount))
@ -75,3 +91,4 @@ fun UpdatesWidget(data: List<Pair<Long, Bitmap?>>?) {
} }
} }
} }
}

View file

@ -2,6 +2,7 @@ package tachiyomi.presentation.widget.util
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.glance.GlanceModifier import androidx.glance.GlanceModifier
import androidx.glance.LocalContext import androidx.glance.LocalContext
@ -34,9 +35,13 @@ fun stringResource(@StringRes id: Int): String {
* *
* @return pair of row and column count * @return pair of row and column count
*/ */
fun DpSize.calculateRowAndColumnCount(): Pair<Int, Int> { fun DpSize.calculateRowAndColumnCount(
topPadding: Dp,
bottomPadding: Dp,
): Pair<Int, Int> {
// Hack: Size provided by Glance manager is not reliable so take at least 1 row and 1 column // Hack: Size provided by Glance manager is not reliable so take at least 1 row and 1 column
// Set max to 10 children each direction because of Glance limitation // Set max to 10 children each direction because of Glance limitation
val height = this.height - topPadding - bottomPadding
val rowCount = (height.value / 95).toInt().coerceIn(1, 10) val rowCount = (height.value / 95).toInt().coerceIn(1, 10)
val columnCount = (width.value / 64).toInt().coerceIn(1, 10) val columnCount = (width.value / 64).toInt().coerceIn(1, 10)
return Pair(rowCount, columnCount) return Pair(rowCount, columnCount)

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/appwidget_coverscreen_background" />
</shape>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/appwidget_coverscreen_background">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/loading"
android:textColor="@android:color/white" />
</FrameLayout>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="appwidget_background">@color/tachiyomi_surface</color> <color name="appwidget_background">@color/tachiyomi_surface</color>
<color name="appwidget_coverscreen_background">#00000000</color>
<color name="appwidget_on_background">@color/tachiyomi_onSurface</color> <color name="appwidget_on_background">@color/tachiyomi_onSurface</color>
<color name="appwidget_surface_variant">@color/tachiyomi_surfaceVariant</color> <color name="appwidget_surface_variant">@color/tachiyomi_surfaceVariant</color>
<color name="appwidget_on_surface_variant">@color/tachiyomi_onSurfaceVariant</color> <color name="appwidget_on_surface_variant">@color/tachiyomi_onSurfaceVariant</color>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/appwidget_updates_description"
android:previewImage="@drawable/updates_grid_coverscreen_widget_preview"
android:initialLayout="@layout/appwidget_coverscreen_loading"
android:resizeMode="horizontal|vertical"
android:widgetCategory="keyguard" />

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<samsung-appwidget-provider
display="sub_screen"
privacyWidget="true" />