Merge branch 'main' into jvm-alt

pull/1837/head
Simon Marquis 2 years ago committed by GitHub
commit e3a0f119c3

@ -45,13 +45,40 @@ jobs:
- name: Check spotless
run: ./gradlew spotlessCheck --init-script gradle/init.gradle.kts --no-configuration-cache
- name: Check Dependency Guard
id: dependencyguard_verify
continue-on-error: true
run: ./gradlew dependencyGuard
- name: Prevent updating Dependency Guard baselines if this is a fork
id: checkfork_dependencyguard
continue-on-error: false
if: steps.dependencyguard_verify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository
run: |
echo "::error::Dependency Guard failed, please update baselines with: ./gradlew dependencyGuardBaseline" && exit 1
# Runs if previous job failed
- name: Generate new Dependency Guard baselines if verification failed and it's a PR
id: dependencyguard_baseline
if: steps.dependencyguard_verify.outcome == 'failure' && github.event_name == 'pull_request'
run: |
./gradlew dependencyGuardBaseline
- name: Push new Dependency Guard baselines if available
uses: stefanzweifel/git-auto-commit-action@v5
if: steps.dependencyguard_baseline.outcome == 'success'
with:
file_pattern: '**/dependencies/*.txt'
disable_globbing: true
commit_message: "🤖 Updates baselines for Dependency Guard"
- name: Run all local screenshot tests (Roborazzi)
id: screenshotsverify
continue-on-error: true
run: ./gradlew verifyRoborazziDemoDebug
- name: Prevent pushing new screenshots if this is a fork
id: checkfork
id: checkfork_screenshots
continue-on-error: false
if: steps.screenshotsverify.outcome == 'failure' && github.event.pull_request.head.repo.full_name != github.repository
run: |
@ -117,13 +144,20 @@ jobs:
run: ./gradlew :app:checkProdReleaseBadging
androidTest:
runs-on: macOS-latest # enables hardware acceleration in the virtual machine
runs-on: ubuntu-latest
timeout-minutes: 55
strategy:
matrix:
api-level: [26, 30]
steps:
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
ls /dev/kvm
- name: Checkout
uses: actions/checkout@v4

@ -7,10 +7,17 @@ on:
jobs:
build:
runs-on: macos-latest
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
ls /dev/kvm
- name: Checkout
uses: actions/checkout@v4

@ -112,8 +112,8 @@ androidx.vectordrawable:vectordrawable:1.1.0
androidx.versionedparcelable:versionedparcelable:1.1.1
androidx.viewpager:viewpager:1.0.0
androidx.window:window:1.0.0
androidx.work:work-runtime-ktx:2.9.0-rc01
androidx.work:work-runtime:2.9.0-rc01
androidx.work:work-runtime-ktx:2.9.0
androidx.work:work-runtime:2.9.0
com.caverock:androidsvg-aar:1.4
com.google.accompanist:accompanist-drawablepainter:0.32.0
com.google.accompanist:accompanist-permissions:0.32.0

@ -90,7 +90,7 @@ class NavigationTest {
lateinit var topicsRepository: TopicsRepository
private fun AndroidComposeTestRule<*, *>.stringResource(@StringRes resId: Int) =
ReadOnlyProperty<Any?, String> { _, _ -> activity.getString(resId) }
ReadOnlyProperty<Any, String> { _, _ -> activity.getString(resId) }
// The strings used for matching in these tests
private val navigateUp by composeTestRule.stringResource(FeatureForyouR.string.feature_foryou_navigate_up)

@ -0,0 +1,70 @@
/*
* 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.util
import android.util.Log
import androidx.profileinstaller.ProfileVerifier
import com.google.samples.apps.nowinandroid.core.network.di.ApplicationScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* Logs the app's Baseline Profile Compilation Status using [ProfileVerifier].
*
* When delivering through Google Play, the baseline profile is compiled during installation.
* In this case you will see the correct state logged without any further action necessary.
* To verify baseline profile installation locally, you need to manually trigger baseline
* profile installation.
*
* For immediate compilation, call:
* ```bash
* adb shell cmd package compile -f -m speed-profile com.example.macrobenchmark.target
* ```
* You can also trigger background optimizations:
* ```bash
* adb shell pm bg-dexopt-job
* ```
* Both jobs run asynchronously and might take some time complete.
*
* To see quick turnaround of the ProfileVerifier, we recommend using `speed-profile`.
* If you don't do either of these steps, you might only see the profile status reported as
* "enqueued for compilation" when running the sample locally.
*
* @see androidx.profileinstaller.ProfileVerifier.CompilationStatus.ResultCode
*/
class ProfileVerifierLogger @Inject constructor(
@ApplicationScope private val scope: CoroutineScope,
) {
companion object {
private const val TAG = "ProfileInstaller"
}
operator fun invoke() = scope.launch {
val status = ProfileVerifier.getCompilationStatusAsync().await()
Log.d(TAG, "Status code: ${status.profileInstallResultCode}")
Log.d(
TAG,
when {
status.isCompiledWithProfile -> "App compiled with profile"
status.hasProfileEnqueuedForCompilation() -> "Profile enqueued for compilation"
else -> "Profile not compiled nor enqueued"
},
)
}
}

@ -17,7 +17,6 @@
package com.google.samples.apps.nowinandroid
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
@ -37,7 +36,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.metrics.performance.JankStats
import androidx.profileinstaller.ProfileVerifier
import com.google.samples.apps.nowinandroid.MainActivityUiState.Loading
import com.google.samples.apps.nowinandroid.MainActivityUiState.Success
import com.google.samples.apps.nowinandroid.core.analytics.AnalyticsHelper
@ -49,12 +47,9 @@ import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
import com.google.samples.apps.nowinandroid.ui.NiaApp
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
private const val TAG = "MainActivity"
@ -90,9 +85,7 @@ class MainActivity : ComponentActivity() {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState
.onEach {
uiState = it
}
.onEach { uiState = it }
.collect()
}
}
@ -152,48 +145,12 @@ class MainActivity : ComponentActivity() {
override fun onResume() {
super.onResume()
lazyStats.get().isTrackingEnabled = true
lifecycleScope.launch {
logCompilationStatus()
}
}
override fun onPause() {
super.onPause()
lazyStats.get().isTrackingEnabled = false
}
/**
* Logs the app's Baseline Profile Compilation Status using [ProfileVerifier].
*/
private suspend fun logCompilationStatus() {
/*
When delivering through Google Play, the baseline profile is compiled during installation.
In this case you will see the correct state logged without any further action necessary.
To verify baseline profile installation locally, you need to manually trigger baseline
profile installation.
For immediate compilation, call:
`adb shell cmd package compile -f -m speed-profile com.example.macrobenchmark.target`
You can also trigger background optimizations:
`adb shell pm bg-dexopt-job`
Both jobs run asynchronously and might take some time complete.
To see quick turnaround of the ProfileVerifier, we recommend using `speed-profile`.
If you don't do either of these steps, you might only see the profile status reported as
"enqueued for compilation" when running the sample locally.
*/
withContext(Dispatchers.IO) {
val status = ProfileVerifier.getCompilationStatusAsync().await()
Log.d(TAG, "ProfileInstaller status code: ${status.profileInstallResultCode}")
Log.d(
TAG,
when {
status.isCompiledWithProfile -> "ProfileInstaller: is compiled with profile"
status.hasProfileEnqueuedForCompilation() ->
"ProfileInstaller: Enqueued for compilation"
else -> "Profile not compiled or enqueued"
},
)
}
}
}
/**

@ -20,6 +20,7 @@ import android.app.Application
import coil.ImageLoader
import coil.ImageLoaderFactory
import com.google.samples.apps.nowinandroid.sync.initializers.Sync
import com.google.samples.apps.nowinandroid.util.ProfileVerifierLogger
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
import javax.inject.Provider
@ -32,10 +33,14 @@ class NiaApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var imageLoader: Provider<ImageLoader>
@Inject
lateinit var profileVerifierLogger: ProfileVerifierLogger
override fun onCreate() {
super.onCreate()
// Initialize Sync; the system responsible for keeping data in the app up to date.
Sync.initialize(context = this)
profileVerifierLogger()
}
override fun newImageLoader(): ImageLoader = imageLoader.get()

@ -20,6 +20,7 @@ import android.app.Activity
import android.util.Log
import android.view.Window
import androidx.metrics.performance.JankStats
import androidx.metrics.performance.JankStats.OnFrameListener
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -29,26 +30,20 @@ import dagger.hilt.android.components.ActivityComponent
@InstallIn(ActivityComponent::class)
object JankStatsModule {
@Provides
fun providesOnFrameListener(): JankStats.OnFrameListener {
return JankStats.OnFrameListener { frameData ->
// Make sure to only log janky frames.
if (frameData.isJank) {
// We're currently logging this but would better report it to a backend.
Log.v("NiA Jank", frameData.toString())
}
fun providesOnFrameListener(): OnFrameListener = OnFrameListener { frameData ->
// Make sure to only log janky frames.
if (frameData.isJank) {
// We're currently logging this but would better report it to a backend.
Log.v("NiA Jank", frameData.toString())
}
}
@Provides
fun providesWindow(activity: Activity): Window {
return activity.window
}
fun providesWindow(activity: Activity): Window = activity.window
@Provides
fun providesJankStats(
window: Window,
frameListener: JankStats.OnFrameListener,
): JankStats {
return JankStats.createAndTrack(window, frameListener)
}
frameListener: OnFrameListener,
): JankStats = JankStats.createAndTrack(window, frameListener)
}

@ -97,9 +97,7 @@ fun NiaApp(
) {
val shouldShowGradientBackground =
appState.currentTopLevelDestination == TopLevelDestination.FOR_YOU
var showSettingsDialog by rememberSaveable {
mutableStateOf(false)
}
var showSettingsDialog by rememberSaveable { mutableStateOf(false) }
NiaBackground {
NiaGradientBackground(

@ -164,9 +164,7 @@ class NiaAppState(
}
}
fun navigateToSearch() {
navController.navigateToSearch()
}
fun navigateToSearch() = navController.navigateToSearch()
}
/**

@ -20,10 +20,10 @@
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M65.08,84.13C64.01,84.13 63.13,83.26 63.13,82.18C63.13,81.11 64,80.24 65.08,80.24C66.15,80.24 67.02,81.11 67.02,82.18C67.02,83.26 66.15,84.13 65.08,84.13ZM43.6,84.13C42.53,84.13 41.65,83.26 41.65,82.18C41.65,81.11 42.52,80.24 43.6,80.24C44.66,80.24 45.54,81.11 45.54,82.18C45.54,83.26 44.67,84.13 43.6,84.13ZM65.77,72.44L69.66,65.73C69.88,65.35 69.74,64.85 69.36,64.63C68.97,64.41 68.48,64.54 68.25,64.93L64.32,71.73C61.31,70.36 57.94,69.59 54.33,69.59C50.73,69.59 47.35,70.36 44.34,71.73L40.41,64.93C40.19,64.54 39.69,64.41 39.31,64.63C38.92,64.85 38.79,65.35 39.01,65.73L42.89,72.44C36.22,76.07 31.67,82.81 31,90.77H77.67C77,82.8 72.44,76.06 65.77,72.44Z"
android:pathData="M65.08,84.13a1.94,1.94 0,1 1,-0.01 -3.9,1.94 1.94,0 0,1 0.01,3.9ZM43.6,84.13a1.94,1.94 0,1 1,-0.01 -3.9,1.94 1.94,0 0,1 0.01,3.9ZM65.77,72.44 L69.66,65.73a0.81,0.81 0,0 0,-0.3 -1.1,0.82 0.82,0 0,0 -1.11,0.3l-3.93,6.8a24,24 0,0 0,-9.99 -2.14c-3.6,0 -6.98,0.77 -9.99,2.14l-3.93,-6.8a0.8,0.8 0,1 0,-1.4 0.8l3.88,6.71A22.91,22.91 0,0 0,31 90.77h46.67a22.9,22.9 0,0 0,-11.9 -18.33Z"
android:fillColor="@color/ic_launcher_foreground_tint"/>
<path
android:pathData="M46.57,35H46.57C46.1,35 45.72,35.38 45.72,35.85L45.72,43.15H44.19C43.35,43.15 42.67,43.83 42.67,44.68C42.67,45.52 43.35,46.2 44.19,46.2H45.72V43.15H47.42C48.17,43.15 48.78,42.54 48.78,41.79L48.78,37.72H49.97C50.43,37.72 50.81,37.34 50.81,36.87V35.85C50.81,35.38 50.43,35 49.97,35H47.42H46.57ZM46.57,54.35H46.57H47.42H49.97C50.43,54.35 50.81,53.97 50.81,53.5V52.48C50.81,52.02 50.43,51.64 49.97,51.64H48.78L48.78,47.56C48.78,46.81 48.17,46.2 47.42,46.2H45.72L45.72,53.5C45.72,53.97 46.1,54.35 46.57,54.35ZM61.54,35H61.54C62.01,35 62.39,35.38 62.39,35.85V43.15H63.92C64.76,43.15 65.44,43.83 65.44,44.68C65.44,45.52 64.76,46.2 63.92,46.2H62.39V43.15H60.69C59.94,43.15 59.33,42.54 59.33,41.79V37.72H58.15C57.68,37.72 57.3,37.34 57.3,36.87V35.85C57.3,35.38 57.68,35 58.15,35H60.69H61.54ZM61.54,54.35H61.54H60.69H58.15C57.68,54.35 57.3,53.97 57.3,53.5V52.48C57.3,52.02 57.68,51.64 58.15,51.64H59.33V47.56C59.33,46.81 59.94,46.2 60.69,46.2H62.39V53.5C62.39,53.97 62.01,54.35 61.54,54.35Z"
android:pathData="M46.57,35a0.85,0.85 0,0 0,-0.85 0.85v7.3h-1.53a1.52,1.52 0,0 0,0 3.05h1.53v-3.05h1.7c0.75,0 1.36,-0.61 1.36,-1.36v-4.07h1.19c0.46,0 0.84,-0.38 0.84,-0.85v-1.02a0.85,0.85 0,0 0,-0.84 -0.85h-3.4ZM46.57,54.35h3.4c0.46,0 0.84,-0.38 0.84,-0.85v-1.02a0.85,0.85 0,0 0,-0.84 -0.84h-1.19v-4.08c0,-0.75 -0.61,-1.36 -1.36,-1.36h-1.7v7.3c0,0.47 0.38,0.85 0.85,0.85ZM61.54,35c0.47,0 0.85,0.38 0.85,0.85v7.3h1.53a1.52,1.52 0,0 1,0 3.05h-1.53v-3.05h-1.7c-0.75,0 -1.36,-0.61 -1.36,-1.36v-4.07h-1.18a0.85,0.85 0,0 1,-0.85 -0.85v-1.02c0,-0.47 0.38,-0.85 0.85,-0.85h3.39ZM61.54,54.35h-3.39a0.85,0.85 0,0 1,-0.85 -0.85v-1.02c0,-0.46 0.38,-0.84 0.85,-0.84h1.18v-4.08c0,-0.75 0.61,-1.36 1.36,-1.36h1.7v7.3c0,0.47 -0.38,0.85 -0.85,0.85Z"
android:fillColor="@color/ic_launcher_foreground_tint"
android:fillType="evenOdd"/>
</vector>

@ -24,11 +24,11 @@
android:pathData="M0,0h108v108h-108z"
android:fillColor="@color/ic_launcher_background_tint"/>
<path
android:pathData="M65.08,84.13C64.01,84.13 63.13,83.26 63.13,82.18C63.13,81.11 64,80.24 65.08,80.24C66.15,80.24 67.02,81.11 67.02,82.18C67.02,83.26 66.15,84.13 65.08,84.13ZM43.6,84.13C42.53,84.13 41.65,83.26 41.65,82.18C41.65,81.11 42.52,80.24 43.6,80.24C44.66,80.24 45.54,81.11 45.54,82.18C45.54,83.26 44.67,84.13 43.6,84.13ZM65.77,72.44L69.66,65.73C69.88,65.35 69.74,64.85 69.36,64.63C68.97,64.41 68.48,64.54 68.25,64.93L64.32,71.73C61.31,70.36 57.94,69.59 54.33,69.59C50.73,69.59 47.35,70.36 44.34,71.73L40.41,64.93C40.19,64.54 39.69,64.41 39.31,64.63C38.92,64.85 38.79,65.35 39.01,65.73L42.89,72.44C36.22,76.07 31.67,82.81 31,90.77H77.67C77,82.8 72.44,76.06 65.77,72.44Z"
android:pathData="M65.08,84.13a1.94,1.94 0,1 1,-0.01 -3.9,1.94 1.94,0 0,1 0.01,3.9ZM43.6,84.13a1.94,1.94 0,1 1,-0.01 -3.9,1.94 1.94,0 0,1 0.01,3.9ZM65.77,72.44 L69.66,65.73a0.81,0.81 0,0 0,-0.3 -1.1,0.82 0.82,0 0,0 -1.11,0.3l-3.93,6.8a24,24 0,0 0,-9.99 -2.14c-3.6,0 -6.98,0.77 -9.99,2.14l-3.93,-6.8a0.8,0.8 0,1 0,-1.4 0.8l3.88,6.71A22.91,22.91 0,0 0,31 90.77h46.67a22.9,22.9 0,0 0,-11.9 -18.33Z"
android:fillColor="@color/ic_launcher_foreground_tint"/>
<path
android:pathData="M46.57,35H46.57C46.1,35 45.72,35.38 45.72,35.85L45.72,43.15H44.19C43.35,43.15 42.67,43.83 42.67,44.68C42.67,45.52 43.35,46.2 44.19,46.2H45.72V43.15H47.42C48.17,43.15 48.78,42.54 48.78,41.79L48.78,37.72H49.97C50.43,37.72 50.81,37.34 50.81,36.87V35.85C50.81,35.38 50.43,35 49.97,35H47.42H46.57ZM46.57,54.35H46.57H47.42H49.97C50.43,54.35 50.81,53.97 50.81,53.5V52.48C50.81,52.02 50.43,51.64 49.97,51.64H48.78L48.78,47.56C48.78,46.81 48.17,46.2 47.42,46.2H45.72L45.72,53.5C45.72,53.97 46.1,54.35 46.57,54.35ZM61.54,35H61.54C62.01,35 62.39,35.38 62.39,35.85V43.15H63.92C64.76,43.15 65.44,43.83 65.44,44.68C65.44,45.52 64.76,46.2 63.92,46.2H62.39V43.15H60.69C59.94,43.15 59.33,42.54 59.33,41.79V37.72H58.15C57.68,37.72 57.3,37.34 57.3,36.87V35.85C57.3,35.38 57.68,35 58.15,35H60.69H61.54ZM61.54,54.35H61.54H60.69H58.15C57.68,54.35 57.3,53.97 57.3,53.5V52.48C57.3,52.02 57.68,51.64 58.15,51.64H59.33V47.56C59.33,46.81 59.94,46.2 60.69,46.2H62.39V53.5C62.39,53.97 62.01,54.35 61.54,54.35Z"
android:pathData="M46.57,35a0.85,0.85 0,0 0,-0.85 0.85v7.3h-1.53a1.52,1.52 0,0 0,0 3.05h1.53v-3.05h1.7c0.75,0 1.36,-0.61 1.36,-1.36v-4.07h1.19c0.46,0 0.84,-0.38 0.84,-0.85v-1.02a0.85,0.85 0,0 0,-0.84 -0.85h-3.4ZM46.57,54.35h3.4c0.46,0 0.84,-0.38 0.84,-0.85v-1.02a0.85,0.85 0,0 0,-0.84 -0.84h-1.19v-4.08c0,-0.75 -0.61,-1.36 -1.36,-1.36h-1.7v7.3c0,0.47 0.38,0.85 0.85,0.85ZM61.54,35c0.47,0 0.85,0.38 0.85,0.85v7.3h1.53a1.52,1.52 0,0 1,0 3.05h-1.53v-3.05h-1.7c-0.75,0 -1.36,-0.61 -1.36,-1.36v-4.07h-1.18a0.85,0.85 0,0 1,-0.85 -0.85v-1.02c0,-0.47 0.38,-0.85 0.85,-0.85h3.39ZM61.54,54.35h-3.39a0.85,0.85 0,0 1,-0.85 -0.85v-1.02c0,-0.46 0.38,-0.84 0.85,-0.84h1.18v-4.08c0,-0.75 0.61,-1.36 1.36,-1.36h1.7v7.3c0,0.47 -0.38,0.85 -0.85,0.85Z"
android:fillColor="@color/ic_launcher_foreground_tint"
android:fillType="evenOdd"/>
</vector>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

@ -29,15 +29,11 @@ import androidx.test.uiautomator.HasChildrenOp.EXACTLY
fun untilHasChildren(
childCount: Int = 1,
op: HasChildrenOp = AT_LEAST,
): UiObject2Condition<Boolean> {
return object : UiObject2Condition<Boolean>() {
override fun apply(element: UiObject2): Boolean {
return when (op) {
AT_LEAST -> element.childCount >= childCount
EXACTLY -> element.childCount == childCount
AT_MOST -> element.childCount <= childCount
}
}
): UiObject2Condition<Boolean> = object : UiObject2Condition<Boolean>() {
override fun apply(element: UiObject2): Boolean = when (op) {
AT_LEAST -> element.childCount >= childCount
EXACTLY -> element.childCount == childCount
AT_MOST -> element.childCount <= childCount
}
}

@ -16,6 +16,7 @@
package com.google.samples.apps.nowinandroid.baselineprofile
import androidx.benchmark.macro.MacrobenchmarkScope
import androidx.benchmark.macro.junit4.BaselineProfileRule
import com.google.samples.apps.nowinandroid.PACKAGE_NAME
import com.google.samples.apps.nowinandroid.startActivityAndAllowNotifications
@ -30,11 +31,9 @@ class StartupBaselineProfile {
@get:Rule val baselineProfileRule = BaselineProfileRule()
@Test
fun generate() =
baselineProfileRule.collect(
PACKAGE_NAME,
includeInStartupProfile = true,
) {
startActivityAndAllowNotifications()
}
fun generate() = baselineProfileRule.collect(
PACKAGE_NAME,
includeInStartupProfile = true,
profileBlock = MacrobenchmarkScope::startActivityAndAllowNotifications,
)
}

@ -86,14 +86,12 @@ internal abstract class PrintApkLocationTask : DefaultTask() {
fun taskAction() {
val hasFiles = sources.orNull?.any { directory ->
directory.asFileTree.files.any {
it.isFile && it.parentFile.path.contains("build${File.separator}generated").not()
it.isFile && "build${File.separator}generated" !in it.parentFile.path
}
} ?: throw RuntimeException("Cannot check androidTest sources")
// Don't print APK location if there are no androidTest source files
if (!hasFiles) {
return
}
if (!hasFiles) return
val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get())
?: throw RuntimeException("Cannot load APKs")

@ -23,15 +23,10 @@ import kotlinx.coroutines.flow.onStart
sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Error(val exception: Throwable? = null) : Result<Nothing>
data class Error(val exception: Throwable) : Result<Nothing>
data object Loading : Result<Nothing>
}
fun <T> Flow<T>.asResult(): Flow<Result<T>> {
return this
.map<T, Result<T>> {
Result.Success(it)
}
.onStart { emit(Result.Loading) }
.catch { emit(Result.Error(it)) }
}
fun <T> Flow<T>.asResult(): Flow<Result<T>> = map<T, Result<T>> { Result.Success(it) }
.onStart { emit(Result.Loading) }
.catch { emit(Result.Error(it)) }

@ -39,9 +39,7 @@ internal class DefaultRecentSearchRepository @Inject constructor(
override fun getRecentSearchQueries(limit: Int): Flow<List<RecentSearchQuery>> =
recentSearchQueryDao.getRecentSearchQueryEntities(limit).map { searchQueries ->
searchQueries.map {
it.asExternalModel()
}
searchQueries.map { it.asExternalModel() }
}
override suspend fun clearRecentSearches() = recentSearchQueryDao.clearRecentSearchQueries()

@ -26,10 +26,10 @@ import javax.inject.Inject
* Fake implementation of the [RecentSearchRepository]
*/
class FakeRecentSearchRepository @Inject constructor() : RecentSearchRepository {
override suspend fun insertOrReplaceRecentSearch(searchQuery: String) { /* no-op */ }
override suspend fun insertOrReplaceRecentSearch(searchQuery: String) = Unit
override fun getRecentSearchQueries(limit: Int): Flow<List<RecentSearchQuery>> =
flowOf(emptyList())
override suspend fun clearRecentSearches() { /* no-op */ }
override suspend fun clearRecentSearches() = Unit
}

@ -27,7 +27,7 @@ import javax.inject.Inject
*/
class FakeSearchContentsRepository @Inject constructor() : SearchContentsRepository {
override suspend fun populateFtsData() { /* no-op */ }
override suspend fun populateFtsData() = Unit
override fun searchContents(searchQuery: String): Flow<SearchResult> = flowOf()
override fun getSearchContentsCount(): Flow<Int> = flowOf(1)
}

@ -55,9 +55,8 @@ class FakeTopicsRepository @Inject constructor(
)
}.flowOn(ioDispatcher)
override fun getTopic(id: String): Flow<Topic> {
return getTopics().map { it.first { topic -> topic.id == id } }
}
override fun getTopic(id: String): Flow<Topic> = getTopics()
.map { it.first { topic -> topic.id == id } }
override suspend fun syncWith(synchronizer: Synchronizer) = true
}

@ -82,7 +82,7 @@ class CompositeUserNewsResourceRepositoryTest {
// Check that only news resources with the given topic id are returned.
assertEquals(
sampleNewsResources
.filter { it.topics.contains(sampleTopic1) }
.filter { sampleTopic1 in it.topics }
.mapToUserNewsResources(emptyUserData),
userNewsResources.first(),
)
@ -104,7 +104,7 @@ class CompositeUserNewsResourceRepositoryTest {
// Check that only news resources with the given topic id are returned.
assertEquals(
sampleNewsResources
.filter { it.topics.contains(sampleTopic1) }
.filter { sampleTopic1 in it.topics }
.mapToUserNewsResources(userData),
userNewsResources.first(),
)

@ -91,14 +91,14 @@ class UserNewsResourceTest {
// Construct the expected FollowableTopic.
val followableTopic = FollowableTopic(
topic = topic,
isFollowed = userData.followedTopics.contains(topic.id),
isFollowed = topic.id in userData.followedTopics,
)
assertTrue(userNewsResource.followableTopics.contains(followableTopic))
}
// Check that the saved flag is set correctly.
assertEquals(
userData.bookmarkedNewsResources.contains(newsResource1.id),
newsResource1.id in userData.bookmarkedNewsResources,
userNewsResource.isSaved,
)
}

@ -34,9 +34,7 @@ val nonPresentInterestsIds = setOf("2")
*/
class TestNewsResourceDao : NewsResourceDao {
private var entitiesStateFlow = MutableStateFlow(
emptyList<NewsResourceEntity>(),
)
private val entitiesStateFlow = MutableStateFlow(emptyList<NewsResourceEntity>())
internal var topicCrossReferences: List<NewsResourceTopicCrossRef> = listOf()
@ -131,7 +129,7 @@ class TestNewsResourceDao : NewsResourceDao {
override suspend fun deleteNewsResources(ids: List<String>) {
val idSet = ids.toSet()
entitiesStateFlow.update { entities ->
entities.filterNot { idSet.contains(it.id) }
entities.filterNot { it.id in idSet }
}
}
}

@ -91,11 +91,10 @@ class TestNiaNetworkDataSource : NiaNetworkDataSource {
}
}
fun List<NetworkChangeList>.after(version: Int?): List<NetworkChangeList> =
when (version) {
null -> this
else -> this.filter { it.changeListVersion > version }
}
fun List<NetworkChangeList>.after(version: Int?): List<NetworkChangeList> = when (version) {
null -> this
else -> filter { it.changeListVersion > version }
}
/**
* Return items from [this] whose id defined by [idGetter] is in [ids] if [ids] is not null
@ -105,7 +104,7 @@ private fun <T> List<T>.matchIds(
idGetter: (T) -> String,
) = when (ids) {
null -> this
else -> ids.toSet().let { idSet -> this.filter { idSet.contains(idGetter(it)) } }
else -> ids.toSet().let { idSet -> filter { idGetter(it) in idSet } }
}
/**

@ -28,20 +28,15 @@ import kotlinx.coroutines.flow.update
*/
class TestTopicDao : TopicDao {
private var entitiesStateFlow = MutableStateFlow(
emptyList<TopicEntity>(),
)
private val entitiesStateFlow = MutableStateFlow(emptyList<TopicEntity>())
override fun getTopicEntity(topicId: String): Flow<TopicEntity> {
override fun getTopicEntity(topicId: String): Flow<TopicEntity> =
throw NotImplementedError("Unused in tests")
}
override fun getTopicEntities(): Flow<List<TopicEntity>> =
entitiesStateFlow
override fun getTopicEntities(): Flow<List<TopicEntity>> = entitiesStateFlow
override fun getTopicEntities(ids: Set<String>): Flow<List<TopicEntity>> =
getTopicEntities()
.map { topics -> topics.filter { it.id in ids } }
getTopicEntities().map { topics -> topics.filter { it.id in ids } }
override suspend fun getOneOffTopicEntities(): List<TopicEntity> = emptyList()
@ -55,15 +50,11 @@ class TestTopicDao : TopicDao {
override suspend fun upsertTopics(entities: List<TopicEntity>) {
// Overwrite old values with new values
entitiesStateFlow.update { oldValues ->
(entities + oldValues).distinctBy(TopicEntity::id)
}
entitiesStateFlow.update { oldValues -> (entities + oldValues).distinctBy(TopicEntity::id) }
}
override suspend fun deleteTopics(ids: List<String>) {
val idSet = ids.toSet()
entitiesStateFlow.update { entities ->
entities.filterNot { idSet.contains(it.id) }
}
entitiesStateFlow.update { entities -> entities.filterNot { it.id in idSet } }
}
}

@ -52,7 +52,6 @@ internal object ListToMapMigration : DataMigration<UserPreferences> {
hasDoneListToMapMigration = true
}
override suspend fun shouldMigrate(currentData: UserPreferences): Boolean {
return !currentData.hasDoneListToMapMigration
}
override suspend fun shouldMigrate(currentData: UserPreferences): Boolean =
!currentData.hasDoneListToMapMigration
}

@ -103,9 +103,7 @@ class NiaPreferencesDataSource @Inject constructor(
suspend fun setDynamicColorPreference(useDynamicColor: Boolean) {
userPreferences.updateData {
it.copy {
this.useDynamicColor = useDynamicColor
}
it.copy { this.useDynamicColor = useDynamicColor }
}
}
@ -190,9 +188,7 @@ class NiaPreferencesDataSource @Inject constructor(
suspend fun setShouldHideOnboarding(shouldHideOnboarding: Boolean) {
userPreferences.updateData {
it.copy {
this.shouldHideOnboarding = shouldHideOnboarding
}
it.copy { this.shouldHideOnboarding = shouldHideOnboarding }
}
}
}

@ -16,7 +16,8 @@
package com.google.samples.apps.nowinandroid.core.designsystem
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
@ -219,60 +220,41 @@ class ThemeTest {
}
@Composable
private fun dynamicLightColorSchemeWithFallback(): ColorScheme {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
dynamicLightColorScheme(LocalContext.current)
} else {
LightDefaultColorScheme
}
private fun dynamicLightColorSchemeWithFallback(): ColorScheme = when {
SDK_INT >= VERSION_CODES.S -> dynamicLightColorScheme(LocalContext.current)
else -> LightDefaultColorScheme
}
@Composable
private fun dynamicDarkColorSchemeWithFallback(): ColorScheme {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
dynamicDarkColorScheme(LocalContext.current)
} else {
DarkDefaultColorScheme
}
private fun dynamicDarkColorSchemeWithFallback(): ColorScheme = when {
SDK_INT >= VERSION_CODES.S -> dynamicDarkColorScheme(LocalContext.current)
else -> DarkDefaultColorScheme
}
private fun emptyGradientColors(colorScheme: ColorScheme): GradientColors {
return GradientColors(container = colorScheme.surfaceColorAtElevation(2.dp))
}
private fun emptyGradientColors(colorScheme: ColorScheme): GradientColors =
GradientColors(container = colorScheme.surfaceColorAtElevation(2.dp))
private fun defaultGradientColors(colorScheme: ColorScheme): GradientColors {
return GradientColors(
top = colorScheme.inverseOnSurface,
bottom = colorScheme.primaryContainer,
container = colorScheme.surface,
)
}
private fun defaultGradientColors(colorScheme: ColorScheme): GradientColors = GradientColors(
top = colorScheme.inverseOnSurface,
bottom = colorScheme.primaryContainer,
container = colorScheme.surface,
)
private fun dynamicGradientColorsWithFallback(colorScheme: ColorScheme): GradientColors {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
emptyGradientColors(colorScheme)
} else {
defaultGradientColors(colorScheme)
}
private fun dynamicGradientColorsWithFallback(colorScheme: ColorScheme): GradientColors = when {
SDK_INT >= VERSION_CODES.S -> emptyGradientColors(colorScheme)
else -> defaultGradientColors(colorScheme)
}
private fun defaultBackgroundTheme(colorScheme: ColorScheme): BackgroundTheme {
return BackgroundTheme(
color = colorScheme.surface,
tonalElevation = 2.dp,
)
}
private fun defaultBackgroundTheme(colorScheme: ColorScheme): BackgroundTheme = BackgroundTheme(
color = colorScheme.surface,
tonalElevation = 2.dp,
)
private fun defaultTintTheme(): TintTheme {
return TintTheme()
}
private fun defaultTintTheme(): TintTheme = TintTheme()
private fun dynamicTintThemeWithFallback(colorScheme: ColorScheme): TintTheme {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
TintTheme(colorScheme.primary)
} else {
TintTheme()
}
private fun dynamicTintThemeWithFallback(colorScheme: ColorScheme): TintTheme = when {
SDK_INT >= VERSION_CODES.S -> TintTheme(colorScheme.primary)
else -> TintTheme()
}
/**

@ -28,6 +28,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color.Companion.Unspecified
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
@ -79,7 +80,7 @@ fun DynamicAsyncImage(
contentScale = ContentScale.Crop,
painter = if (isError.not() && !isLocalInspection) imageLoader else placeholder,
contentDescription = contentDescription,
colorFilter = if (iconTint != null) ColorFilter.tint(iconTint) else null,
colorFilter = if (iconTint != Unspecified) ColorFilter.tint(iconTint) else null,
)
}
}

@ -40,9 +40,9 @@ import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
fun NiaTopAppBar(
@StringRes titleRes: Int,
navigationIcon: ImageVector,
navigationIconContentDescription: String?,
navigationIconContentDescription: String,
actionIcon: ImageVector,
actionIconContentDescription: String?,
actionIconContentDescription: String,
modifier: Modifier = Modifier,
colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(),
onNavigationClick: () -> Unit = {},

@ -230,10 +230,5 @@ fun LazyStaggeredGridState.scrollbarState(
return state
}
private inline fun <T> List<T>.floatSumOf(selector: (T) -> Float): Float {
var sum = 0f
for (element in this) {
sum += selector(element)
}
return sum
}
private inline fun <T> List<T>.floatSumOf(selector: (T) -> Float): Float =
fold(initial = 0f) { accumulator, listItem -> accumulator + selector(listItem) }

@ -25,7 +25,7 @@ import androidx.compose.ui.graphics.Color
*/
@Immutable
data class TintTheme(
val iconTint: Color? = null,
val iconTint: Color = Color.Unspecified,
)
/**

@ -32,12 +32,12 @@
android:fillColor="#8C4190"
android:fillAlpha="0.11"/>
<path
android:pathData="M171,119L326,119A25,25 0,0 1,351 144L351,144A25,25 0,0 1,326 169L171,169A25,25 0,0 1,146 144L146,144A25,25 0,0 1,171 119z"
android:pathData="M171,119h155a25,25 0,0 1,25 25,25 25,0 0,1 -25,25H171a25,25 0,0 1,-25 -25,25 25,0 0,1 25,-25z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#5DD4FB"/>
<path
android:pathData="M156.023,33.705C154.247,37.971 153.333,42.543 153.333,47.161L188.666,47.161L224,47.161C224,42.543 223.086,37.971 221.31,33.705C219.535,29.44 216.932,25.563 213.651,22.298C210.37,19.033 206.475,16.444 202.188,14.677C197.901,12.91 193.307,12 188.667,12C184.026,12 179.432,12.91 175.145,14.677C170.858,16.444 166.963,19.033 163.682,22.298C160.401,25.563 157.798,29.44 156.023,33.705ZM153.333,47.161C153.333,51.778 152.409,56.35 150.615,60.616C148.821,64.882 146.192,68.758 142.877,72.023C139.562,75.288 135.627,77.878 131.296,79.645C126.965,81.412 122.323,82.322 117.635,82.322C112.947,82.322 108.305,81.412 103.974,79.645C99.643,77.878 95.708,75.288 92.393,72.023C89.078,68.758 86.449,64.882 84.655,60.616C82.861,56.35 81.938,51.778 81.938,47.161L117.635,47.161L153.333,47.161ZM12,47.161C12,42.543 12.904,37.971 14.661,33.705C16.419,29.439 18.994,25.563 22.242,22.298C25.489,19.033 29.344,16.443 33.586,14.676C37.829,12.909 42.376,12 46.968,12C51.561,12 56.108,12.909 60.351,14.676C64.593,16.443 68.448,19.033 71.695,22.298C74.942,25.563 77.518,29.439 79.276,33.705C81.033,37.971 81.938,42.543 81.938,47.161L46.968,47.161L12,47.161Z"
android:pathData="M156.02,33.7a35,35 0,0 0,-2.69 13.46L224,47.16a35,35 0,0 0,-10.35 -24.86A35.34,35.34 0,0 0,188.67 12a35.48,35.48 0,0 0,-24.99 10.3,35.15 35.15,0 0,0 -7.66,11.4ZM153.33,47.16c0,4.62 -0.92,9.19 -2.72,13.46a35.13,35.13 0,0 1,-7.73 11.4,35.74 35.74,0 0,1 -11.58,7.63 36.17,36.17 0,0 1,-38.9 -7.63,35.13 35.13,0 0,1 -7.75,-11.4 34.7,34.7 0,0 1,-2.71 -13.46h71.4ZM12,47.16A35.33,35.33 0,0 1,22.24 22.3,34.96 34.96,0 0,1 46.97,12a34.8,34.8 0,0 1,24.72 10.3,35.19 35.19,0 0,1 10.25,24.86L12,47.16Z"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
@ -53,11 +53,11 @@
</path>
<path
android:strokeWidth="1"
android:pathData="M317.068,82.136L317.065,47.103L309.777,81.37L317.057,47.102L302.805,79.105L317.051,47.099L296.456,75.439L317.045,47.094L291.007,70.534L317.04,47.089L286.698,64.602L317.036,47.083L283.716,57.905L317.034,47.076L282.192,50.734L317.033,47.068L282.192,43.403L317.034,47.061L283.716,36.232L317.036,47.054L286.698,29.534L317.04,47.048L291.007,23.603L317.045,47.042L296.456,18.697L317.051,47.038L302.805,15.032L317.057,47.035L309.777,12.766L317.065,47.033L317.068,12L317.072,47.033L324.359,12.766L317.079,47.035L331.332,15.032L317.086,47.038L337.681,18.697L317.092,47.042L343.129,23.603L317.097,47.048L347.438,29.534L317.1,47.054L350.42,36.232L317.102,47.061L351.944,43.403L317.103,47.068L351.944,50.734L317.102,47.076L350.42,57.905L317.1,47.083L347.438,64.602L317.097,47.089L343.129,70.534L317.092,47.094L337.681,75.439L317.086,47.099L331.332,79.105L317.079,47.102L324.359,81.37L317.072,47.103L317.068,82.136Z"
android:pathData="M317.07,82.14V47.1l-7.3,34.27 7.29,-34.27 -14.25,32 14.24,-32 -20.6,28.34 20.6,-28.35L291,70.53l26.03,-23.44L286.7,64.6l30.34,-17.52 -33.32,10.83 33.31,-10.83 -34.84,3.65 34.84,-3.66 -34.84,-3.67 34.84,3.66 -33.31,-10.83 33.32,10.82 -30.34,-17.52 30.34,17.52 -26.03,-23.45 26.04,23.44 -20.6,-28.34 20.6,28.34 -14.25,-32 14.26,32 -7.28,-34.27 7.29,34.26V12v35.03l7.29,-34.26 -7.28,34.27 14.25,-32 -14.24,32 20.6,-28.34 -20.6,28.34 26.04,-23.44 -26.03,23.45 30.34,-17.52 -30.34,17.52 33.32,-10.82 -33.32,10.83 34.84,-3.66 -34.84,3.67 34.84,3.66 -34.84,-3.65 33.32,10.83 -33.32,-10.83 30.34,17.52L317.1,47.1l26.03,23.44 -26.04,-23.44 20.6,28.35 -20.6,-28.34 14.24,32 -14.25,-32 7.28,34.27 -7.29,-34.27v35.04Z"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:strokeColor="#FF8B5E"/>
<path
android:pathData="M38.205,170L38.975,170A26.205,26.205 89.056,0 0,65.18 143.795L65.18,143.795A26.205,26.205 89.056,0 0,38.975 117.59L38.205,117.59A26.205,26.205 89.056,0 0,12 143.795L12,143.795A26.205,26.205 89.056,0 0,38.205 170z"
android:pathData="M38.2,170h0.77a26.2,26.2 89.06,0 0,26.21 -26.2,26.2 26.2,89.06 0,0 -26.2,-26.21h-0.77A26.2,26.2 89.06,0 0,12 143.79,26.2 26.2,89.06 0,0 38.2,170z"
android:fillColor="#FFA8FF"/>
</vector>

@ -37,22 +37,20 @@ class GetFollowableTopicsUseCase @Inject constructor(
*
* @param sortBy - the field used to sort the topics. Default NONE = no sorting.
*/
operator fun invoke(sortBy: TopicSortField = NONE): Flow<List<FollowableTopic>> {
return combine(
userDataRepository.userData,
topicsRepository.getTopics(),
) { userData, topics ->
val followedTopics = topics
.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id in userData.followedTopics,
)
}
when (sortBy) {
NAME -> followedTopics.sortedBy { it.topic.name }
else -> followedTopics
operator fun invoke(sortBy: TopicSortField = NONE): Flow<List<FollowableTopic>> = combine(
userDataRepository.userData,
topicsRepository.getTopics(),
) { userData, topics ->
val followedTopics = topics
.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = topic.id in userData.followedTopics,
)
}
when (sortBy) {
NAME -> followedTopics.sortedBy { it.topic.name }
else -> followedTopics
}
}
}

@ -45,14 +45,13 @@ data class UserNewsResource internal constructor(
followableTopics = newsResource.topics.map { topic ->
FollowableTopic(
topic = topic,
isFollowed = userData.followedTopics.contains(topic.id),
isFollowed = topic.id in userData.followedTopics,
)
},
isSaved = userData.bookmarkedNewsResources.contains(newsResource.id),
hasBeenViewed = userData.viewedNewsResources.contains(newsResource.id),
isSaved = newsResource.id in userData.bookmarkedNewsResources,
hasBeenViewed = newsResource.id in userData.viewedNewsResources,
)
}
fun List<NewsResource>.mapToUserNewsResources(userData: UserData): List<UserNewsResource> {
return map { UserNewsResource(it, userData) }
}
fun List<NewsResource>.mapToUserNewsResources(userData: UserData): List<UserNewsResource> =
map { UserNewsResource(it, userData) }

@ -24,10 +24,10 @@ import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityCompat.checkSelfPermission
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.InboxStyle
import androidx.core.app.NotificationManagerCompat
@ -57,28 +57,22 @@ internal class SystemTrayNotifier @Inject constructor(
override fun postNewsNotifications(
newsResources: List<NewsResource>,
) = with(context) {
if (ActivityCompat.checkSelfPermission(
this,
permission.POST_NOTIFICATIONS,
) != PackageManager.PERMISSION_GRANTED
) {
if (checkSelfPermission(this, permission.POST_NOTIFICATIONS) != PERMISSION_GRANTED) {
return
}
val truncatedNewsResources = newsResources
.take(MAX_NUM_NOTIFICATIONS)
val truncatedNewsResources = newsResources.take(MAX_NUM_NOTIFICATIONS)
val newsNotifications = truncatedNewsResources
.map { newsResource ->
createNewsNotification {
setSmallIcon(R.drawable.core_notifications_ic_nia_notification)
.setContentTitle(newsResource.title)
.setContentText(newsResource.content)
.setContentIntent(newsPendingIntent(newsResource))
.setGroup(NEWS_NOTIFICATION_GROUP)
.setAutoCancel(true)
}
val newsNotifications = truncatedNewsResources.map { newsResource ->
createNewsNotification {
setSmallIcon(R.drawable.core_notifications_ic_nia_notification)
.setContentTitle(newsResource.title)
.setContentText(newsResource.content)
.setContentIntent(newsPendingIntent(newsResource))
.setGroup(NEWS_NOTIFICATION_GROUP)
.setAutoCancel(true)
}
}
val summaryNotification = createNewsNotification {
val title = getString(
R.string.core_notifications_news_notification_group_summary,
@ -113,9 +107,7 @@ internal class SystemTrayNotifier @Inject constructor(
newsResources: List<NewsResource>,
title: String,
): InboxStyle = newsResources
.fold(InboxStyle()) { inboxStyle, newsResource ->
inboxStyle.addLine(newsResource.title)
}
.fold(InboxStyle()) { inboxStyle, newsResource -> inboxStyle.addLine(newsResource.title) }
.setBigContentTitle(title)
.setSummaryText(title)
}

@ -25,7 +25,6 @@ import dagger.hilt.android.testing.HiltTestApplication
* A custom runner to set up the instrumented application class for tests.
*/
class NiaTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
override fun newApplication(cl: ClassLoader, name: String, context: Context): Application =
super.newApplication(cl, HiltTestApplication::class.java.name, context)
}

@ -43,9 +43,7 @@ class TestNewsRepository : NewsRepository {
}
}
query.filterNewsIds?.let { filterNewsIds ->
result = newsResources.filter {
filterNewsIds.contains(it.id)
}
result = newsResources.filter { it.id in filterNewsIds }
}
result
}

@ -32,7 +32,5 @@ class TestRecentSearchRepository : RecentSearchRepository {
cachedRecentSearches.add(RecentSearchQuery(searchQuery))
}
override suspend fun clearRecentSearches() {
cachedRecentSearches.clear()
}
override suspend fun clearRecentSearches() = cachedRecentSearches.clear()
}

@ -29,18 +29,15 @@ class TestSearchContentsRepository : SearchContentsRepository {
private val cachedTopics: MutableList<Topic> = mutableListOf()
private val cachedNewsResources: MutableList<NewsResource> = mutableListOf()
override suspend fun populateFtsData() { /* no-op */ }
override suspend fun populateFtsData() = Unit
override fun searchContents(searchQuery: String): Flow<SearchResult> = flowOf(
SearchResult(
topics = cachedTopics.filter {
it.name.contains(searchQuery) ||
it.shortDescription.contains(searchQuery) ||
it.longDescription.contains(searchQuery)
searchQuery in it.name || searchQuery in it.shortDescription || searchQuery in it.longDescription
},
newsResources = cachedNewsResources.filter {
it.content.contains(searchQuery) ||
it.title.contains(searchQuery)
searchQuery in it.content || searchQuery in it.title
},
),
)

@ -33,9 +33,8 @@ class TestTopicsRepository : TopicsRepository {
override fun getTopics(): Flow<List<Topic>> = topicsFlow
override fun getTopic(id: String): Flow<Topic> {
return topicsFlow.map { topics -> topics.find { it.id == id }!! }
}
override fun getTopic(id: String): Flow<Topic> =
topicsFlow.map { topics -> topics.find { it.id == id }!! }
/**
* A test-only API to allow controlling the list of topics from tests.

@ -32,11 +32,7 @@ import org.junit.runner.Description
class MainDispatcherRule(
private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}
override fun starting(description: Description) = Dispatchers.setMain(testDispatcher)
override fun finished(description: Description) {
Dispatchers.resetMain()
}
override fun finished(description: Description) = Dispatchers.resetMain()
}

@ -26,5 +26,5 @@ class TestAnalyticsHelper : AnalyticsHelper {
events.add(event)
}
fun hasLogged(event: AnalyticsEvent) = events.contains(event)
fun hasLogged(event: AnalyticsEvent) = event in events
}

@ -26,9 +26,7 @@ class TestSyncManager : SyncManager {
override val isSyncing: Flow<Boolean> = syncStatusFlow
override fun requestSync() {
TODO("Not yet implemented")
}
override fun requestSync(): Unit = TODO("Not yet implemented")
/**
* A test-only API to set the sync status from tests.

@ -50,7 +50,7 @@ fun rememberMetricsStateHolder(): Holder {
*/
@Composable
fun TrackJank(
vararg keys: Any?,
vararg keys: Any,
reportMetric: suspend CoroutineScope.(state: Holder) -> Unit,
) {
val metrics = rememberMetricsStateHolder()
@ -65,7 +65,7 @@ fun TrackJank(
*/
@Composable
fun TrackDisposableJank(
vararg keys: Any?,
vararg keys: Any,
reportMetric: DisposableEffectScope.(state: Holder) -> DisposableEffectResult,
) {
val metrics = rememberMetricsStateHolder()

@ -47,6 +47,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.testTag
@ -237,7 +238,7 @@ private fun EmptyState(modifier: Modifier = Modifier) {
Image(
modifier = Modifier.fillMaxWidth(),
painter = painterResource(id = R.drawable.feature_bookmarks_img_empty_bookmarks),
colorFilter = if (iconTint != null) ColorFilter.tint(iconTint) else null,
colorFilter = if (iconTint != Color.Unspecified) ColorFilter.tint(iconTint) else null,
contentDescription = null,
)

@ -24,9 +24,7 @@ import com.google.samples.apps.nowinandroid.feature.bookmarks.BookmarksRoute
const val BOOKMARKS_ROUTE = "bookmarks_route"
fun NavController.navigateToBookmarks(navOptions: NavOptions? = null) {
this.navigate(BOOKMARKS_ROUTE, navOptions)
}
fun NavController.navigateToBookmarks(navOptions: NavOptions) = navigate(BOOKMARKS_ROUTE, navOptions)
fun NavGraphBuilder.bookmarksScreen(
onTopicClick: (String) -> Unit,

@ -18,6 +18,7 @@ plugins {
alias(libs.plugins.nowinandroid.android.feature)
alias(libs.plugins.nowinandroid.android.library.compose)
alias(libs.plugins.nowinandroid.android.library.jacoco)
alias(libs.plugins.roborazzi)
}
android {
@ -32,6 +33,7 @@ dependencies {
testImplementation(libs.hilt.android.testing)
testImplementation(libs.robolectric)
testImplementation(projects.core.testing)
testDemoImplementation(libs.roborazzi)
androidTestImplementation(projects.core.testing)
}

@ -30,9 +30,7 @@ const val FOR_YOU_ROUTE = "for_you_route/{$LINKED_NEWS_RESOURCE_ID}"
private const val DEEP_LINK_URI_PATTERN =
"https://www.nowinandroid.apps.samples.google.com/foryou/{$LINKED_NEWS_RESOURCE_ID}"
fun NavController.navigateToForYou(navOptions: NavOptions? = null) {
this.navigate(FOR_YOU_ROUTE, navOptions)
}
fun NavController.navigateToForYou(navOptions: NavOptions) = navigate(FOR_YOU_ROUTE, navOptions)
fun NavGraphBuilder.forYouScreen(onTopicClick: (String) -> Unit) {
composable(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

@ -26,9 +26,7 @@ import com.google.samples.apps.nowinandroid.feature.interests.InterestsRoute
private const val INTERESTS_GRAPH_ROUTE_PATTERN = "interests_graph"
const val INTERESTS_ROUTE = "interests_route"
fun NavController.navigateToInterestsGraph(navOptions: NavOptions? = null) {
this.navigate(INTERESTS_GRAPH_ROUTE_PATTERN, navOptions)
}
fun NavController.navigateToInterestsGraph(navOptions: NavOptions) = navigate(INTERESTS_GRAPH_ROUTE_PATTERN, navOptions)
fun NavGraphBuilder.interestsGraph(
onTopicClick: (String) -> Unit,

@ -264,9 +264,7 @@ fun EmptySearchResultBody(
) { offset ->
tryAnotherSearchString.getStringAnnotations(start = offset, end = offset)
.firstOrNull()
?.let {
onInterestsClick()
}
?.let { onInterestsClick() }
}
}
}
@ -520,9 +518,7 @@ private fun SearchTextField(
}
},
onValueChange = {
if (!it.contains("\n")) {
onSearchQueryChanged(it)
}
if ("\n" !in it) onSearchQueryChanged(it)
},
modifier = Modifier
.fillMaxWidth()

@ -24,9 +24,7 @@ import com.google.samples.apps.nowinandroid.feature.search.SearchRoute
const val SEARCH_ROUTE = "search_route"
fun NavController.navigateToSearch(navOptions: NavOptions? = null) {
this.navigate(SEARCH_ROUTE, navOptions)
}
fun NavController.navigateToSearch(navOptions: NavOptions? = null) = navigate(SEARCH_ROUTE, navOptions)
fun NavGraphBuilder.searchScreen(
onBackClick: () -> Unit,

@ -117,22 +117,16 @@ private fun topicUiState(
when (followedTopicToTopicResult) {
is Result.Success -> {
val (followedTopics, topic) = followedTopicToTopicResult.data
val followed = followedTopics.contains(topicId)
TopicUiState.Success(
followableTopic = FollowableTopic(
topic = topic,
isFollowed = followed,
isFollowed = topicId in followedTopics,
),
)
}
is Result.Loading -> {
TopicUiState.Loading
}
is Result.Error -> {
TopicUiState.Error
}
is Result.Loading -> TopicUiState.Loading
is Result.Error -> TopicUiState.Error
}
}
}
@ -151,26 +145,13 @@ private fun newsUiState(
val bookmark: Flow<Set<String>> = userDataRepository.userData
.map { it.bookmarkedNewsResources }
return combine(
newsStream,
bookmark,
::Pair,
)
return combine(newsStream, bookmark, ::Pair)
.asResult()
.map { newsToBookmarksResult ->
when (newsToBookmarksResult) {
is Result.Success -> {
val news = newsToBookmarksResult.data.first
NewsUiState.Success(news)
}
is Result.Loading -> {
NewsUiState.Loading
}
is Result.Error -> {
NewsUiState.Error
}
is Result.Success -> NewsUiState.Success(newsToBookmarksResult.data.first)
is Result.Loading -> NewsUiState.Loading
is Result.Error -> NewsUiState.Error
}
}
}

@ -40,7 +40,7 @@ internal class TopicArgs(val topicId: String) {
fun NavController.navigateToTopic(topicId: String) {
val encodedId = URLEncoder.encode(topicId, URL_CHARACTER_ENCODING)
this.navigate("topic_route/$encodedId") {
navigate("topic_route/$encodedId") {
launchSingleTop = true
}
}

@ -34,15 +34,13 @@ import org.jetbrains.uast.UQualifiedReferenceExpression
*/
class DesignSystemDetector : Detector(), Detector.UastScanner {
override fun getApplicableUastTypes(): List<Class<out UElement>> {
return listOf(
UCallExpression::class.java,
UQualifiedReferenceExpression::class.java,
)
}
override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(
UCallExpression::class.java,
UQualifiedReferenceExpression::class.java,
)
override fun createUastHandler(context: JavaContext): UElementHandler {
return object : UElementHandler() {
override fun createUastHandler(context: JavaContext): UElementHandler =
object : UElementHandler() {
override fun visitCallExpression(node: UCallExpression) {
val name = node.methodName ?: return
val preferredName = METHOD_NAMES[name] ?: return
@ -55,7 +53,6 @@ class DesignSystemDetector : Detector(), Detector.UastScanner {
reportIssue(context, node, name, preferredName)
}
}
}
companion object {
@JvmField

Loading…
Cancel
Save