Merge branch 'android:main' into main

pull/620/head
Kwak Wonjo 1 year ago committed by GitHub
commit 511752a126
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -27,7 +27,7 @@ jobs:
- name: Run instrumented tests with GMD - name: Run instrumented tests with GMD
run: ./gradlew cleanManagedDevices --unused-only && run: ./gradlew cleanManagedDevices --unused-only &&
./gradlew ${{ matrix.device-config }}DemoDebugAndroidTest -Dorg.gradle.workers.max=1 ./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 - name: Upload test reports
if: success() || failure() if: success() || failure()

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

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

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

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

@ -25,24 +25,24 @@ android {
testOptions { testOptions {
unitTests { unitTests {
isIncludeAndroidResources = true isIncludeAndroidResources = true
isReturnDefaultValues = true
} }
} }
} }
dependencies { dependencies {
implementation(project(":core:analytics"))
implementation(project(":core:common")) implementation(project(":core:common"))
implementation(project(":core:model"))
implementation(project(":core:database")) implementation(project(":core:database"))
implementation(project(":core:datastore")) implementation(project(":core:datastore"))
implementation(project(":core:model"))
implementation(project(":core:network")) implementation(project(":core:network"))
implementation(project(":core:analytics")) implementation(project(":core:notifications"))
testImplementation(project(":core:testing"))
testImplementation(project(":core:datastore-test"))
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json) 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.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource 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.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.notifications.Notifier
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import javax.inject.Inject import javax.inject.Inject
@ -46,6 +48,7 @@ class OfflineFirstNewsRepository @Inject constructor(
private val newsResourceDao: NewsResourceDao, private val newsResourceDao: NewsResourceDao,
private val topicDao: TopicDao, private val topicDao: TopicDao,
private val network: NiaNetworkDataSource, private val network: NiaNetworkDataSource,
private val notifier: Notifier,
) : NewsRepository { ) : NewsRepository {
override fun getNewsResources( override fun getNewsResources(
@ -69,6 +72,16 @@ class OfflineFirstNewsRepository @Inject constructor(
}, },
modelDeleter = newsResourceDao::deleteNewsResources, modelDeleter = newsResourceDao::deleteNewsResources,
modelUpdater = { changedIds -> 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 -> changedIds.chunked(SYNC_BATCH_SIZE).forEach { chunkedIds ->
val networkNewsResources = network.getNewsResources(ids = chunkedIds) val networkNewsResources = network.getNewsResources(ids = chunkedIds)
@ -92,6 +105,20 @@ class OfflineFirstNewsRepository @Inject constructor(
.flatten(), .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 * Reports on if synchronization is in progress
*/ */
interface SyncStatusMonitor { interface SyncManager {
val isSyncing: Flow<Boolean> 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.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList 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.network.model.NetworkNewsResource
import com.google.samples.apps.nowinandroid.core.testing.notifications.TestNotifier
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher
@ -58,6 +59,8 @@ class OfflineFirstNewsRepositoryTest {
private lateinit var network: TestNiaNetworkDataSource private lateinit var network: TestNiaNetworkDataSource
private lateinit var notifier: TestNotifier
private lateinit var synchronizer: Synchronizer private lateinit var synchronizer: Synchronizer
@get:Rule @get:Rule
@ -68,6 +71,7 @@ class OfflineFirstNewsRepositoryTest {
newsResourceDao = TestNewsResourceDao() newsResourceDao = TestNewsResourceDao()
topicDao = TestTopicDao() topicDao = TestTopicDao()
network = TestNiaNetworkDataSource() network = TestNiaNetworkDataSource()
notifier = TestNotifier()
synchronizer = TestSynchronizer( synchronizer = TestSynchronizer(
NiaPreferencesDataSource( NiaPreferencesDataSource(
tmpFolder.testUserPreferencesDataStore(testScope), tmpFolder.testUserPreferencesDataStore(testScope),
@ -78,6 +82,7 @@ class OfflineFirstNewsRepositoryTest {
newsResourceDao = newsResourceDao, newsResourceDao = newsResourceDao,
topicDao = topicDao, topicDao = topicDao,
network = network, network = network,
notifier = notifier,
) )
} }
@ -145,6 +150,12 @@ class OfflineFirstNewsRepositoryTest {
expected = network.latestChangeListVersion(CollectionType.NewsResources), expected = network.latestChangeListVersion(CollectionType.NewsResources),
actual = synchronizer.getChangeListVersions().newsResourceVersion, 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 @Test
@ -186,6 +197,13 @@ class OfflineFirstNewsRepositoryTest {
expected = network.latestChangeListVersion(CollectionType.NewsResources), expected = network.latestChangeListVersion(CollectionType.NewsResources),
actual = synchronizer.getChangeListVersions().newsResourceVersion, 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 @Test
@ -225,6 +243,12 @@ class OfflineFirstNewsRepositoryTest {
expected = changeList.last().changeListVersion, expected = changeList.last().changeListVersion,
actual = synchronizer.getChangeListVersions().newsResourceVersion, 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 @Test

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

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

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

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

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

@ -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 { dependencies {
implementation(project(":core:common")) api(libs.androidx.compose.ui.test)
implementation(project(":core:data"))
implementation(project(":core:domain"))
implementation(project(":core:model"))
implementation(libs.kotlinx.datetime)
api(libs.junit4)
api(libs.androidx.test.core) api(libs.androidx.test.core)
api(libs.kotlinx.coroutines.test)
api(libs.turbine)
api(libs.androidx.test.espresso.core) api(libs.androidx.test.espresso.core)
api(libs.androidx.test.runner)
api(libs.androidx.test.rules) api(libs.androidx.test.rules)
api(libs.androidx.compose.ui.test) api(libs.androidx.test.runner)
api(libs.hilt.android.testing) api(libs.hilt.android.testing)
api(libs.junit4)
api(libs.kotlinx.coroutines.test)
api(libs.turbine)
debugApi(libs.androidx.compose.ui.testManifest) 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 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.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
class TestSyncStatusMonitor : SyncStatusMonitor { class TestSyncManager : SyncManager {
private val syncStatusFlow = MutableStateFlow(false) private val syncStatusFlow = MutableStateFlow(false)
override val isSyncing: Flow<Boolean> = syncStatusFlow override val isSyncing: Flow<Boolean> = syncStatusFlow
override fun requestSync() {
TODO("Not yet implemented")
}
/** /**
* A test-only API to set the sync status from tests. * A test-only API to set the sync status from tests.
*/ */

@ -27,28 +27,28 @@ android {
} }
dependencies { 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)
api(libs.androidx.compose.foundation.layout) api(libs.androidx.compose.foundation.layout)
api(libs.androidx.compose.material.iconsExtended) api(libs.androidx.compose.material.iconsExtended)
api(libs.androidx.compose.material3) 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)
api(libs.androidx.compose.runtime.livedata) 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.metrics)
api(libs.androidx.tracing.ktx) 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")) androidTestImplementation(project(":core:testing"))
} }

@ -21,6 +21,7 @@ import android.net.Uri
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridScope 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.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Devices
@ -78,6 +80,7 @@ fun LazyGridScope.newsFeed(
) )
}, },
onTopicClick = onTopicClick, onTopicClick = onTopicClick,
modifier = Modifier.padding(horizontal = 8.dp),
) )
} }
} }

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

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

@ -38,6 +38,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
@ -247,7 +248,7 @@ private fun LazyGridScope.onboarding(
text = stringResource(R.string.onboarding_guidance_subtitle), text = stringResource(R.string.onboarding_guidance_subtitle),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 8.dp, start = 16.dp, end = 16.dp), .padding(top = 8.dp, start = 24.dp, end = 24.dp),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) )
@ -265,8 +266,9 @@ private fun LazyGridScope.onboarding(
onClick = saveFollowedTopics, onClick = saveFollowedTopics,
enabled = onboardingUiState.isDismissable, enabled = onboardingUiState.isDismissable,
modifier = Modifier modifier = Modifier
.padding(horizontal = 40.dp) .padding(horizontal = 24.dp)
.width(364.dp), .widthIn(364.dp)
.fillMaxWidth(),
) { ) {
Text( Text(
text = stringResource(R.string.done), text = stringResource(R.string.done),
@ -453,7 +455,8 @@ fun ForYouScreenTopicSelection(
ForYouScreen( ForYouScreen(
isSyncing = false, isSyncing = false,
onboardingUiState = OnboardingUiState.Shown( onboardingUiState = OnboardingUiState.Shown(
topics = userNewsResources.flatMap { news -> news.followableTopics }, topics = userNewsResources.flatMap { news -> news.followableTopics }
.distinctBy { it.topic.id },
), ),
feedState = NewsFeedUiState.Success( feedState = NewsFeedUiState.Success(
feed = userNewsResources, feed = userNewsResources,

@ -20,7 +20,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery import com.google.samples.apps.nowinandroid.core.data.repository.NewsResourceQuery
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
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.domain.GetFollowableTopicsUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase import com.google.samples.apps.nowinandroid.core.domain.GetUserNewsResourcesUseCase
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
@ -41,7 +41,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ForYouViewModel @Inject constructor( class ForYouViewModel @Inject constructor(
syncStatusMonitor: SyncStatusMonitor, syncManager: SyncManager,
private val userDataRepository: UserDataRepository, private val userDataRepository: UserDataRepository,
getUserNewsResources: GetUserNewsResourcesUseCase, getUserNewsResources: GetUserNewsResourcesUseCase,
getFollowableTopics: GetFollowableTopicsUseCase, getFollowableTopics: GetFollowableTopicsUseCase,
@ -50,7 +50,7 @@ class ForYouViewModel @Inject constructor(
private val shouldShowOnboarding: Flow<Boolean> = private val shouldShowOnboarding: Flow<Boolean> =
userDataRepository.userData.map { !it.shouldHideOnboarding } userDataRepository.userData.map { !it.shouldHideOnboarding }
val isSyncing = syncStatusMonitor.isSyncing val isSyncing = syncManager.isSyncing
.stateIn( .stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000), started = SharingStarted.WhileSubscribed(5_000),

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

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

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

@ -93,19 +93,20 @@ androidx-test-ext = { group = "androidx.test.ext", name = "junit-ktx", version.r
androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTestRules" } androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTestRules" }
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" } androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" }
androidx-test-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "androidxUiAutomator" } 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-window-manager = { module = "androidx.window:window", version.ref = "androidxWindowManager" }
androidx-work-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "androidxWork" } 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" } 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 = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
coil-kt-compose = { group = "io.coil-kt", name = "coil-compose", 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" } 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-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx"} firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx" }
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx"} firebase-cloud-messaging = { group = "com.google.firebase", name = "firebase-messaging-ktx" }
firebase-crashlytics-gradle = { group = "com.google.firebase", name="firebase-crashlytics-gradle", version.ref = "firebaseCrashlyticsPlugin"} firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx" }
firebase-performance = { group = "com.google.firebase", name = "firebase-perf-ktx"} firebase-crashlytics-gradle = { group = "com.google.firebase", name = "firebase-crashlytics-gradle", version.ref = "firebaseCrashlyticsPlugin" }
firebase-performance-gradle = { group = "com.google.firebase", name = "perf-plugin", version.ref = "firebasePerfPlugin"} 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 = { 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-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" } hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
@ -137,9 +138,9 @@ ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devto
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin"} firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsPlugin" }
firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerfPlugin"} firebase-perf = { id = "com.google.firebase.firebase-perf", version.ref = "firebasePerfPlugin" }
gms = { id = "com.google.gms.google-services", version.ref = "gmsPlugin"} gms = { id = "com.google.gms.google-services", version.ref = "gmsPlugin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", 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:ui")
include(":core:testing") include(":core:testing")
include(":core:analytics") include(":core:analytics")
include(":core:notifications")
include(":feature:foryou") include(":feature:foryou")
include(":feature:interests") include(":feature:interests")

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

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

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

@ -0,0 +1,38 @@
<?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>
<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"
tools:node="remove" />
</application>
</manifest>

@ -29,7 +29,13 @@
android:value="androidx.startup" android:value="androidx.startup"
tools:node="remove" /> tools:node="remove" />
</provider> </provider>
<service
android:name=".services.SyncNotificationsService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application> </application>
</manifest> </manifest>

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

@ -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 android.content.Context
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.work.ExistingWorkPolicy
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkInfo.State import androidx.work.WorkInfo.State
import androidx.work.WorkManager 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.initializers.SyncWorkName
import com.google.samples.apps.nowinandroid.sync.workers.SyncWorker
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import javax.inject.Inject import javax.inject.Inject
/** /**
* [SyncStatusMonitor] backed by [WorkInfo] from [WorkManager] * [SyncManager] backed by [WorkInfo] from [WorkManager]
*/ */
class WorkManagerSyncStatusMonitor @Inject constructor( class WorkManagerSyncManager @Inject constructor(
@ApplicationContext context: Context, @ApplicationContext private val context: Context,
) : SyncStatusMonitor { ) : SyncManager {
override val isSyncing: Flow<Boolean> = override val isSyncing: Flow<Boolean> =
WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData(SyncWorkName) WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData(SyncWorkName)
.map(MutableList<WorkInfo>::anyRunning) .map(MutableList<WorkInfo>::anyRunning)
.asFlow() .asFlow()
.conflate() .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 } private val List<WorkInfo>.anyRunning get() = any { it.state == State.RUNNING }
Loading…
Cancel
Save