commit
3ca31bedec
@ -0,0 +1,6 @@
|
|||||||
|
# https://editorconfig.org/
|
||||||
|
# This configuration is used by ktlint when spotless invokes it
|
||||||
|
|
||||||
|
[*.{kt,kts}]
|
||||||
|
ij_kotlin_allow_trailing_comma=true
|
||||||
|
ij_kotlin_allow_trailing_comma_on_call_site=true
|
@ -0,0 +1,38 @@
|
|||||||
|
name: Android CI with GMD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
android-ci:
|
||||||
|
runs-on: macos-12
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
device-config: [ "pixel4api30aospatd", "pixelcapi30aospatd" ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: '11'
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v2
|
||||||
|
|
||||||
|
- name: Run instrumented tests with GMD
|
||||||
|
run: ./gradlew cleanManagedDevices --unused-only &&
|
||||||
|
./gradlew ${{ matrix.device-config }}DemoDebugAndroidTest -Dorg.gradle.workers.max=1
|
||||||
|
-Pandroid.testoptions.manageddevices.emulator.gpu="swiftshader_indirect" -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true
|
||||||
|
|
||||||
|
- name: Upload test reports
|
||||||
|
if: success() || failure()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: test-reports
|
||||||
|
path: |
|
||||||
|
'**/*/build/reports/androidTests/'
|
@ -0,0 +1,2 @@
|
|||||||
|
# This file can be used to trigger an internal build by changing the number below
|
||||||
|
3
|
@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "YourProjectId",
|
||||||
|
"project_id": "abc",
|
||||||
|
"storage_bucket": "abc"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "Your:App:Id",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.google.samples.apps.nowinandroid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "Your:App:Id",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.google.samples.apps.nowinandroid.demo.debug"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "Your:App:Id",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.google.samples.apps.nowinandroid.demo.benchmark"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "Your:App:Id",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.google.samples.apps.nowinandroid.benchmark"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "Your:App:Id",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.google.samples.apps.nowinandroid.debug"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "Your:App:Id",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.google.samples.apps.nowinandroid.demo"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "APlaceholderAPIKeyWith-ThirtyNineCharsX"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* 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.ui
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||||
|
import androidx.compose.material3.windowsizeclass.WindowSizeClass
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.google.accompanist.testharness.TestHarness
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.util.NetworkMonitor
|
||||||
|
import com.google.samples.apps.nowinandroid.uitesthiltmanifest.HiltComponentActivity
|
||||||
|
import dagger.hilt.android.testing.BindValue
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests that the navigation UI is rendered correctly on different screen sizes.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||||
|
@HiltAndroidTest
|
||||||
|
class NavigationUiTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the components' state and is used to perform injection on your test
|
||||||
|
*/
|
||||||
|
@get:Rule(order = 0)
|
||||||
|
val hiltRule = HiltAndroidRule(this)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a temporary folder used to create a Data Store file. This guarantees that
|
||||||
|
* the file is removed in between each test, preventing a crash.
|
||||||
|
*/
|
||||||
|
@BindValue
|
||||||
|
@get:Rule(order = 1)
|
||||||
|
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use a test activity to set the content on.
|
||||||
|
*/
|
||||||
|
@get:Rule(order = 2)
|
||||||
|
val composeTestRule = createAndroidComposeRule<HiltComponentActivity>()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var networkMonitor: NetworkMonitor
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
hiltRule.inject()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun compactWidth_compactHeight_showsNavigationBar() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
TestHarness(size = DpSize(400.dp, 400.dp)) {
|
||||||
|
BoxWithConstraints {
|
||||||
|
NiaApp(
|
||||||
|
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||||
|
DpSize(maxWidth, maxHeight),
|
||||||
|
),
|
||||||
|
networkMonitor = networkMonitor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mediumWidth_compactHeight_showsNavigationRail() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
TestHarness(size = DpSize(610.dp, 400.dp)) {
|
||||||
|
BoxWithConstraints {
|
||||||
|
NiaApp(
|
||||||
|
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||||
|
DpSize(maxWidth, maxHeight),
|
||||||
|
),
|
||||||
|
networkMonitor = networkMonitor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun expandedWidth_compactHeight_showsNavigationRail() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
TestHarness(size = DpSize(900.dp, 400.dp)) {
|
||||||
|
BoxWithConstraints {
|
||||||
|
NiaApp(
|
||||||
|
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||||
|
DpSize(maxWidth, maxHeight),
|
||||||
|
),
|
||||||
|
networkMonitor = networkMonitor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun compactWidth_mediumHeight_showsNavigationBar() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
TestHarness(size = DpSize(400.dp, 500.dp)) {
|
||||||
|
BoxWithConstraints {
|
||||||
|
NiaApp(
|
||||||
|
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||||
|
DpSize(maxWidth, maxHeight),
|
||||||
|
),
|
||||||
|
networkMonitor = networkMonitor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mediumWidth_mediumHeight_showsNavigationRail() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
TestHarness(size = DpSize(610.dp, 500.dp)) {
|
||||||
|
BoxWithConstraints {
|
||||||
|
NiaApp(
|
||||||
|
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||||
|
DpSize(maxWidth, maxHeight),
|
||||||
|
),
|
||||||
|
networkMonitor = networkMonitor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun expandedWidth_mediumHeight_showsNavigationRail() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
TestHarness(size = DpSize(900.dp, 500.dp)) {
|
||||||
|
BoxWithConstraints {
|
||||||
|
NiaApp(
|
||||||
|
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||||
|
DpSize(maxWidth, maxHeight),
|
||||||
|
),
|
||||||
|
networkMonitor = networkMonitor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun compactWidth_expandedHeight_showsNavigationBar() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
TestHarness(size = DpSize(400.dp, 1000.dp)) {
|
||||||
|
BoxWithConstraints {
|
||||||
|
NiaApp(
|
||||||
|
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||||
|
DpSize(maxWidth, maxHeight),
|
||||||
|
),
|
||||||
|
networkMonitor = networkMonitor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag("NiaBottomBar").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithTag("NiaNavRail").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mediumWidth_expandedHeight_showsNavigationRail() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
TestHarness(size = DpSize(610.dp, 1000.dp)) {
|
||||||
|
BoxWithConstraints {
|
||||||
|
NiaApp(
|
||||||
|
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||||
|
DpSize(maxWidth, maxHeight),
|
||||||
|
),
|
||||||
|
networkMonitor = networkMonitor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun expandedWidth_expandedHeight_showsNavigationRail() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
TestHarness(size = DpSize(900.dp, 1000.dp)) {
|
||||||
|
BoxWithConstraints {
|
||||||
|
NiaApp(
|
||||||
|
windowSizeClass = WindowSizeClass.calculateFromSize(
|
||||||
|
DpSize(maxWidth, maxHeight),
|
||||||
|
),
|
||||||
|
networkMonitor = networkMonitor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithTag("NiaNavRail").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithTag("NiaBottomBar").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,26 @@
|
|||||||
|
<?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"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<application>
|
||||||
|
<!-- Enable Firebase analytics for `prod` builds -->
|
||||||
|
<meta-data
|
||||||
|
tools:replace="android:value"
|
||||||
|
android:name="firebase_analytics_collection_deactivated"
|
||||||
|
android:value="false" />
|
||||||
|
</application>
|
||||||
|
</manifest>
|
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* 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 androidx.test.uiautomator
|
||||||
|
|
||||||
|
import androidx.test.uiautomator.HasChildrenOp.AT_LEAST
|
||||||
|
import androidx.test.uiautomator.HasChildrenOp.AT_MOST
|
||||||
|
import androidx.test.uiautomator.HasChildrenOp.EXACTLY
|
||||||
|
|
||||||
|
// These helpers need to be in the androidx.test.uiautomator package,
|
||||||
|
// because the abstract class has package local method that needs to be implemented.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Condition will be satisfied if given element has specified count of children
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class HasChildrenOp {
|
||||||
|
AT_LEAST,
|
||||||
|
EXACTLY,
|
||||||
|
AT_MOST,
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
|
||||||
|
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.api.artifacts.VersionCatalogsExtension
|
||||||
|
import org.gradle.kotlin.dsl.configure
|
||||||
|
import org.gradle.kotlin.dsl.dependencies
|
||||||
|
import org.gradle.kotlin.dsl.getByType
|
||||||
|
|
||||||
|
class AndroidApplicationFirebaseConventionPlugin : Plugin<Project> {
|
||||||
|
override fun apply(target: Project) {
|
||||||
|
with(target) {
|
||||||
|
with(pluginManager) {
|
||||||
|
apply("com.google.gms.google-services")
|
||||||
|
apply("com.google.firebase.firebase-perf")
|
||||||
|
apply("com.google.firebase.crashlytics")
|
||||||
|
}
|
||||||
|
|
||||||
|
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
|
||||||
|
dependencies {
|
||||||
|
val bom = libs.findLibrary("firebase-bom").get()
|
||||||
|
add("implementation", platform(bom))
|
||||||
|
"implementation"(libs.findLibrary("firebase.analytics").get())
|
||||||
|
"implementation"(libs.findLibrary("firebase.performance").get())
|
||||||
|
"implementation"(libs.findLibrary("firebase.crashlytics").get())
|
||||||
|
}
|
||||||
|
|
||||||
|
extensions.configure<ApplicationAndroidComponentsExtension> {
|
||||||
|
finalizeDsl {
|
||||||
|
it.buildTypes.forEach { buildType ->
|
||||||
|
// Disable the Crashlytics mapping file upload. This feature should only be
|
||||||
|
// enabled if a Firebase backend is available and configured in
|
||||||
|
// google-services.json.
|
||||||
|
buildType.configure<CrashlyticsExtension> {
|
||||||
|
mappingFileUploadEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import com.google.devtools.ksp.gradle.KspExtension
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.api.artifacts.VersionCatalogsExtension
|
||||||
|
import org.gradle.api.tasks.InputDirectory
|
||||||
|
import org.gradle.api.tasks.PathSensitive
|
||||||
|
import org.gradle.api.tasks.PathSensitivity
|
||||||
|
import org.gradle.kotlin.dsl.configure
|
||||||
|
import org.gradle.kotlin.dsl.dependencies
|
||||||
|
import org.gradle.kotlin.dsl.getByType
|
||||||
|
import org.gradle.process.CommandLineArgumentProvider
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class AndroidRoomConventionPlugin : Plugin<Project> {
|
||||||
|
|
||||||
|
override fun apply(target: Project) {
|
||||||
|
with(target) {
|
||||||
|
pluginManager.apply("com.google.devtools.ksp")
|
||||||
|
|
||||||
|
extensions.configure<KspExtension> {
|
||||||
|
// The schemas directory contains a schema file for each version of the Room database.
|
||||||
|
// This is required to enable Room auto migrations.
|
||||||
|
// See https://developer.android.com/reference/kotlin/androidx/room/AutoMigration.
|
||||||
|
arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
|
||||||
|
}
|
||||||
|
|
||||||
|
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
|
||||||
|
dependencies {
|
||||||
|
add("implementation", libs.findLibrary("room.runtime").get())
|
||||||
|
add("implementation", libs.findLibrary("room.ktx").get())
|
||||||
|
add("ksp", libs.findLibrary("room.compiler").get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://issuetracker.google.com/issues/132245929
|
||||||
|
* [Export schemas](https://developer.android.com/training/data-storage/room/migrating-db-versions#export-schemas)
|
||||||
|
*/
|
||||||
|
class RoomSchemaArgProvider(
|
||||||
|
@get:InputDirectory
|
||||||
|
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||||
|
val schemaDir: File,
|
||||||
|
) : CommandLineArgumentProvider {
|
||||||
|
override fun asArguments() = listOf("room.schemaLocation=${schemaDir.path}")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import com.android.build.api.dsl.CommonExtension
|
||||||
|
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.invoke
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure project for Gradle managed devices
|
||||||
|
*/
|
||||||
|
internal fun configureGradleManagedDevices(
|
||||||
|
commonExtension: CommonExtension<*, *, *, *>,
|
||||||
|
) {
|
||||||
|
val deviceConfigs = listOf(
|
||||||
|
DeviceConfig("Pixel 4", 30, "aosp-atd"),
|
||||||
|
DeviceConfig("Pixel 6", 31, "aosp"),
|
||||||
|
DeviceConfig("Pixel C", 30, "aosp-atd"),
|
||||||
|
)
|
||||||
|
|
||||||
|
commonExtension.testOptions {
|
||||||
|
managedDevices {
|
||||||
|
devices {
|
||||||
|
deviceConfigs.forEach { deviceConfig ->
|
||||||
|
maybeCreate(deviceConfig.taskName, ManagedVirtualDevice::class.java).apply {
|
||||||
|
device = deviceConfig.device
|
||||||
|
apiLevel = deviceConfig.apiLevel
|
||||||
|
systemImageSource = deviceConfig.systemImageSource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class DeviceConfig(
|
||||||
|
val device: String,
|
||||||
|
val apiLevel: Int,
|
||||||
|
val systemImageSource: String,
|
||||||
|
) {
|
||||||
|
val taskName = buildString {
|
||||||
|
append(device.toLowerCase(Locale.ROOT).replace(" ", ""))
|
||||||
|
append("api")
|
||||||
|
append(apiLevel.toString())
|
||||||
|
append(systemImageSource.replace("-", ""))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is shared between :app and :benchmarks module to provide configurations type safety.
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
enum class NiaBuildType(val applicationIdSuffix: String? = null) {
|
||||||
|
DEBUG(".debug"),
|
||||||
|
RELEASE,
|
||||||
|
BENCHMARK(".benchmark")
|
||||||
|
}
|
@ -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.
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
id("nowinandroid.android.library")
|
||||||
|
id("nowinandroid.android.library.compose")
|
||||||
|
id("nowinandroid.android.hilt")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.google.samples.apps.nowinandroid.core.analytics"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(platform(libs.firebase.bom))
|
||||||
|
implementation(libs.androidx.compose.runtime)
|
||||||
|
implementation(libs.androidx.core.ktx)
|
||||||
|
implementation(libs.firebase.analytics)
|
||||||
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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.analytics
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
abstract class AnalyticsModule {
|
||||||
|
@Binds
|
||||||
|
abstract fun bindsAnalyticsHelper(analyticsHelperImpl: StubAnalyticsHelper): AnalyticsHelper
|
||||||
|
}
|
@ -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 />
|
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* 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.analytics
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an analytics event.
|
||||||
|
*
|
||||||
|
* @param type - the event type. Wherever possible use one of the standard
|
||||||
|
* event `Types`, however, if there is no suitable event type already defined, a custom event can be
|
||||||
|
* defined as long as it is configured in your backend analytics system (for example, by creating a
|
||||||
|
* Firebase Analytics custom event).
|
||||||
|
*
|
||||||
|
* @param extras - list of parameters which supply additional context to the event. See `Param`.
|
||||||
|
*/
|
||||||
|
data class AnalyticsEvent(
|
||||||
|
val type: String,
|
||||||
|
val extras: List<Param> = emptyList(),
|
||||||
|
) {
|
||||||
|
// Standard analytics types.
|
||||||
|
class Types {
|
||||||
|
companion object {
|
||||||
|
const val SCREEN_VIEW = "screen_view" // (extras: SCREEN_NAME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A key-value pair used to supply extra context to an analytics event.
|
||||||
|
*
|
||||||
|
* @param key - the parameter key. Wherever possible use one of the standard `ParamKeys`,
|
||||||
|
* however, if no suitable key is available you can define your own as long as it is configured
|
||||||
|
* in your backend analytics system (for example, by creating a Firebase Analytics custom
|
||||||
|
* parameter).
|
||||||
|
*
|
||||||
|
* @param value - the parameter value.
|
||||||
|
*/
|
||||||
|
data class Param(val key: String, val value: String)
|
||||||
|
|
||||||
|
// Standard parameter keys.
|
||||||
|
class ParamKeys {
|
||||||
|
companion object {
|
||||||
|
const val SCREEN_NAME = "screen_name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* 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.analytics
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for logging analytics events. See `FirebaseAnalyticsHelper` and
|
||||||
|
* `StubAnalyticsHelper` for implementations.
|
||||||
|
*/
|
||||||
|
interface AnalyticsHelper {
|
||||||
|
fun logEvent(event: AnalyticsEvent)
|
||||||
|
}
|
@ -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.analytics
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
private const val TAG = "StubAnalyticsHelper"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of AnalyticsHelper just writes the events to logcat. Used in builds where no
|
||||||
|
* analytics events should be sent to a backend.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class StubAnalyticsHelper @Inject constructor() : AnalyticsHelper {
|
||||||
|
override fun logEvent(event: AnalyticsEvent) {
|
||||||
|
Log.d(TAG, "Received analytics event: $event")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* 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.analytics
|
||||||
|
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global key used to obtain access to the AnalyticsHelper through a CompositionLocal.
|
||||||
|
*/
|
||||||
|
val LocalAnalyticsHelper = staticCompositionLocalOf<AnalyticsHelper> {
|
||||||
|
// Provide a default AnalyticsHelper which does nothing. This is so that tests and previews
|
||||||
|
// do not have to provide one. For real app builds provide a different implementation.
|
||||||
|
NoOpAnalyticsHelper()
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* 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.analytics
|
||||||
|
|
||||||
|
import com.google.firebase.analytics.FirebaseAnalytics
|
||||||
|
import com.google.firebase.analytics.ktx.analytics
|
||||||
|
import com.google.firebase.ktx.Firebase
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
abstract class AnalyticsModule {
|
||||||
|
@Binds
|
||||||
|
abstract fun bindsAnalyticsHelper(analyticsHelperImpl: FirebaseAnalyticsHelper): AnalyticsHelper
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideFirebaseAnalytics(): FirebaseAnalytics { return Firebase.analytics }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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.analytics
|
||||||
|
|
||||||
|
import com.google.firebase.analytics.FirebaseAnalytics
|
||||||
|
import com.google.firebase.analytics.ktx.logEvent
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of `AnalyticsHelper` which logs events to a Firebase backend.
|
||||||
|
*/
|
||||||
|
class FirebaseAnalyticsHelper @Inject constructor(
|
||||||
|
private val firebaseAnalytics: FirebaseAnalytics,
|
||||||
|
) : AnalyticsHelper {
|
||||||
|
|
||||||
|
override fun logEvent(event: AnalyticsEvent) {
|
||||||
|
firebaseAnalytics.logEvent(event.type) {
|
||||||
|
for (extra in event.extras) {
|
||||||
|
// Truncate parameter keys and values according to firebase maximum length values.
|
||||||
|
param(
|
||||||
|
key = extra.key.take(40),
|
||||||
|
value = extra.value.take(100),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,29 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* 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.data.model
|
|
||||||
|
|
||||||
import com.google.samples.apps.nowinandroid.core.database.model.AuthorEntity
|
|
||||||
import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
|
|
||||||
|
|
||||||
fun NetworkAuthor.asEntity() = AuthorEntity(
|
|
||||||
id = id,
|
|
||||||
name = name,
|
|
||||||
imageUrl = imageUrl,
|
|
||||||
twitter = twitter,
|
|
||||||
mediumPage = mediumPage,
|
|
||||||
bio = bio,
|
|
||||||
)
|
|
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* 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.data.repository
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
fun AnalyticsHelper.logNewsResourceBookmarkToggled(newsResourceId: String, isBookmarked: Boolean) {
|
||||||
|
val eventType = if (isBookmarked) "news_resource_saved" else "news_resource_unsaved"
|
||||||
|
val paramKey = if (isBookmarked) "saved_news_resource_id" else "unsaved_news_resource_id"
|
||||||
|
logEvent(
|
||||||
|
AnalyticsEvent(
|
||||||
|
type = eventType,
|
||||||
|
extras = listOf(
|
||||||
|
Param(key = paramKey, value = newsResourceId),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AnalyticsHelper.logTopicFollowToggled(followedTopicId: String, isFollowed: Boolean) {
|
||||||
|
val eventType = if (isFollowed) "topic_followed" else "topic_unfollowed"
|
||||||
|
val paramKey = if (isFollowed) "followed_topic_id" else "unfollowed_topic_id"
|
||||||
|
logEvent(
|
||||||
|
AnalyticsEvent(
|
||||||
|
type = eventType,
|
||||||
|
extras = listOf(
|
||||||
|
Param(key = paramKey, value = followedTopicId),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun AnalyticsHelper.logThemeChanged(themeName: String) =
|
||||||
|
logEvent(
|
||||||
|
AnalyticsEvent(
|
||||||
|
type = "theme_changed",
|
||||||
|
extras = listOf(
|
||||||
|
Param(key = "theme_name", value = themeName),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun AnalyticsHelper.logDarkThemeConfigChanged(darkThemeConfigName: String) =
|
||||||
|
logEvent(
|
||||||
|
AnalyticsEvent(
|
||||||
|
type = "dark_theme_config_changed",
|
||||||
|
extras = listOf(
|
||||||
|
Param(key = "dark_theme_config", value = darkThemeConfigName),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun AnalyticsHelper.logDynamicColorPreferenceChanged(useDynamicColor: Boolean) =
|
||||||
|
logEvent(
|
||||||
|
AnalyticsEvent(
|
||||||
|
type = "dynamic_color_preference_changed",
|
||||||
|
extras = listOf(
|
||||||
|
Param(key = "dynamic_color_preference", value = useDynamicColor.toString()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun AnalyticsHelper.logOnboardingStateChanged(shouldHideOnboarding: Boolean) {
|
||||||
|
val eventType = if (shouldHideOnboarding) "onboarding_complete" else "onboarding_reset"
|
||||||
|
logEvent(
|
||||||
|
AnalyticsEvent(type = eventType),
|
||||||
|
)
|
||||||
|
}
|
@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* 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.data.repository
|
|
||||||
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.Synchronizer
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.changeListSync
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.model.asEntity
|
|
||||||
import com.google.samples.apps.nowinandroid.core.database.dao.AuthorDao
|
|
||||||
import com.google.samples.apps.nowinandroid.core.database.model.AuthorEntity
|
|
||||||
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
|
|
||||||
import com.google.samples.apps.nowinandroid.core.datastore.ChangeListVersions
|
|
||||||
import com.google.samples.apps.nowinandroid.core.model.data.Author
|
|
||||||
import com.google.samples.apps.nowinandroid.core.network.NiaNetworkDataSource
|
|
||||||
import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disk storage backed implementation of the [AuthorsRepository].
|
|
||||||
* Reads are exclusively from local storage to support offline access.
|
|
||||||
*/
|
|
||||||
class OfflineFirstAuthorsRepository @Inject constructor(
|
|
||||||
private val authorDao: AuthorDao,
|
|
||||||
private val network: NiaNetworkDataSource,
|
|
||||||
) : AuthorsRepository {
|
|
||||||
|
|
||||||
override fun getAuthorStream(id: String): Flow<Author> =
|
|
||||||
authorDao.getAuthorEntityStream(id).map {
|
|
||||||
it.asExternalModel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getAuthorsStream(): Flow<List<Author>> =
|
|
||||||
authorDao.getAuthorEntitiesStream()
|
|
||||||
.map { it.map(AuthorEntity::asExternalModel) }
|
|
||||||
|
|
||||||
override suspend fun syncWith(synchronizer: Synchronizer): Boolean =
|
|
||||||
synchronizer.changeListSync(
|
|
||||||
versionReader = ChangeListVersions::authorVersion,
|
|
||||||
changeListFetcher = { currentVersion ->
|
|
||||||
network.getAuthorChangeList(after = currentVersion)
|
|
||||||
},
|
|
||||||
versionUpdater = { latestVersion ->
|
|
||||||
copy(authorVersion = latestVersion)
|
|
||||||
},
|
|
||||||
modelDeleter = authorDao::deleteAuthors,
|
|
||||||
modelUpdater = { changedIds ->
|
|
||||||
val networkAuthors = network.getAuthors(ids = changedIds)
|
|
||||||
authorDao.upsertAuthors(
|
|
||||||
entities = networkAuthors.map(NetworkAuthor::asEntity)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* 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.data.repository.fake
|
|
||||||
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.Synchronizer
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.repository.AuthorsRepository
|
|
||||||
import com.google.samples.apps.nowinandroid.core.model.data.Author
|
|
||||||
import com.google.samples.apps.nowinandroid.core.network.Dispatcher
|
|
||||||
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
|
|
||||||
import com.google.samples.apps.nowinandroid.core.network.fake.FakeAssetManager
|
|
||||||
import com.google.samples.apps.nowinandroid.core.network.fake.FakeDataSource
|
|
||||||
import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
|
|
||||||
import java.io.InputStream
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fake implementation of the [AuthorsRepository] that returns hardcoded authors.
|
|
||||||
*
|
|
||||||
* This allows us to run the app with fake data, without needing an internet connection or working
|
|
||||||
* backend.
|
|
||||||
*/
|
|
||||||
class FakeAuthorsRepository @Inject constructor(
|
|
||||||
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
|
|
||||||
private val networkJson: Json,
|
|
||||||
private val assets: FakeAssetManager,
|
|
||||||
) : AuthorsRepository {
|
|
||||||
|
|
||||||
override fun getAuthorsStream(): Flow<List<Author>> = flow {
|
|
||||||
emit(
|
|
||||||
assets.open(FakeDataSource.AUTHORS)
|
|
||||||
.use<InputStream, List<NetworkAuthor>>(networkJson::decodeFromStream)
|
|
||||||
.map {
|
|
||||||
Author(
|
|
||||||
id = it.id,
|
|
||||||
name = it.name,
|
|
||||||
imageUrl = it.imageUrl,
|
|
||||||
twitter = it.twitter,
|
|
||||||
mediumPage = it.mediumPage,
|
|
||||||
bio = it.bio,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.flowOn(ioDispatcher)
|
|
||||||
|
|
||||||
override fun getAuthorStream(id: String): Flow<Author> {
|
|
||||||
return getAuthorsStream().map { it.first { author -> author.id == id } }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun syncWith(synchronizer: Synchronizer) = true
|
|
||||||
}
|
|
@ -1,180 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* 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.data.repository
|
|
||||||
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.Synchronizer
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.model.asEntity
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.testdoubles.CollectionType
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.testdoubles.TestAuthorDao
|
|
||||||
import com.google.samples.apps.nowinandroid.core.data.testdoubles.TestNiaNetworkDataSource
|
|
||||||
import com.google.samples.apps.nowinandroid.core.database.dao.AuthorDao
|
|
||||||
import com.google.samples.apps.nowinandroid.core.database.model.AuthorEntity
|
|
||||||
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
|
|
||||||
import com.google.samples.apps.nowinandroid.core.datastore.NiaPreferencesDataSource
|
|
||||||
import com.google.samples.apps.nowinandroid.core.datastore.test.testUserPreferencesDataStore
|
|
||||||
import com.google.samples.apps.nowinandroid.core.model.data.Author
|
|
||||||
import com.google.samples.apps.nowinandroid.core.network.model.NetworkAuthor
|
|
||||||
import com.google.samples.apps.nowinandroid.core.network.model.NetworkChangeList
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.rules.TemporaryFolder
|
|
||||||
|
|
||||||
class OfflineFirstAuthorsRepositoryTest {
|
|
||||||
|
|
||||||
private lateinit var subject: OfflineFirstAuthorsRepository
|
|
||||||
|
|
||||||
private lateinit var authorDao: AuthorDao
|
|
||||||
|
|
||||||
private lateinit var network: TestNiaNetworkDataSource
|
|
||||||
|
|
||||||
private lateinit var synchronizer: Synchronizer
|
|
||||||
|
|
||||||
@get:Rule
|
|
||||||
val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build()
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
authorDao = TestAuthorDao()
|
|
||||||
network = TestNiaNetworkDataSource()
|
|
||||||
val niaPreferencesDataSource = NiaPreferencesDataSource(
|
|
||||||
tmpFolder.testUserPreferencesDataStore()
|
|
||||||
)
|
|
||||||
synchronizer = TestSynchronizer(niaPreferencesDataSource)
|
|
||||||
|
|
||||||
subject = OfflineFirstAuthorsRepository(
|
|
||||||
authorDao = authorDao,
|
|
||||||
network = network,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun offlineFirstAuthorsRepository_Authors_stream_is_backed_by_Authors_dao() =
|
|
||||||
runTest {
|
|
||||||
assertEquals(
|
|
||||||
authorDao.getAuthorEntitiesStream()
|
|
||||||
.first()
|
|
||||||
.map(AuthorEntity::asExternalModel),
|
|
||||||
subject.getAuthorsStream()
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun offlineFirstAuthorsRepository_sync_pulls_from_network() =
|
|
||||||
runTest {
|
|
||||||
subject.syncWith(synchronizer)
|
|
||||||
|
|
||||||
val networkAuthors = network.getAuthors()
|
|
||||||
.map(NetworkAuthor::asEntity)
|
|
||||||
|
|
||||||
val dbAuthors = authorDao.getAuthorEntitiesStream()
|
|
||||||
.first()
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
networkAuthors.map(AuthorEntity::id),
|
|
||||||
dbAuthors.map(AuthorEntity::id)
|
|
||||||
)
|
|
||||||
|
|
||||||
// After sync version should be updated
|
|
||||||
assertEquals(
|
|
||||||
network.latestChangeListVersion(CollectionType.Authors),
|
|
||||||
synchronizer.getChangeListVersions().authorVersion
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun offlineFirstAuthorsRepository_incremental_sync_pulls_from_network() =
|
|
||||||
runTest {
|
|
||||||
// Set author version to 5
|
|
||||||
synchronizer.updateChangeListVersions {
|
|
||||||
copy(authorVersion = 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
subject.syncWith(synchronizer)
|
|
||||||
|
|
||||||
val changeList = network.changeListsAfter(
|
|
||||||
CollectionType.Authors,
|
|
||||||
version = 5
|
|
||||||
)
|
|
||||||
val changeListIds = changeList
|
|
||||||
.map(NetworkChangeList::id)
|
|
||||||
.toSet()
|
|
||||||
|
|
||||||
val network = network.getAuthors()
|
|
||||||
.map(NetworkAuthor::asEntity)
|
|
||||||
.filter { it.id in changeListIds }
|
|
||||||
|
|
||||||
val db = authorDao.getAuthorEntitiesStream()
|
|
||||||
.first()
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
network.map(AuthorEntity::id),
|
|
||||||
db.map(AuthorEntity::id)
|
|
||||||
)
|
|
||||||
|
|
||||||
// After sync version should be updated
|
|
||||||
assertEquals(
|
|
||||||
changeList.last().changeListVersion,
|
|
||||||
synchronizer.getChangeListVersions().authorVersion
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun offlineFirstAuthorsRepository_sync_deletes_items_marked_deleted_on_network() =
|
|
||||||
runTest {
|
|
||||||
val networkAuthors = network.getAuthors()
|
|
||||||
.map(NetworkAuthor::asEntity)
|
|
||||||
.map(AuthorEntity::asExternalModel)
|
|
||||||
|
|
||||||
// Delete half of the items on the network
|
|
||||||
val deletedItems = networkAuthors
|
|
||||||
.map(Author::id)
|
|
||||||
.partition { it.chars().sum() % 2 == 0 }
|
|
||||||
.first
|
|
||||||
.toSet()
|
|
||||||
|
|
||||||
deletedItems.forEach {
|
|
||||||
network.editCollection(
|
|
||||||
collectionType = CollectionType.Authors,
|
|
||||||
id = it,
|
|
||||||
isDelete = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
subject.syncWith(synchronizer)
|
|
||||||
|
|
||||||
val dbAuthors = authorDao.getAuthorEntitiesStream()
|
|
||||||
.first()
|
|
||||||
.map(AuthorEntity::asExternalModel)
|
|
||||||
|
|
||||||
// Assert that items marked deleted on the network have been deleted locally
|
|
||||||
assertEquals(
|
|
||||||
networkAuthors.map(Author::id) - deletedItems,
|
|
||||||
dbAuthors.map(Author::id)
|
|
||||||
)
|
|
||||||
|
|
||||||
// After sync version should be updated
|
|
||||||
assertEquals(
|
|
||||||
network.latestChangeListVersion(CollectionType.Authors),
|
|
||||||
synchronizer.getChangeListVersions().authorVersion
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* 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.data.testdoubles
|
|
||||||
|
|
||||||
import com.google.samples.apps.nowinandroid.core.database.dao.AuthorDao
|
|
||||||
import com.google.samples.apps.nowinandroid.core.database.model.AuthorEntity
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test double for [AuthorDao]
|
|
||||||
*/
|
|
||||||
class TestAuthorDao : AuthorDao {
|
|
||||||
|
|
||||||
private var entitiesStateFlow = MutableStateFlow(
|
|
||||||
listOf(
|
|
||||||
AuthorEntity(
|
|
||||||
id = "1",
|
|
||||||
name = "Topic",
|
|
||||||
imageUrl = "imageUrl",
|
|
||||||
twitter = "twitter",
|
|
||||||
mediumPage = "mediumPage",
|
|
||||||
bio = "bio",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun getAuthorEntitiesStream(): Flow<List<AuthorEntity>> =
|
|
||||||
entitiesStateFlow
|
|
||||||
|
|
||||||
override fun getAuthorEntityStream(authorId: String): Flow<AuthorEntity> {
|
|
||||||
throw NotImplementedError("Unused in tests")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun insertOrIgnoreAuthors(authorEntities: List<AuthorEntity>): List<Long> {
|
|
||||||
entitiesStateFlow.value = authorEntities
|
|
||||||
// Assume no conflicts on insert
|
|
||||||
return authorEntities.map { it.id.toLong() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updateAuthors(entities: List<AuthorEntity>) {
|
|
||||||
throw NotImplementedError("Unused in tests")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun upsertAuthors(entities: List<AuthorEntity>) {
|
|
||||||
entitiesStateFlow.value = entities
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun deleteAuthors(ids: List<String>) {
|
|
||||||
val idSet = ids.toSet()
|
|
||||||
entitiesStateFlow.update { entities ->
|
|
||||||
entities.filterNot { idSet.contains(it.id) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,192 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 12,
|
||||||
|
"identityHash": "f83b94b22ba0a0ce640922a3475e7c3e",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "news_resources",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `url` TEXT NOT NULL, `header_image_url` TEXT, `publish_date` INTEGER NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "headerImageUrl",
|
||||||
|
"columnName": "header_image_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "publishDate",
|
||||||
|
"columnName": "publish_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "news_resources_topics",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`news_resource_id` TEXT NOT NULL, `topic_id` TEXT NOT NULL, PRIMARY KEY(`news_resource_id`, `topic_id`), FOREIGN KEY(`news_resource_id`) REFERENCES `news_resources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`topic_id`) REFERENCES `topics`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "newsResourceId",
|
||||||
|
"columnName": "news_resource_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "topicId",
|
||||||
|
"columnName": "topic_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"news_resource_id",
|
||||||
|
"topic_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_news_resources_topics_news_resource_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"news_resource_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_news_resources_topics_news_resource_id` ON `${TABLE_NAME}` (`news_resource_id`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_news_resources_topics_topic_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"topic_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_news_resources_topics_topic_id` ON `${TABLE_NAME}` (`topic_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "news_resources",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"news_resource_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "topics",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"topic_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "topics",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `shortDescription` TEXT NOT NULL, `longDescription` TEXT NOT NULL DEFAULT '', `url` TEXT NOT NULL DEFAULT '', `imageUrl` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "shortDescription",
|
||||||
|
"columnName": "shortDescription",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "longDescription",
|
||||||
|
"columnName": "longDescription",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "''"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "imageUrl",
|
||||||
|
"columnName": "imageUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true,
|
||||||
|
"defaultValue": "''"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f83b94b22ba0a0ce640922a3475e7c3e')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue