Merge branch 'github/main'

pull/836/head
Automerger 2 years ago
commit f00009e3ba

@ -34,6 +34,7 @@ data class AnalyticsEvent(
class Types { class Types {
companion object { companion object {
const val SCREEN_VIEW = "screen_view" // (extras: SCREEN_NAME) const val SCREEN_VIEW = "screen_view" // (extras: SCREEN_NAME)
const val VIEW_SEARCH_RESULTS = "view_search_results" // (extras: SEARCH_TERM)
} }
} }
@ -53,6 +54,7 @@ data class AnalyticsEvent(
class ParamKeys { class ParamKeys {
companion object { companion object {
const val SCREEN_NAME = "screen_name" const val SCREEN_NAME = "screen_name"
const val SEARCH_TERM = "search_term"
} }
} }
} }

@ -63,10 +63,12 @@ class OfflineFirstNewsRepository @Inject constructor(
) )
.map { it.map(PopulatedNewsResource::asExternalModel) } .map { it.map(PopulatedNewsResource::asExternalModel) }
override suspend fun syncWith(synchronizer: Synchronizer) = override suspend fun syncWith(synchronizer: Synchronizer): Boolean {
synchronizer.changeListSync( var isFirstSync = false
return synchronizer.changeListSync(
versionReader = ChangeListVersions::newsResourceVersion, versionReader = ChangeListVersions::newsResourceVersion,
changeListFetcher = { currentVersion -> changeListFetcher = { currentVersion ->
isFirstSync = currentVersion <= 0
network.getNewsResourceChangeList(after = currentVersion) network.getNewsResourceChangeList(after = currentVersion)
}, },
versionUpdater = { latestVersion -> versionUpdater = { latestVersion ->
@ -94,6 +96,12 @@ class OfflineFirstNewsRepository @Inject constructor(
else -> emptySet() else -> emptySet()
} }
if (isFirstSync) {
// When we first retrieve news, mark everything viewed, so that we aren't
// overwhelmed with all historical news.
niaPreferencesDataSource.setNewsResourcesViewed(changedIds, true)
}
// Obtain the news resources which have changed from the network and upsert them locally // Obtain the news resources which have changed from the network and upsert them locally
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)
@ -138,3 +146,4 @@ class OfflineFirstNewsRepository @Inject constructor(
}, },
) )
} }
}

@ -37,6 +37,18 @@ class ConnectivityManagerNetworkMonitor @Inject constructor(
) : NetworkMonitor { ) : NetworkMonitor {
override val isOnline: Flow<Boolean> = callbackFlow { override val isOnline: Flow<Boolean> = callbackFlow {
val connectivityManager = context.getSystemService<ConnectivityManager>() val connectivityManager = context.getSystemService<ConnectivityManager>()
if (connectivityManager == null) {
channel.trySend(false)
channel.close()
return@callbackFlow
}
/**
* Sends the latest connectivity status to the underlying channel.
*/
fun update() {
channel.trySend(connectivityManager.isCurrentlyConnected())
}
/** /**
* The callback's methods are invoked on changes to *any* network, not just the active * The callback's methods are invoked on changes to *any* network, not just the active
@ -44,47 +56,37 @@ class ConnectivityManagerNetworkMonitor @Inject constructor(
* ConnectivityManager. * ConnectivityManager.
*/ */
val callback = object : NetworkCallback() { val callback = object : NetworkCallback() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) = update()
channel.trySend(connectivityManager.isCurrentlyConnected())
}
override fun onLost(network: Network) { override fun onLost(network: Network) = update()
channel.trySend(connectivityManager.isCurrentlyConnected())
}
override fun onCapabilitiesChanged( override fun onCapabilitiesChanged(
network: Network, network: Network,
networkCapabilities: NetworkCapabilities, networkCapabilities: NetworkCapabilities,
) { ) = update()
channel.trySend(connectivityManager.isCurrentlyConnected())
}
} }
connectivityManager?.registerNetworkCallback( connectivityManager.registerNetworkCallback(
Builder() Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build(), .build(),
callback, callback,
) )
channel.trySend(connectivityManager.isCurrentlyConnected()) update()
awaitClose { awaitClose {
connectivityManager?.unregisterNetworkCallback(callback) connectivityManager.unregisterNetworkCallback(callback)
} }
} }
.conflate() .conflate()
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private fun ConnectivityManager?.isCurrentlyConnected() = when (this) { private fun ConnectivityManager.isCurrentlyConnected() = when {
null -> false
else -> when {
VERSION.SDK_INT >= VERSION_CODES.M -> VERSION.SDK_INT >= VERSION_CODES.M ->
activeNetwork activeNetwork
?.let(::getNetworkCapabilities) ?.let(::getNetworkCapabilities)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
?: false else -> activeNetworkInfo?.isConnected
else -> activeNetworkInfo?.isConnected ?: false } ?: false
}
}
} }

@ -288,6 +288,33 @@ class OfflineFirstNewsRepositoryTest {
) )
} }
@Test
fun offlineFirstNewsRepository_sync_marks_as_read_on_first_run() =
testScope.runTest {
subject.syncWith(synchronizer)
assertEquals(
network.getNewsResources().map { it.id }.toSet(),
niaPreferencesDataSource.userData.first().viewedNewsResources,
)
}
@Test
fun offlineFirstNewsRepository_sync_does_not_mark_as_read_on_subsequent_run() =
testScope.runTest {
// Pretend that we already have up to change list 7
synchronizer.updateChangeListVersions {
copy(newsResourceVersion = 7)
}
subject.syncWith(synchronizer)
assertEquals(
emptySet(),
niaPreferencesDataSource.userData.first().viewedNewsResources,
)
}
@Test @Test
fun offlineFirstNewsRepository_sends_notifications_for_newly_synced_news_that_is_followed() = fun offlineFirstNewsRepository_sends_notifications_for_newly_synced_news_that_is_followed() =
testScope.runTest { testScope.runTest {

@ -139,12 +139,18 @@ class NiaPreferencesDataSource @Inject constructor(
} }
suspend fun setNewsResourceViewed(newsResourceId: String, viewed: Boolean) { suspend fun setNewsResourceViewed(newsResourceId: String, viewed: Boolean) {
setNewsResourcesViewed(listOf(newsResourceId), viewed)
}
suspend fun setNewsResourcesViewed(newsResourceIds: List<String>, viewed: Boolean) {
userPreferences.updateData { userPreferences.updateData {
it.copy { it.copy {
newsResourceIds.forEach {
if (viewed) { if (viewed) {
viewedNewsResourceIds.put(newsResourceId, true) viewedNewsResourceIds.put(it, true)
} else { } else {
viewedNewsResourceIds.remove(newsResourceId) viewedNewsResourceIds.remove(it)
}
} }
} }
} }

@ -19,6 +19,9 @@ package com.google.samples.apps.nowinandroid.feature.search
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsEvent
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsEvent.Param
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
import com.google.samples.apps.nowinandroid.core.data.repository.RecentSearchRepository import com.google.samples.apps.nowinandroid.core.data.repository.RecentSearchRepository
import com.google.samples.apps.nowinandroid.core.domain.GetRecentSearchQueriesUseCase import com.google.samples.apps.nowinandroid.core.domain.GetRecentSearchQueriesUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsCountUseCase import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsCountUseCase
@ -42,6 +45,7 @@ class SearchViewModel @Inject constructor(
recentSearchQueriesUseCase: GetRecentSearchQueriesUseCase, recentSearchQueriesUseCase: GetRecentSearchQueriesUseCase,
private val recentSearchRepository: RecentSearchRepository, private val recentSearchRepository: RecentSearchRepository,
private val savedStateHandle: SavedStateHandle, private val savedStateHandle: SavedStateHandle,
private val analyticsHelper: AnalyticsHelper,
) : ViewModel() { ) : ViewModel() {
val searchQuery = savedStateHandle.getStateFlow(SEARCH_QUERY, "") val searchQuery = savedStateHandle.getStateFlow(SEARCH_QUERY, "")
@ -105,6 +109,14 @@ class SearchViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
recentSearchRepository.insertOrReplaceRecentSearch(query) recentSearchRepository.insertOrReplaceRecentSearch(query)
} }
analyticsHelper.logEvent(
AnalyticsEvent(
type = SEARCH_QUERY,
extras = listOf(
Param(SEARCH_QUERY, query),
),
),
)
} }
fun clearRecentSearches() { fun clearRecentSearches() {

@ -17,6 +17,7 @@
package com.google.samples.apps.nowinandroid.feature.search package com.google.samples.apps.nowinandroid.feature.search
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import com.google.samples.apps.nowinandroid.core.analytics.NoOpAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.domain.GetRecentSearchQueriesUseCase import com.google.samples.apps.nowinandroid.core.domain.GetRecentSearchQueriesUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsCountUseCase import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsCountUseCase
import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsUseCase import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsUseCase
@ -68,6 +69,7 @@ class SearchViewModelTest {
recentSearchQueriesUseCase = getRecentQueryUseCase, recentSearchQueriesUseCase = getRecentQueryUseCase,
savedStateHandle = SavedStateHandle(), savedStateHandle = SavedStateHandle(),
recentSearchRepository = recentSearchRepository, recentSearchRepository = recentSearchRepository,
analyticsHelper = NoOpAnalyticsHelper(),
) )
} }

Loading…
Cancel
Save