Merge branch 'main' into jr/track-viewed

pull/595/head
James Rose 2 years ago
commit 050db2cb72

@ -27,7 +27,7 @@ jobs:
- name: Run instrumented tests with GMD
run: ./gradlew cleanManagedDevices --unused-only &&
./gradlew ${{ matrix.device-config }}DemoDebugAndroidTest -Dorg.gradle.workers.max=1
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true --info
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
- name: Upload test reports
if: success() || failure()

@ -65,9 +65,8 @@ android {
}
dependencies {
implementation(project(":core:ui"))
implementation(project(":core:designsystem"))
implementation(libs.androidx.activity.compose)
implementation(project(":core:ui"))
implementation(libs.accompanist.flowlayout)
implementation(libs.androidx.activity.compose)
}

@ -68,13 +68,13 @@ android {
}
dependencies {
implementation(libs.androidx.benchmark.macro)
implementation(libs.androidx.test.core)
implementation(libs.androidx.test.espresso.core)
implementation(libs.androidx.test.ext)
implementation(libs.androidx.test.runner)
implementation(libs.androidx.test.rules)
implementation(libs.androidx.test.runner)
implementation(libs.androidx.test.uiautomator)
implementation(libs.androidx.benchmark.macro)
}
androidComponents {

@ -27,9 +27,9 @@ java {
dependencies {
compileOnly(libs.android.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.firebase.performance.gradle)
compileOnly(libs.firebase.crashlytics.gradle)
compileOnly(libs.firebase.performance.gradle)
compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.ksp.gradlePlugin)
}

Binary file not shown.

@ -1,5 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

@ -24,10 +24,9 @@ android {
}
dependencies {
implementation(libs.kotlinx.coroutines.android)
implementation(platform(libs.firebase.bom))
implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.core.ktx)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.analytics)
implementation(libs.kotlinx.coroutines.android)
}

@ -25,24 +25,24 @@ android {
testOptions {
unitTests {
isIncludeAndroidResources = true
isReturnDefaultValues = true
}
}
}
dependencies {
implementation(project(":core:analytics"))
implementation(project(":core:common"))
implementation(project(":core:model"))
implementation(project(":core:database"))
implementation(project(":core:datastore"))
implementation(project(":core:model"))
implementation(project(":core:network"))
implementation(project(":core:analytics"))
testImplementation(project(":core:testing"))
testImplementation(project(":core:datastore-test"))
implementation(project(":core:notifications"))
implementation(libs.androidx.core.ktx)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
}
testImplementation(project(":core:datastore-test"))
testImplementation(project(":core:testing"))
}

@ -30,7 +30,9 @@ import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.notifications.Notifier
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import javax.inject.Inject
@ -46,6 +48,7 @@ class OfflineFirstNewsRepository @Inject constructor(
private val newsResourceDao: NewsResourceDao,
private val topicDao: TopicDao,
private val network: NiaNetworkDataSource,
private val notifier: Notifier,
) : NewsRepository {
override fun getNewsResources(
@ -69,6 +72,16 @@ class OfflineFirstNewsRepository @Inject constructor(
},
modelDeleter = newsResourceDao::deleteNewsResources,
modelUpdater = { changedIds ->
// TODO: Make this more efficient, there is no need to retrieve populated
// news resources when all that's needed are the ids
val existingNewsResourceIds = newsResourceDao.getNewsResources(
useFilterNewsIds = true,
filterNewsIds = changedIds.toSet(),
)
.first()
.map { it.entity.id }
.toSet()
changedIds.chunked(SYNC_BATCH_SIZE).forEach { chunkedIds ->
val networkNewsResources = network.getNewsResources(ids = chunkedIds)
@ -92,6 +105,20 @@ class OfflineFirstNewsRepository @Inject constructor(
.flatten(),
)
}
val addedNewsResources = newsResourceDao.getNewsResources(
useFilterNewsIds = true,
filterNewsIds = changedIds.toSet(),
)
.first()
.filter { !existingNewsResourceIds.contains(it.entity.id) }
.map(PopulatedNewsResource::asExternalModel)
// TODO: Define business logic for notifications on first time sync.
// we probably do not want to send notifications on first install.
// We can easily check if the change list version is 0 and not send notifications
// if it is.
if (addedNewsResources.isNotEmpty()) notifier.onNewsAdded(addedNewsResources)
},
)
}

@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.Flow
/**
* Reports on if synchronization is in progress
*/
interface SyncStatusMonitor {
interface SyncManager {
val isSyncing: Flow<Boolean>
fun requestSync()
}

@ -36,6 +36,7 @@ import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferen
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
import com.google.samples.apps.nowinandroid.core.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.testing.notifications.TestNotifier
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@ -58,6 +59,8 @@ class OfflineFirstNewsRepositoryTest {
private lateinit var network: TestNiaNetworkDataSource
private lateinit var notifier: TestNotifier
private lateinit var synchronizer: Synchronizer
@get:Rule
@ -68,6 +71,7 @@ class OfflineFirstNewsRepositoryTest {
newsResourceDao = TestNewsResourceDao()
topicDao = TestTopicDao()
network = TestNiaNetworkDataSource()
notifier = TestNotifier()
synchronizer = TestSynchronizer(
NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore(testScope),
@ -78,6 +82,7 @@ class OfflineFirstNewsRepositoryTest {
newsResourceDao = newsResourceDao,
topicDao = topicDao,
network = network,
notifier = notifier,
)
}
@ -145,6 +150,12 @@ class OfflineFirstNewsRepositoryTest {
expected = network.latestChangeListVersion(CollectionType.NewsResources),
actual = synchronizer.getChangeListVersions().newsResourceVersion,
)
// Notifier should have been called with new news resources
assertEquals(
expected = newsResourcesFromDb.map(NewsResource::id).sorted(),
actual = notifier.addedNewsResources.first().map(NewsResource::id).sorted(),
)
}
@Test
@ -186,6 +197,13 @@ class OfflineFirstNewsRepositoryTest {
expected = network.latestChangeListVersion(CollectionType.NewsResources),
actual = synchronizer.getChangeListVersions().newsResourceVersion,
)
// Notifier should have been called with news resources from network that are not
// deleted
assertEquals(
expected = (newsResourcesFromNetwork.map(NewsResource::id) - deletedItems).sorted(),
actual = notifier.addedNewsResources.first().map(NewsResource::id).sorted(),
)
}
@Test
@ -225,6 +243,12 @@ class OfflineFirstNewsRepositoryTest {
expected = changeList.last().changeListVersion,
actual = synchronizer.getChangeListVersions().newsResourceVersion,
)
// Notifier should have been called with only added news resources from network
assertEquals(
expected = newsResourcesFromNetwork.map(NewsResource::id).sorted(),
actual = notifier.addedNewsResources.first().map(NewsResource::id).sorted(),
)
}
@Test

@ -24,8 +24,8 @@ android {
dependencies {
api(project(":core:datastore"))
api(libs.androidx.dataStore.core)
implementation(project(":core:common"))
implementation(project(":core:testing"))
api(libs.androidx.dataStore.core)
}

@ -33,6 +33,11 @@ android {
consumerProguardFiles("consumer-proguard-rules.pro")
}
namespace = "com.google.samples.apps.nowinandroid.core.datastore"
testOptions {
unitTests {
isReturnDefaultValues = true
}
}
}
// Setup protobuf configuration, generating lite Java and Kotlin classes
@ -57,12 +62,10 @@ protobuf {
dependencies {
implementation(project(":core:common"))
implementation(project(":core:model"))
testImplementation(project(":core:testing"))
testImplementation(project(":core:datastore-test"))
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.dataStore.core)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.protobuf.kotlin.lite)
testImplementation(project(":core:datastore-test"))
testImplementation(project(":core:testing"))
}

@ -30,16 +30,20 @@ android {
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.coil.kt.compose)
lintPublish(project(":lint"))
api(libs.androidx.compose.foundation)
api(libs.androidx.compose.foundation.layout)
api(libs.androidx.compose.material.iconsExtended)
api(libs.androidx.compose.material3)
debugApi(libs.androidx.compose.ui.tooling)
api(libs.androidx.compose.runtime)
api(libs.androidx.compose.ui.tooling.preview)
api(libs.androidx.compose.ui.util)
api(libs.androidx.compose.runtime)
lintPublish(project(":lint"))
debugApi(libs.androidx.compose.ui.tooling)
implementation(libs.androidx.core.ktx)
implementation(libs.coil.kt.compose)
androidTestImplementation(project(":core:testing"))
}

@ -24,15 +24,13 @@ android {
}
dependencies {
implementation(project(":core:data"))
implementation(project(":core:model"))
testImplementation(project(":core:testing"))
implementation(libs.hilt.android)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.datetime)
implementation(libs.hilt.android)
kapt(libs.hilt.compiler)
testImplementation(project(":core:testing"))
}

@ -41,17 +41,14 @@ secrets {
dependencies {
implementation(project(":core:common"))
implementation(project(":core:model"))
testImplementation(project(":core:testing"))
implementation(libs.coil.kt)
implementation(libs.coil.kt.svg)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json)
implementation(libs.okhttp.logging)
implementation(libs.retrofit.core)
implementation(libs.retrofit.kotlin.serialization)
implementation(libs.coil.kt)
implementation(libs.coil.kt.svg)
testImplementation(project(":core:testing"))
}

@ -0,0 +1,35 @@
/*
* Copyright 2023 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
*
* https://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.
*/
plugins {
id("nowinandroid.android.library")
id("nowinandroid.android.library.compose")
id("nowinandroid.android.hilt")
}
android {
namespace = "com.google.samples.apps.nowinandroid.core.notifications"
}
dependencies {
implementation(project(":core:model"))
implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.core.ktx)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.cloud.messaging)
}

@ -0,0 +1,31 @@
/*
* Copyright 2023 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
*
* https://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.samples.apps.nowinandroid.core.notifications
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
abstract class NotificationsModule {
@Binds
abstract fun bindNotifier(
notifier: NoOpNotifier,
): Notifier
}

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2023 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"/>

@ -0,0 +1,32 @@
/*
* Copyright 2023 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
*
* https://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.samples.apps.nowinandroid.core.notifications
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import javax.inject.Inject
import javax.inject.Singleton
/**
* Implementation of [Notifier] that displays notifications in the system tray.
*/
@Singleton
class AndroidSystemNotifier @Inject constructor() : Notifier {
override fun onNewsAdded(newsResources: List<NewsResource>) {
// TODO, create notification and display to the user
}
}

@ -0,0 +1,27 @@
/*
* Copyright 2023 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
*
* https://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.samples.apps.nowinandroid.core.notifications
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import javax.inject.Inject
/**
* Implementation of [Notifier] which does nothing. Useful for tests and previews.
*/
class NoOpNotifier @Inject constructor() : Notifier {
override fun onNewsAdded(newsResources: List<NewsResource>) = Unit
}

@ -0,0 +1,26 @@
/*
* Copyright 2023 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
*
* https://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.samples.apps.nowinandroid.core.notifications
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
/**
* Interface for creating notifications in the app
*/
interface Notifier {
fun onNewsAdded(newsResources: List<NewsResource>)
}

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 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.
-->
<resources>
<string name="sync_notification_title">Now in Android</string>
<string name="sync_notification_channel_name">Sync</string>
<string name="sync_notification_channel_description">Background tasks for Now in Android</string>
</resources>

@ -0,0 +1,31 @@
/*
* Copyright 2023 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
*
* https://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.samples.apps.nowinandroid.core.notifications
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
abstract class NotificationsModule {
@Binds
abstract fun bindNotifier(
notifier: AndroidSystemNotifier,
): Notifier
}

@ -24,23 +24,22 @@ android {
}
dependencies {
implementation(project(":core:common"))
implementation(project(":core:data"))
implementation(project(":core:domain"))
implementation(project(":core:model"))
implementation(libs.kotlinx.datetime)
api(libs.junit4)
api(libs.androidx.compose.ui.test)
api(libs.androidx.test.core)
api(libs.kotlinx.coroutines.test)
api(libs.turbine)
api(libs.androidx.test.espresso.core)
api(libs.androidx.test.runner)
api(libs.androidx.test.rules)
api(libs.androidx.compose.ui.test)
api(libs.androidx.test.runner)
api(libs.hilt.android.testing)
api(libs.junit4)
api(libs.kotlinx.coroutines.test)
api(libs.turbine)
debugApi(libs.androidx.compose.ui.testManifest)
implementation(project(":core:common"))
implementation(project(":core:data"))
implementation(project(":core:domain"))
implementation(project(":core:model"))
implementation(project(":core:notifications"))
implementation(libs.kotlinx.datetime)
}

@ -0,0 +1,34 @@
/*
* Copyright 2023 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
*
* https://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.samples.apps.nowinandroid.core.testing.notifications
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.notifications.Notifier
/**
* Aggregates news resources that have been notified for addition
*/
class TestNotifier : Notifier {
private val mutableAddedNewResources = mutableListOf<List<NewsResource>>()
val addedNewsResources: List<List<NewsResource>> = mutableAddedNewResources
override fun onNewsAdded(newsResources: List<NewsResource>) {
mutableAddedNewResources.add(newsResources)
}
}

@ -16,16 +16,20 @@
package com.google.samples.apps.nowinandroid.core.testing.util
import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor
import com.google.samples.apps.nowinandroid.core.data.util.SyncManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class TestSyncStatusMonitor : SyncStatusMonitor {
class TestSyncManager : SyncManager {
private val syncStatusFlow = MutableStateFlow(false)
override val isSyncing: Flow<Boolean> = syncStatusFlow
override fun requestSync() {
TODO("Not yet implemented")
}
/**
* A test-only API to set the sync status from tests.
*/

@ -27,28 +27,28 @@ android {
}
dependencies {
implementation(project(":core:designsystem"))
implementation(project(":core:model"))
implementation(project(":core:domain"))
implementation(project(":core:analytics"))
implementation(libs.androidx.browser)
implementation(libs.androidx.core.ktx)
implementation(libs.coil.kt)
implementation(libs.coil.kt.compose)
implementation(libs.kotlinx.datetime)
api(libs.androidx.compose.foundation)
api(libs.androidx.compose.foundation.layout)
api(libs.androidx.compose.material.iconsExtended)
api(libs.androidx.compose.material3)
debugApi(libs.androidx.compose.ui.tooling)
api(libs.androidx.compose.ui.tooling.preview)
api(libs.androidx.compose.ui.util)
api(libs.androidx.compose.runtime)
api(libs.androidx.compose.runtime.livedata)
api(libs.androidx.compose.ui.tooling.preview)
api(libs.androidx.compose.ui.util)
api(libs.androidx.metrics)
api(libs.androidx.tracing.ktx)
debugApi(libs.androidx.compose.ui.tooling)
implementation(project(":core:analytics"))
implementation(project(":core:designsystem"))
implementation(project(":core:domain"))
implementation(project(":core:model"))
implementation(libs.androidx.browser)
implementation(libs.androidx.core.ktx)
implementation(libs.coil.kt)
implementation(libs.coil.kt.compose)
implementation(libs.kotlinx.datetime)
androidTestImplementation(project(":core:testing"))
}

@ -39,7 +39,7 @@ fun AnalyticsHelper.logScreenView(screenName: String) {
)
}
fun AnalyticsHelper.logNewsResourceOpened(newsResourceId: String, newsResourceTitle: String) {
fun AnalyticsHelper.logNewsResourceOpened(newsResourceId: String) {
logEvent(
event = AnalyticsEvent(
type = "news_resource_opened",

@ -21,6 +21,7 @@ import android.net.Uri
import androidx.annotation.ColorInt
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridScope
@ -31,6 +32,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Devices
@ -68,7 +70,6 @@ fun LazyGridScope.newsFeed(
onClick = {
analyticsHelper.logNewsResourceOpened(
newsResourceId = userNewsResource.id,
newsResourceTitle = userNewsResource.title,
)
launchCustomChromeTab(context, resourceUrl, backgroundColor)
onNewsResourceViewed(userNewsResource.id)
@ -81,6 +82,7 @@ fun LazyGridScope.newsFeed(
)
},
onTopicClick = onTopicClick,
modifier = Modifier.padding(horizontal = 8.dp),
)
}
}

@ -37,6 +37,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -328,16 +329,20 @@ private fun ExpandedNewsResourcePreview(
@PreviewParameter(UserNewsResourcePreviewParameterProvider::class)
userNewsResources: List<UserNewsResource>,
) {
NiaTheme {
Surface {
NewsResourceCardExpanded(
userNewsResource = userNewsResources[0],
isBookmarked = true,
hasBeenViewed = false,
onToggleBookmark = {},
onClick = {},
onTopicClick = {},
)
CompositionLocalProvider(
LocalInspectionMode provides true,
) {
NiaTheme {
Surface {
NewsResourceCardExpanded(
userNewsResource = userNewsResources[0],
isBookmarked = true,
hasBeenViewed = false,
onToggleBookmark = {},
onClick = {},
onTopicClick = {},
)
}
}
}
}

@ -58,7 +58,6 @@ fun LazyListScope.userNewsResourceCardItems(
onClick = {
analyticsHelper.logNewsResourceOpened(
newsResourceId = userNewsResource.id,
newsResourceTitle = userNewsResource.title,
)
when (onItemClick) {
null -> launchCustomChromeTab(context, resourceUrl, backgroundColor)

@ -163,7 +163,7 @@ private fun EmptyState(modifier: Modifier = Modifier) {
contentDescription = null,
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(48.dp))
Text(
text = stringResource(id = R.string.bookmarks_empty_error),

@ -27,7 +27,7 @@ android {
}
dependencies {
implementation(libs.kotlinx.datetime)
implementation(libs.accompanist.flowlayout)
implementation(libs.kotlinx.datetime)
implementation(libs.androidx.activity.compose)
}

@ -16,7 +16,7 @@
package com.google.samples.apps.nowinandroid.feature.foryou
import android.app.Activity
import androidx.activity.compose.ReportDrawnWhen
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@ -38,6 +38,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.grid.GridCells
@ -56,13 +57,11 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -72,7 +71,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.trace
import androidx.core.view.doOnPreDraw
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicAsyncImage
@ -127,23 +125,8 @@ internal fun ForYouScreen(
val isOnboardingLoading = onboardingUiState is OnboardingUiState.Loading
val isFeedLoading = feedState is NewsFeedUiState.Loading
// Workaround to call Activity.reportFullyDrawn from Jetpack Compose.
// This code should be called when the UI is ready for use
// and relates to Time To Full Display.
// TODO replace with ReportDrawnWhen { } once androidx.activity-compose 1.7.0 is used (currently alpha)
if (!isSyncing && !isOnboardingLoading && !isFeedLoading) {
val localView = LocalView.current
// We use Unit to call reportFullyDrawn only on the first recomposition,
// however it will be called again if this composable goes out of scope.
// Activity.reportFullyDrawn() has its own check for this
// and is safe to call multiple times though.
LaunchedEffect(Unit) {
// We're leveraging the fact, that the current view is directly set as content of Activity.
val activity = localView.context as? Activity ?: return@LaunchedEffect
// To be sure not to call in the middle of a frame draw.
localView.doOnPreDraw { activity.reportFullyDrawn() }
}
}
// This code should be called when the UI is ready for use and relates to Time To Full Display.
ReportDrawnWhen { !isSyncing && !isOnboardingLoading && !isFeedLoading }
val state = rememberLazyGridState()
TrackScrollJank(scrollableState = state, stateName = "forYou:feed")
@ -250,7 +233,7 @@ private fun LazyGridScope.onboarding(
text = stringResource(R.string.onboarding_guidance_subtitle),
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp, start = 16.dp, end = 16.dp),
.padding(top = 8.dp, start = 24.dp, end = 24.dp),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium,
)
@ -268,8 +251,9 @@ private fun LazyGridScope.onboarding(
onClick = saveFollowedTopics,
enabled = onboardingUiState.isDismissable,
modifier = Modifier
.padding(horizontal = 40.dp)
.width(364.dp),
.padding(horizontal = 24.dp)
.widthIn(364.dp)
.fillMaxWidth(),
) {
Text(
text = stringResource(R.string.done),
@ -458,7 +442,8 @@ fun ForYouScreenTopicSelection(
ForYouScreen(
isSyncing = false,
onboardingUiState = OnboardingUiState.Shown(
topics = userNewsResources.flatMap { news -> news.followableTopics },
topics = userNewsResources.flatMap { news -> news.followableTopics }
.distinctBy { it.topic.id },
),
feedState = NewsFeedUiState.Success(
feed = userNewsResources,

@ -20,7 +20,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor
import com.google.samples.apps.nowinandroid.core.data.util.SyncManager
import com.google.samples.apps.nowinandroid.core.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import dagger.hilt.android.lifecycle.HiltViewModel
@ -35,7 +35,7 @@ import javax.inject.Inject
@HiltViewModel
class ForYouViewModel @Inject constructor(
syncStatusMonitor: SyncStatusMonitor,
syncManager: SyncManager,
private val userDataRepository: UserDataRepository,
userNewsResourceRepository: UserNewsResourceRepository,
getFollowableTopics: GetFollowableTopicsUseCase,
@ -44,7 +44,7 @@ class ForYouViewModel @Inject constructor(
private val shouldShowOnboarding: Flow<Boolean> =
userDataRepository.userData.map { !it.shouldHideOnboarding }
val isSyncing = syncStatusMonitor.isSyncing
val isSyncing = syncManager.isSyncing
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
@ -52,7 +52,7 @@ class ForYouViewModel @Inject constructor(
)
val feedState: StateFlow<NewsFeedUiState> =
userNewsResourceRepository.getUserNewsResourcesForFollowedTopics()
userNewsResourceRepository.observeAllForFollowedTopics()
.map(NewsFeedUiState::Success)
.stateIn(
scope = viewModelScope,

@ -30,7 +30,7 @@ import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserData
import com.google.samples.apps.nowinandroid.core.testing.repository.emptyUserData
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
import com.google.samples.apps.nowinandroid.core.testing.util.TestNetworkMonitor
import com.google.samples.apps.nowinandroid.core.testing.util.TestSyncStatusMonitor
import com.google.samples.apps.nowinandroid.core.testing.util.TestSyncManager
import com.google.samples.apps.nowinandroid.core.ui.NewsFeedUiState
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@ -52,7 +52,7 @@ class ForYouViewModelTest {
val mainDispatcherRule = MainDispatcherRule()
private val networkMonitor = TestNetworkMonitor()
private val syncStatusMonitor = TestSyncStatusMonitor()
private val syncManager = TestSyncManager()
private val userDataRepository = TestUserDataRepository()
private val topicsRepository = TestTopicsRepository()
private val newsRepository = TestNewsRepository()
@ -70,7 +70,7 @@ class ForYouViewModelTest {
@Before
fun setup() {
viewModel = ForYouViewModel(
syncStatusMonitor = syncStatusMonitor,
syncManager = syncManager,
userDataRepository = userDataRepository,
userNewsResourceRepository = userNewsResourceRepository,
getFollowableTopics = getFollowableTopicsUseCase,
@ -106,7 +106,7 @@ class ForYouViewModelTest {
@Test
fun stateIsLoadingWhenAppIsSyncingWithNoInterests() = runTest {
syncStatusMonitor.setSyncing(true)
syncManager.setSyncing(true)
val collectJob =
launch(UnconfinedTestDispatcher()) { viewModel.isSyncing.collect() }

@ -65,7 +65,7 @@ fun InterestsItem(
.padding(vertical = itemSeparation),
) {
InterestsIcon(topicImageUrl, iconModifier.size(64.dp))
Spacer(modifier = Modifier.width(16.dp))
Spacer(modifier = Modifier.width(24.dp))
InterestContent(name, description)
}
NiaIconToggleButton(

@ -38,9 +38,9 @@ fun TopicsTabContent(
) {
LazyColumn(
modifier = modifier
.padding(horizontal = 16.dp)
.padding(horizontal = 24.dp)
.testTag("interests:topics"),
contentPadding = PaddingValues(top = 8.dp),
contentPadding = PaddingValues(vertical = 16.dp),
) {
topics.forEach { followableTopic ->
val topicId = followableTopic.topic.id

@ -2,7 +2,7 @@
accompanist = "0.28.0"
androidDesugarJdkLibs = "1.2.2"
androidGradlePlugin = "7.4.1"
androidxActivity = "1.6.1"
androidxActivity = "1.7.0"
androidxAppCompat = "1.5.1"
androidxBrowser = "1.4.0"
androidxComposeBom = "2023.01.00"
@ -87,26 +87,26 @@ androidx-metrics = { group = "androidx.metrics", name = "metrics-performance", v
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" }
androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxNavigation" }
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "androidxProfileinstaller" }
androidx-startup = { group = "androidx.startup", name = "startup-runtime", version.ref = "androidxStartup" }
androidx-test-core = { group = "androidx.test", name = "core", version.ref = "androidxTestCore" }
androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidxEspresso" }
androidx-test-ext = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidxTestExt" }
androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTestRules" }
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" }
androidx-test-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "androidxUiAutomator" }
androidx-tracing-ktx = { group = "androidx.tracing", name="tracing-ktx", version.ref = "androidxTracing" }
androidx-tracing-ktx = { group = "androidx.tracing", name = "tracing-ktx", version.ref = "androidxTracing" }
androidx-window-manager = { module = "androidx.window:window", version.ref = "androidxWindowManager" }
androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidxWork" }
androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidxWork" }
coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
coil-kt-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
coil-kt-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil" }
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref="firebaseBom"}
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx"}
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx"}
firebase-crashlytics-gradle = { group = "com.google.firebase", name="firebase-crashlytics-gradle", version.ref = "firebaseCrashlyticsPlugin"}
firebase-performance = { group = "com.google.firebase", name = "firebase-perf-ktx"}
firebase-performance-gradle = { group = "com.google.firebase", name = "perf-plugin", version.ref = "firebasePerfPlugin"}
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx" }
firebase-cloud-messaging = { group = "com.google.firebase", name = "firebase-messaging-ktx" }
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx" }
firebase-crashlytics-gradle = { group = "com.google.firebase", name = "firebase-crashlytics-gradle", version.ref = "firebaseCrashlyticsPlugin" }
firebase-performance = { group = "com.google.firebase", name = "firebase-perf-ktx" }
firebase-performance-gradle = { group = "com.google.firebase", name = "perf-plugin", version.ref = "firebasePerfPlugin" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testing", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
@ -138,9 +138,9 @@ ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devto
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin"}
firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerfPlugin"}
gms = { id = "com.google.gms.google-services", version.ref = "gmsPlugin"}
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin" }
firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerfPlugin" }
gms = { id = "com.google.gms.google-services", version.ref = "gmsPlugin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

@ -47,6 +47,7 @@ include(":core:network")
include(":core:ui")
include(":core:testing")
include(":core:analytics")
include(":core:notifications")
include(":feature:foryou")
include(":feature:interests")

@ -16,11 +16,12 @@
package com.google.samples.apps.nowinandroid.core.sync.test
import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor
import com.google.samples.apps.nowinandroid.core.data.util.SyncManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import javax.inject.Inject
class NeverSyncingSyncStatusMonitor @Inject constructor() : SyncStatusMonitor {
class NeverSyncingSyncManager @Inject constructor() : SyncManager {
override val isSyncing: Flow<Boolean> = flowOf(false)
override fun requestSync() = Unit
}

@ -16,7 +16,7 @@
package com.google.samples.apps.nowinandroid.core.sync.test
import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor
import com.google.samples.apps.nowinandroid.core.data.util.SyncManager
import com.google.samples.apps.nowinandroid.sync.di.SyncModule
import dagger.Binds
import dagger.Module
@ -31,6 +31,6 @@ import dagger.hilt.testing.TestInstallIn
interface TestSyncModule {
@Binds
fun bindsSyncStatusMonitor(
syncStatusMonitor: NeverSyncingSyncStatusMonitor,
): SyncStatusMonitor
syncStatusMonitor: NeverSyncingSyncManager,
): SyncManager
}

@ -27,24 +27,22 @@ android {
}
dependencies {
implementation(project(":core:analytics"))
implementation(project(":core:common"))
implementation(project(":core:model"))
implementation(project(":core:data"))
implementation(project(":core:datastore"))
implementation(project(":core:analytics"))
implementation(libs.kotlinx.coroutines.android)
implementation(project(":core:model"))
implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.tracing.ktx)
implementation(libs.androidx.startup)
implementation(libs.androidx.work.ktx)
implementation(libs.firebase.cloud.messaging)
implementation(libs.hilt.ext.work)
testImplementation(project(":core:testing"))
androidTestImplementation(project(":core:testing"))
implementation(libs.kotlinx.coroutines.android)
kapt(libs.hilt.ext.compiler)
testImplementation(project(":core:testing"))
androidTestImplementation(project(":core:testing"))
androidTestImplementation(libs.androidx.work.testing)
}

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<service
android:name=".services.SyncNotificationsService"
android:exported="false"
tools:node="remove" />
</application>
</manifest>

@ -18,18 +18,13 @@
xmlns:tools="http://schemas.android.com/tools">
<application>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- TODO: b/2173216 Disable auto sync startup till it works well with instrumented tests -->
<meta-data
android:name="com.google.samples.apps.nowinandroid.sync.initializers.SyncInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
<service
android:name=".services.SyncNotificationsService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>

@ -16,8 +16,8 @@
package com.google.samples.apps.nowinandroid.sync.di
import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor
import com.google.samples.apps.nowinandroid.sync.status.WorkManagerSyncStatusMonitor
import com.google.samples.apps.nowinandroid.core.data.util.SyncManager
import com.google.samples.apps.nowinandroid.sync.status.WorkManagerSyncManager
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -28,6 +28,6 @@ import dagger.hilt.components.SingletonComponent
interface SyncModule {
@Binds
fun bindsSyncStatusMonitor(
syncStatusMonitor: WorkManagerSyncStatusMonitor,
): SyncStatusMonitor
syncStatusMonitor: WorkManagerSyncManager,
): SyncManager
}

@ -17,31 +17,14 @@
package com.google.samples.apps.nowinandroid.sync.initializers
import android.content.Context
import androidx.startup.AppInitializer
import androidx.startup.Initializer
import androidx.work.ExistingWorkPolicy
import androidx.work.WorkManager
import androidx.work.WorkManagerInitializer
import com.google.samples.apps.nowinandroid.sync.workers.SyncWorker
object Sync {
// This method is a workaround to manually initialize the sync process instead of relying on
// automatic initialization with Androidx Startup. It is called from the app module's
// Application.onCreate() and should be only done once.
// This method is initializes sync, the process that keeps the app's data current.
// It is called from the app module's Application.onCreate() and should be only done once.
fun initialize(context: Context) {
AppInitializer.getInstance(context)
.initializeComponent(SyncInitializer::class.java)
}
}
// This name should not be changed otherwise the app may have concurrent sync requests running
internal const val SyncWorkName = "SyncWorkName"
/**
* Registers work to sync the data layer periodically on app startup.
*/
class SyncInitializer : Initializer<Sync> {
override fun create(context: Context): Sync {
WorkManager.getInstance(context).apply {
// Run sync on app startup and ensure only one sync worker runs at any time
enqueueUniqueWork(
@ -50,10 +33,8 @@ class SyncInitializer : Initializer<Sync> {
SyncWorker.startUpSyncWork(),
)
}
return Sync
}
override fun dependencies(): List<Class<out Initializer<*>>> =
listOf(WorkManagerInitializer::class.java)
}
// This name should not be changed otherwise the app may have concurrent sync requests running
internal const val SyncWorkName = "SyncWorkName"

@ -0,0 +1,38 @@
/*
* Copyright 2023 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
*
* https://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.samples.apps.nowinandroid.sync.services
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.google.samples.apps.nowinandroid.core.data.util.SyncManager
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
private const val SYNC_TOPIC = "sync"
@AndroidEntryPoint
class SyncNotificationsService : FirebaseMessagingService() {
@Inject
lateinit var syncManager: SyncManager
override fun onMessageReceived(message: RemoteMessage) {
if (SYNC_TOPIC == message.from) {
syncManager.requestSync()
}
}
}

@ -19,27 +19,39 @@ package com.google.samples.apps.nowinandroid.sync.status
import android.content.Context
import androidx.lifecycle.asFlow
import androidx.lifecycle.map
import androidx.work.ExistingWorkPolicy
import androidx.work.WorkInfo
import androidx.work.WorkInfo.State
import androidx.work.WorkManager
import com.google.samples.apps.nowinandroid.core.data.util.SyncStatusMonitor
import com.google.samples.apps.nowinandroid.core.data.util.SyncManager
import com.google.samples.apps.nowinandroid.sync.initializers.SyncWorkName
import com.google.samples.apps.nowinandroid.sync.workers.SyncWorker
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import javax.inject.Inject
/**
* [SyncStatusMonitor] backed by [WorkInfo] from [WorkManager]
* [SyncManager] backed by [WorkInfo] from [WorkManager]
*/
class WorkManagerSyncStatusMonitor @Inject constructor(
@ApplicationContext context: Context,
) : SyncStatusMonitor {
class WorkManagerSyncManager @Inject constructor(
@ApplicationContext private val context: Context,
) : SyncManager {
override val isSyncing: Flow<Boolean> =
WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData(SyncWorkName)
.map(MutableList<WorkInfo>::anyRunning)
.asFlow()
.conflate()
override fun requestSync() {
val workManager = WorkManager.getInstance(context)
// Run sync on app startup and ensure only one sync worker runs at any time
workManager.enqueueUniqueWork(
SyncWorkName,
ExistingWorkPolicy.KEEP,
SyncWorker.startUpSyncWork(),
)
}
}
private val List<WorkInfo>.anyRunning get() = any { it.state == State.RUNNING }
Loading…
Cancel
Save