diff --git a/app/src/main/java/com/google/android/samples/socialite/SocialApp.kt b/app/src/main/java/com/google/android/samples/socialite/SocialApp.kt index f9ad806e..af0789c8 100644 --- a/app/src/main/java/com/google/android/samples/socialite/SocialApp.kt +++ b/app/src/main/java/com/google/android/samples/socialite/SocialApp.kt @@ -16,7 +16,7 @@ package com.google.android.samples.socialite import android.app.Application -import com.google.android.samples.socialite.widget.SocialiteWidget +import com.google.android.samples.socialite.widget.SociaLiteAppWidget import com.google.android.samples.socialite.widget.model.WidgetModelRepository import dagger.hilt.android.HiltAndroidApp import javax.inject.Inject @@ -24,9 +24,8 @@ import javax.inject.Inject @HiltAndroidApp class SocialApp : Application() { - @Inject lateinit var repository: WidgetModelRepository override fun onCreate() { super.onCreate() - SocialiteWidget.update(this@SocialApp, repository) + SociaLiteAppWidget.update(this@SocialApp) } } diff --git a/app/src/main/java/com/google/android/samples/socialite/data/AppDatabase.kt b/app/src/main/java/com/google/android/samples/socialite/data/AppDatabase.kt index 30ca9f0f..e74f55d9 100644 --- a/app/src/main/java/com/google/android/samples/socialite/data/AppDatabase.kt +++ b/app/src/main/java/com/google/android/samples/socialite/data/AppDatabase.kt @@ -37,7 +37,7 @@ import com.google.android.samples.socialite.widget.model.WidgetModelDao WidgetModel::class, ], views = [ChatWithLastMessage::class], - version = 2, + version = 1, ) abstract class AppDatabase : RoomDatabase() { abstract fun contactDao(): ContactDao diff --git a/app/src/main/java/com/google/android/samples/socialite/di/DatabaseModule.kt b/app/src/main/java/com/google/android/samples/socialite/di/DatabaseModule.kt index 15932ea8..8353a546 100644 --- a/app/src/main/java/com/google/android/samples/socialite/di/DatabaseModule.kt +++ b/app/src/main/java/com/google/android/samples/socialite/di/DatabaseModule.kt @@ -71,9 +71,6 @@ object DatabaseModule { @Provides fun providesWidgetModelDao(database: AppDatabase): WidgetModelDao = database.widgetDao() - @Provides - fun provideWidgetModelRepository(widgetModelDao: WidgetModelDao, @ApplicationContext appContext: Context) = WidgetModelRepository(widgetModelDao, providesApplicationCoroutineScope(), appContext) - @Provides @Singleton @AppCoroutineScope diff --git a/app/src/main/java/com/google/android/samples/socialite/widget/SociaLiteAppWidget.kt b/app/src/main/java/com/google/android/samples/socialite/widget/SociaLiteAppWidget.kt new file mode 100644 index 00000000..43bbfb61 --- /dev/null +++ b/app/src/main/java/com/google/android/samples/socialite/widget/SociaLiteAppWidget.kt @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.samples.socialite.widget + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.util.Log +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.graphics.drawable.toBitmapOrNull +import androidx.core.net.toUri +import androidx.glance.GlanceId +import androidx.glance.GlanceModifier +import androidx.glance.GlanceTheme +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.LocalContext +import androidx.glance.action.clickable +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.ImageProvider +import androidx.glance.appwidget.SizeMode +import androidx.glance.appwidget.action.actionStartActivity +import androidx.glance.appwidget.components.Scaffold +import androidx.glance.appwidget.components.TitleBar +import androidx.glance.appwidget.lazy.LazyColumn +import androidx.glance.appwidget.provideContent +import androidx.glance.appwidget.updateAll +import androidx.glance.layout.Alignment +import androidx.glance.layout.Box +import androidx.glance.layout.Column +import androidx.glance.layout.ContentScale +import androidx.glance.layout.Row +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.padding +import androidx.glance.layout.size +import androidx.glance.layout.wrapContentHeight +import androidx.glance.text.FontFamily +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import coil.Coil +import coil.request.CachePolicy +import coil.request.ErrorResult +import coil.request.ImageRequest +import coil.request.SuccessResult +import coil.transform.CircleCropTransformation +import com.google.android.samples.socialite.MainActivity +import com.google.android.samples.socialite.R +import com.google.android.samples.socialite.SocialApp +import com.google.android.samples.socialite.model.Contact +import com.google.android.samples.socialite.widget.model.WidgetModel +import com.google.android.samples.socialite.widget.model.WidgetModelRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +class SociaLiteAppWidget : GlanceAppWidget() { + + override val sizeMode: SizeMode + get() = SizeMode.Exact + + override suspend fun provideGlance(context: Context, glanceId: GlanceId) { + + val glanceAppWidgetManager = GlanceAppWidgetManager(context) + val widgetId = glanceAppWidgetManager.getAppWidgetId(glanceId) + Log.i("SOCIAL", "$widgetId") + provideContent { + Content(widgetId, context) + } + } + + companion object { + fun update(context: Context) { + CoroutineScope(Dispatchers.Default).launch { + SociaLiteAppWidget().updateAll(context) + } + } + } + + @Composable + private fun Content(widgetId: Int, context: Context) { + val repository = WidgetModelRepository.get(context) + val model = repository.loadModel(widgetId).collectAsState(null).value + GlanceTheme { + Scaffold( + titleBar = { + TitleBar( + textColor = GlanceTheme.colors.onSurface, + startIcon = ImageProvider(R.mipmap.ic_launcher), + title = "SociaLite", + ) + }, + backgroundColor = GlanceTheme.colors.widgetBackground, + modifier = GlanceModifier.fillMaxSize(), + ) { + when (model) { + null -> ZeroState(repository, widgetId, context) + else -> FavoriteContact(model) + } + } + } + } + + @Composable + private fun FavoriteContact(model: WidgetModel) { + val appContext = LocalContext.current.applicationContext + Box( + modifier = GlanceModifier.fillMaxSize().then( + + if (model.unreadMessages) { + GlanceModifier.clickable( + actionStartActivity( + Intent(appContext, MainActivity::class.java) + .setAction(Intent.ACTION_VIEW) + .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + .setData("https://socialite.google.com/chat/${model.contactId}".toUri()), + ), + ) + } else { + GlanceModifier + }, + ), + contentAlignment = Alignment.Center, + ) { + Column { + Image( + provider = ImageProvider(model.photo.toUri()), + contentDescription = model.displayName, + ) + Text( + text = model.displayName, + style = TextStyle( + fontWeight = FontWeight.Bold, + color = GlanceTheme.colors.onBackground, + ), + ) + Text( + text = if (model.unreadMessages) "New Message!" else "No messages", + style = TextStyle(color = GlanceTheme.colors.onBackground), + ) + } + } + } + + @Composable + private fun ZeroState(repository: WidgetModelRepository, widgetId: Int, context: Context) { + Box(modifier = GlanceModifier.fillMaxSize(), contentAlignment = Alignment.TopStart) { + LazyColumn { + items(Contact.CONTACTS.size) { contactIndex -> + val contact = Contact.CONTACTS[contactIndex] + val profileImage = remember(contact.iconUri) { mutableStateOf(null) } + + LaunchedEffect(contact.iconUri) { + profileImage.value = getImage(contact.iconUri, false, context) + } + + Row( + modifier = GlanceModifier.fillMaxWidth().clickable( + { + runBlocking { + repository.create( + WidgetModel( + widgetId, + contact.id, + contact.name, + contact.iconUri.toString(), + false, + ), + ) + } + }, + ).wrapContentHeight().padding(4.dp), + verticalAlignment = Alignment.Vertical.CenterVertically, + ) { + Image( + modifier = GlanceModifier.size(48.dp).padding(end = 8.dp), + contentScale = ContentScale.Fit, + provider = if (profileImage.value == null) { + ImageProvider(R.drawable.ic_launcher_background) + } else { + ImageProvider( + profileImage.value!!, + ) + }, + contentDescription = "Avatar", + + ) + Column( + modifier = GlanceModifier.defaultWeight(), + horizontalAlignment = Alignment.Horizontal.Start, + ) { + Text( + text = contact.name, + style = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + color = GlanceTheme.colors.onBackground, + ), + ) + Text( + text = "Click to select as favorite contact", + style = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + fontFamily = FontFamily.Monospace, + color = GlanceTheme.colors.onBackground, + ), + ) + } + } + } + } + } + } + + private suspend fun getImage(url: Uri, force: Boolean = false, context: Context): Bitmap? { + val request = + ImageRequest.Builder(context).transformations(CircleCropTransformation()).data(url) + .apply { + if (force) { + memoryCachePolicy(CachePolicy.DISABLED) + diskCachePolicy(CachePolicy.DISABLED) + } + }.build() + + // Request the image to be loaded and throw error if it failed + return when (val result = Coil.imageLoader(context).execute(request)) { + is ErrorResult -> { + Log.e("GLANCE", "Error " + result.throwable.message) + throw result.throwable + } + + is SuccessResult -> result.drawable.toBitmapOrNull() + } + } +} diff --git a/app/src/main/java/com/google/android/samples/socialite/widget/SocialiteWidgetReceiver.kt b/app/src/main/java/com/google/android/samples/socialite/widget/SocialiteWidgetReceiver.kt index 4e6df79d..5f228a4d 100644 --- a/app/src/main/java/com/google/android/samples/socialite/widget/SocialiteWidgetReceiver.kt +++ b/app/src/main/java/com/google/android/samples/socialite/widget/SocialiteWidgetReceiver.kt @@ -16,258 +16,20 @@ package com.google.android.samples.socialite.widget import android.content.Context -import android.content.Intent -import android.graphics.Bitmap -import android.net.Uri -import android.util.Log -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.core.graphics.drawable.toBitmapOrNull -import androidx.core.net.toUri -import androidx.glance.GlanceId -import androidx.glance.GlanceModifier -import androidx.glance.GlanceTheme -import androidx.glance.Image -import androidx.glance.ImageProvider -import androidx.glance.LocalContext -import androidx.glance.action.clickable import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.GlanceAppWidgetReceiver -import androidx.glance.appwidget.ImageProvider -import androidx.glance.appwidget.SizeMode -import androidx.glance.appwidget.action.actionStartActivity -import androidx.glance.appwidget.components.Scaffold -import androidx.glance.appwidget.components.TitleBar -import androidx.glance.appwidget.lazy.LazyColumn -import androidx.glance.appwidget.provideContent -import androidx.glance.appwidget.updateAll -import androidx.glance.layout.Alignment -import androidx.glance.layout.Box -import androidx.glance.layout.Column -import androidx.glance.layout.ContentScale -import androidx.glance.layout.Row -import androidx.glance.layout.fillMaxSize -import androidx.glance.layout.fillMaxWidth -import androidx.glance.layout.padding -import androidx.glance.layout.size -import androidx.glance.layout.wrapContentHeight -import androidx.glance.text.FontFamily -import androidx.glance.text.FontWeight -import androidx.glance.text.Text -import androidx.glance.text.TextStyle -import coil.Coil -import coil.request.CachePolicy -import coil.request.ErrorResult -import coil.request.ImageRequest -import coil.request.SuccessResult -import coil.transform.CircleCropTransformation -import com.google.android.samples.socialite.MainActivity -import com.google.android.samples.socialite.R import com.google.android.samples.socialite.SocialApp -import com.google.android.samples.socialite.model.Contact -import com.google.android.samples.socialite.widget.model.WidgetModel import com.google.android.samples.socialite.widget.model.WidgetModelRepository -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking class SocialiteWidgetReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget - get() = SocialiteWidget() + get() = SociaLiteAppWidget() override fun onDeleted(context: Context, appWidgetIds: IntArray) { super.onDeleted(context, appWidgetIds) - val repository = (context.applicationContext as SocialApp).repository + val repository = WidgetModelRepository.get(context) repository.delete(appWidgetIds) } } -class SocialiteWidget : GlanceAppWidget() { - - override val sizeMode: SizeMode - get() = SizeMode.Exact - - override suspend fun provideGlance(context: Context, glanceId: GlanceId) { - val repository = (context.applicationContext as SocialApp).repository - val glanceAppWidgetManager = GlanceAppWidgetManager(context) - val widgetId = glanceAppWidgetManager.getAppWidgetId(glanceId) - Log.i("SOCIAL", "$widgetId") - provideContent { - Content(widgetId, repository, context) - } - } - - companion object { - fun update(context: Context, repository: WidgetModelRepository) { - CoroutineScope(Dispatchers.Default).launch { - SocialiteWidget().updateAll(context) - } - } - } - - @Composable - private fun Content(widgetId: Int, repository: WidgetModelRepository, context: Context) { - val model = repository.loadModel(widgetId).collectAsState(null).value - GlanceTheme { - Scaffold( - titleBar = { - TitleBar( - textColor = GlanceTheme.colors.onSurface, - startIcon = ImageProvider(R.mipmap.ic_launcher), - title = "SociaLite", - ) - }, - backgroundColor = GlanceTheme.colors.widgetBackground, - modifier = GlanceModifier.fillMaxSize(), - ) { - when (model) { - null -> ZeroState(repository, widgetId, context) - else -> FavoriteContact(model) - } - } - } - } - - @Composable - private fun FavoriteContact(model: WidgetModel) { - val appContext = LocalContext.current.applicationContext - Box( - modifier = GlanceModifier.fillMaxSize().then( - - if (model.unreadMessages) { - GlanceModifier.clickable( - actionStartActivity( - Intent(appContext, MainActivity::class.java) - .setAction(Intent.ACTION_VIEW) - .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - .setData("https://socialite.google.com/chat/${model.contactId}".toUri()), - ), - ) - } else { - GlanceModifier - }, - ), - contentAlignment = Alignment.Center, - ) { - Column { - Image( - provider = ImageProvider(model.photo.toUri()), - contentDescription = model.displayName, - ) - Text( - text = model.displayName, - style = TextStyle( - fontWeight = FontWeight.Bold, - color = GlanceTheme.colors.onBackground, - ), - ) - Text( - text = if (model.unreadMessages) "New Message!" else "No messages", - style = TextStyle(color = GlanceTheme.colors.onBackground), - ) - } - } - } - - @Composable - private fun ZeroState(repository: WidgetModelRepository, widgetId: Int, context: Context) { - Box(modifier = GlanceModifier.fillMaxSize(), contentAlignment = Alignment.TopStart) { - LazyColumn { - items(Contact.CONTACTS.size) { contactIndex -> - val contact = Contact.CONTACTS[contactIndex] - var profileImage by remember(contact.iconUri) { mutableStateOf(null) } - - LaunchedEffect(contact.iconUri) { - profileImage = getImage(contact.iconUri, false, context) - } - - Row( - modifier = GlanceModifier.fillMaxWidth().clickable( - { - runBlocking { - repository.create( - WidgetModel( - widgetId, - contact.id, - contact.name, - contact.iconUri.toString(), - false, - ), - ) - } - }, - ).wrapContentHeight().padding(4.dp), - verticalAlignment = Alignment.Vertical.CenterVertically, - ) { - Image( - modifier = GlanceModifier.size(48.dp).padding(end = 8.dp), - contentScale = ContentScale.Fit, - provider = if (profileImage == null) { - ImageProvider(R.drawable.ic_launcher_background) - } else { - ImageProvider( - profileImage!!, - ) - }, - contentDescription = "Avatar", - - ) - Column( - modifier = GlanceModifier.defaultWeight(), - horizontalAlignment = Alignment.Horizontal.Start, - ) { - Text( - text = contact.name, - style = TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 16.sp, - color = GlanceTheme.colors.onBackground, - ), - ) - Text( - text = "Click to select as favorite contact", - style = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - fontFamily = FontFamily.Monospace, - color = GlanceTheme.colors.onBackground, - ), - ) - } - } - } - } - } - } - - private suspend fun getImage(url: Uri, force: Boolean = false, context: Context): Bitmap? { - val request = - ImageRequest.Builder(context).transformations(CircleCropTransformation()).data(url) - .apply { - if (force) { - memoryCachePolicy(CachePolicy.DISABLED) - diskCachePolicy(CachePolicy.DISABLED) - } - }.build() - - // Request the image to be loaded and throw error if it failed - return when (val result = Coil.imageLoader(context).execute(request)) { - is ErrorResult -> { - Log.e("GLANCE", "Error " + result.throwable.message) - throw result.throwable - } - - is SuccessResult -> result.drawable.toBitmapOrNull() - } - } -} diff --git a/app/src/main/java/com/google/android/samples/socialite/widget/model/WidgetModelRepository.kt b/app/src/main/java/com/google/android/samples/socialite/widget/model/WidgetModelRepository.kt index a62aced0..0709a243 100644 --- a/app/src/main/java/com/google/android/samples/socialite/widget/model/WidgetModelRepository.kt +++ b/app/src/main/java/com/google/android/samples/socialite/widget/model/WidgetModelRepository.kt @@ -19,8 +19,12 @@ package com.google.android.samples.socialite.widget.model import android.content.Context import androidx.glance.appwidget.updateAll import com.google.android.samples.socialite.di.AppCoroutineScope -import com.google.android.samples.socialite.widget.SocialiteWidget +import com.google.android.samples.socialite.widget.SociaLiteAppWidget +import dagger.hilt.EntryPoint +import dagger.hilt.EntryPoints +import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.CoroutineScope @@ -30,9 +34,28 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch + @Singleton class WidgetModelRepository @Inject internal constructor(private val widgetModelDao: WidgetModelDao, @AppCoroutineScope private val coroutineScope: CoroutineScope, @ApplicationContext private val appContext: Context) { + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface WidgetModelRepositoryEntryoint { + fun widgetModelRepository() : WidgetModelRepository; + } + + companion object { + fun get(applicationContext: Context): WidgetModelRepository { + var widgetModelRepositoryEntryoint: WidgetModelRepositoryEntryoint = EntryPoints.get( + applicationContext, + WidgetModelRepositoryEntryoint::class.java, + ) + return widgetModelRepositoryEntryoint.widgetModelRepository(); + } + + } + suspend fun create(model: WidgetModel): WidgetModel { widgetModelDao.insert(model) return widgetModelDao.loadWidgetModel(model.widgetId).filterNotNull().first() @@ -58,7 +81,7 @@ class WidgetModelRepository @Inject internal constructor(private val widgetModel coroutineScope.launch { widgetModelDao.modelsForContact(contactId).filterNotNull().forEach { model -> widgetModelDao.update(WidgetModel(model.widgetId, model.contactId, model.displayName, model.photo, unread)) - SocialiteWidget().updateAll(appContext) + SociaLiteAppWidget().updateAll(appContext) } } }