Skip to content

Commit

Permalink
Glance Widget Solution
Browse files Browse the repository at this point in the history
Change-Id: Ie75625aa5ca568f41f94ef58b98a583c79aebe0d
  • Loading branch information
secondsun committed Apr 4, 2024
1 parent 5506e28 commit dc4a93f
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 42 deletions.
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ dependencies {
androidTestImplementation(libs.espresso.core)
baselineProfile(project(":baselineprofile"))

implementation(libs.glance.appwidget)
implementation(libs.glance.material)

val composeBom = platform(libs.compose.bom)
implementation(composeBom)
androidTestImplementation(composeBom)
Expand Down
18 changes: 18 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,23 @@
android:supportsRtl="true"
android:theme="@style/Theme.Social"
tools:targetApi="34">

<receiver
android:name=".widget.SociaLiteAppWidgetReceiver"
android:exported="true"
android:label="@string/favorite_contact_widget_name">


<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>


<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/socialite_widget_info" />
</receiver>

<activity
android:name=".MainActivity"
android:exported="true"
Expand All @@ -47,6 +64,7 @@
android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize"
android:supportsPictureInPicture="true">


<!-- This activity is the one that's shown in the launcher. -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,19 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.glance.appwidget.updateAll
import com.google.android.samples.socialite.ui.Main
import com.google.android.samples.socialite.ui.ShortcutParams
import com.google.android.samples.socialite.widget.SociaLiteAppWidget
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
runBlocking { SociaLiteAppWidget().updateAll(this@MainActivity) }
setContent {
Main(
shortcutParams = extractShortcutParams(intent),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,54 @@
package com.google.android.samples.socialite.widget

import android.content.Context
import android.content.Intent
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.core.net.toUri
import androidx.glance.GlanceId
import androidx.glance.GlanceTheme
import androidx.glance.LocalContext
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetManager
import androidx.glance.appwidget.action.actionStartActivity
import androidx.glance.appwidget.provideContent
import androidx.glance.layout.Box
import com.google.android.samples.socialite.MainActivity
import com.google.android.samples.socialite.widget.model.WidgetModel
import com.google.android.samples.socialite.widget.model.WidgetModelRepository
import com.google.android.samples.socialite.widget.model.WidgetState.Empty
import com.google.android.samples.socialite.widget.model.WidgetState.Loading
import com.google.android.samples.socialite.widget.ui.FavoriteContact
import com.google.android.samples.socialite.widget.ui.ZeroState

class SociaLiteAppWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
TODO("Not yet implemented")
val widgetId = GlanceAppWidgetManager(context).getAppWidgetId(id)
val repository = WidgetModelRepository.get(context)

provideContent {
GlanceTheme {
Content(repository, widgetId)
}
}
}

@Composable
private fun Content(repository: WidgetModelRepository, widgetId: Int) {
val model = repository.loadModel(widgetId).collectAsState(Loading).value
val context = LocalContext.current
when (model) {
is Empty -> ZeroState(widgetId)
is Loading -> Box {}
is WidgetModel -> FavoriteContact(
model,
actionStartActivity(
Intent(context.applicationContext, MainActivity::class.java)
.setAction(Intent.ACTION_VIEW)
.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
.setData("https://socialite.google.com/chat/${model.contactId}".toUri()),
),
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ class SociaLiteAppWidgetConfigActivity : ComponentActivity() {

HomeBackground()
val viewModel: HomeViewModel = hiltViewModel()

LazyColumn(
modifier = modifier,
contentPadding = innerPadding,
Expand All @@ -100,7 +99,26 @@ class SociaLiteAppWidgetConfigActivity : ComponentActivity() {

ContactRow(
contact = contact,
onClick = TODO("Replace with code from codelab"),
onClick = {
runBlocking {
widgetModelRepository.createOrUpdate(
WidgetModel(
appWidgetId,
contact.id,
contact.name,
contact.iconUri.toString(),
false,
),
)
SociaLiteAppWidget().updateAll(this@SociaLiteAppWidgetConfigActivity)
val resultValue = Intent().putExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
appWidgetId,
)
setResult(RESULT_OK, resultValue)
finish()
}
},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,22 @@

package com.google.android.samples.socialite.widget

import android.content.Context
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import com.google.android.samples.socialite.widget.model.WidgetModelRepository
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class SociaLiteAppWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = TODO("Create instance of SociaLiteAppWidget")
override val glanceAppWidget: GlanceAppWidget = SociaLiteAppWidget()

@Inject
lateinit var repository: WidgetModelRepository

override fun onDeleted(context: Context, appWidgetIds: IntArray) {
super.onDeleted(context, appWidgetIds)
repository.cleanupWidgetModels(context)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.android.samples.socialite.model.Contact

sealed interface WidgetState {
object Empty : WidgetState
object Loading : WidgetState
}

@Entity(
foreignKeys = [
ForeignKey(
Expand All @@ -42,4 +47,4 @@ data class WidgetModel(
val displayName: String,
val photo: String,
val unreadMessages: Boolean = false,
)
) : WidgetState
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,16 @@ import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

@Singleton
class WidgetModelRepository @Inject internal constructor(private val widgetModelDao: WidgetModelDao, @AppCoroutineScope private val coroutineScope: CoroutineScope, @ApplicationContext private val appContext: Context) {
class WidgetModelRepository @Inject internal constructor(
private val widgetModelDao: WidgetModelDao,
@AppCoroutineScope private val coroutineScope: CoroutineScope,
@ApplicationContext private val appContext: Context,
) {

@EntryPoint
@InstallIn(SingletonComponent::class)
Expand All @@ -54,21 +58,29 @@ class WidgetModelRepository @Inject internal constructor(private val widgetModel
}
}

suspend fun create(model: WidgetModel): WidgetModel {
widgetModelDao.insert(model)
return widgetModelDao.loadWidgetModel(model.widgetId).filterNotNull().first()
suspend fun createOrUpdate(model: WidgetModel): WidgetModel? {
val maybeModel = widgetModelDao.loadWidgetModel(model.widgetId).first()
if (maybeModel == null) {
widgetModelDao.insert(model)
} else {
widgetModelDao.update(model)
}
SociaLiteAppWidget().updateAll(appContext)
return widgetModelDao.loadWidgetModel(model.widgetId).first()
}

fun loadModel(widgetId: Int): Flow<WidgetModel?> {
return widgetModelDao.loadWidgetModel(widgetId).distinctUntilChanged()
fun loadModel(widgetId: Int): Flow<WidgetState> {
return widgetModelDao.loadWidgetModel(widgetId).map { it ?: WidgetState.Empty }
.distinctUntilChanged()
}

fun cleanupWidgetModels(context: Context) {
coroutineScope.launch {
val widgetManager = GlanceAppWidgetManager(context)
val widgetIds = widgetManager.getGlanceIds(SociaLiteAppWidget::class.java).map { glanceId ->
widgetManager.getAppWidgetId(glanceId)
}.toList()
val widgetIds =
widgetManager.getGlanceIds(SociaLiteAppWidget::class.java).map { glanceId ->
widgetManager.getAppWidgetId(glanceId)
}.toList()

widgetModelDao.findOrphanModels(widgetIds).forEach { model ->
widgetModelDao.delete(
Expand All @@ -80,21 +92,20 @@ class WidgetModelRepository @Inject internal constructor(private val widgetModel

fun updateUnreadMessagesForContact(contactId: Long, unread: Boolean) {
coroutineScope.launch {
widgetModelDao.modelsForContact(contactId).filterNotNull().forEach { model ->
widgetModelDao.update(WidgetModel(model.widgetId, model.contactId, model.displayName, model.photo, unread))
SociaLiteAppWidget().updateAll(appContext)
widgetModelDao.modelsForContact(contactId).forEach { model ->
if (model != null) {
widgetModelDao.update(
WidgetModel(
model.widgetId,
model.contactId,
model.displayName,
model.photo,
unread,
),
)
SociaLiteAppWidget().updateAll(appContext)
}
}
}
}

suspend fun createOrUpdate(model: WidgetModel): WidgetModel {
val maybeModel = widgetModelDao.loadWidgetModel(model.widgetId).first()
if (maybeModel == null) {
widgetModelDao.insert(model)
} else {
widgetModelDao.update(model)
}
SociaLiteAppWidget().updateAll(appContext)
return widgetModelDao.loadWidgetModel(model.widgetId).filterNotNull().first()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,19 @@
package com.google.android.samples.socialite.widget.ui

import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.action.Action
import androidx.glance.action.clickable
import androidx.glance.appwidget.ImageProvider
import androidx.glance.appwidget.appWidgetBackground
import androidx.glance.appwidget.cornerRadius
import androidx.glance.background
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.Column
import androidx.glance.layout.ContentScale
import androidx.glance.layout.fillMaxSize
Expand All @@ -40,34 +39,35 @@ import androidx.glance.layout.wrapContentHeight
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
import com.google.android.samples.socialite.R
import com.google.android.samples.socialite.widget.model.WidgetModel

@Composable
fun FavoriteContact(model: WidgetModel, onClick: Action) {
Box(
Column(
modifier = GlanceModifier.fillMaxSize().clickable(onClick)
.cornerRadius(8.dp).padding(bottom = 8.dp),
contentAlignment = Alignment.TopCenter,
.background(GlanceTheme.colors.widgetBackground).appWidgetBackground()
.padding(bottom = 8.dp),
verticalAlignment = Alignment.Vertical.Bottom,
horizontalAlignment = Alignment.Horizontal.CenterHorizontally,
) {
Image(
modifier = GlanceModifier.fillMaxSize().cornerRadius(8.dp),
modifier = GlanceModifier.fillMaxWidth().wrapContentHeight().defaultWeight()
.cornerRadius(16.dp),
provider = ImageProvider(model.photo.toUri()),
contentScale = ContentScale.Crop,
contentDescription = model.displayName,
)
Column(
modifier = GlanceModifier.fillMaxWidth().wrapContentHeight(),
verticalAlignment = Alignment.Vertical.Top,
modifier = GlanceModifier.fillMaxWidth().wrapContentHeight().padding(top = 4.dp),
verticalAlignment = Alignment.Vertical.Bottom,
horizontalAlignment = Alignment.Horizontal.CenterHorizontally,
) {
Text(
text = model.displayName,
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 24.sp,
color = ColorProvider(Color.White),
color = (GlanceTheme.colors.onSurface),
),
)

Expand All @@ -76,7 +76,7 @@ fun FavoriteContact(model: WidgetModel, onClick: Action) {
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
color = ColorProvider(Color.White),
color = (GlanceTheme.colors.onSurface),
),
)
}
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,6 @@
<string name="pause_title">Pause</string>
<string name="ff_title">Fast forward</string>
<string name="rw_title">Rewind</string>
<string name="favorite_contact_widget_name">Favorite Contact</string>

</resources>
Loading

0 comments on commit dc4a93f

Please sign in to comment.