commit
ddabba8e4a
@ -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,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,56 @@
|
|||||||
|
/*
|
||||||
|
* 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.dsl.ApplicationExtension
|
||||||
|
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<ApplicationExtension> {
|
||||||
|
buildTypes.configureEach {
|
||||||
|
// Disable the Crashlytics mapping file upload. This feature should only be
|
||||||
|
// enabled if a Firebase backend is available and configured in
|
||||||
|
// google-services.json.
|
||||||
|
configure<CrashlyticsExtension> {
|
||||||
|
mappingFileUploadEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.samples.apps.nowinandroid
|
||||||
|
|
||||||
|
import com.android.build.api.variant.LibraryAndroidComponentsExtension
|
||||||
|
import org.gradle.api.Project
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable unnecessary Android instrumented tests for the [project] if there is no `androidTest` folder.
|
||||||
|
* Otherwise, these projects would be compiled, packaged, installed and ran only to end-up with the following message:
|
||||||
|
*
|
||||||
|
* > Starting 0 tests on AVD
|
||||||
|
*
|
||||||
|
* Note: this could be improved by checking other potential sourceSets based on buildTypes and flavors.
|
||||||
|
*/
|
||||||
|
internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests(
|
||||||
|
project: Project,
|
||||||
|
) = beforeVariants {
|
||||||
|
it.enableAndroidTest = it.enableAndroidTest
|
||||||
|
&& project.projectDir.resolve("src/androidTest").exists()
|
||||||
|
}
|
Binary file not shown.
@ -1,5 +0,0 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
@ -0,0 +1 @@
|
|||||||
|
/build
|
@ -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,24 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of AnalyticsHelper which does nothing. Useful for tests and previews.
|
||||||
|
*/
|
||||||
|
class NoOpAnalyticsHelper : AnalyticsHelper {
|
||||||
|
override fun logEvent(event: AnalyticsEvent) = Unit
|
||||||
|
}
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* 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.di
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.CompositeUserNewsResourceRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.UserNewsResourceRepository
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface UserNewsResourceRepositoryModule {
|
||||||
|
@Binds
|
||||||
|
fun bindsUserNewsResourceRepository(
|
||||||
|
userDataRepository: CompositeUserNewsResourceRepository,
|
||||||
|
): UserNewsResourceRepository
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.samples.apps.nowinandroid.core.data.model
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.core.database.model.RecentSearchQueryEntity
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
|
data class RecentSearchQuery(
|
||||||
|
val query: String,
|
||||||
|
val queriedDate: Instant = Clock.System.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun RecentSearchQueryEntity.asExternalModel() = RecentSearchQuery(
|
||||||
|
query = query,
|
||||||
|
queriedDate = queriedDate,
|
||||||
|
)
|
@ -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),
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* 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.model.data.UserNewsResource
|
||||||
|
import com.google.samples.apps.nowinandroid.core.model.data.mapToUserNewsResources
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a [UserNewsResourceRepository] by combining a [NewsRepository] with a
|
||||||
|
* [UserDataRepository].
|
||||||
|
*/
|
||||||
|
class CompositeUserNewsResourceRepository @Inject constructor(
|
||||||
|
val newsRepository: NewsRepository,
|
||||||
|
val userDataRepository: UserDataRepository,
|
||||||
|
) : UserNewsResourceRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns available news resources (joined with user data) matching the given query.
|
||||||
|
*/
|
||||||
|
override fun observeAll(
|
||||||
|
query: NewsResourceQuery,
|
||||||
|
): Flow<List<UserNewsResource>> =
|
||||||
|
newsRepository.getNewsResources(query)
|
||||||
|
.combine(userDataRepository.userData) { newsResources, userData ->
|
||||||
|
newsResources.mapToUserNewsResources(userData)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns available news resources (joined with user data) for the followed topics.
|
||||||
|
*/
|
||||||
|
override fun observeAllForFollowedTopics(): Flow<List<UserNewsResource>> =
|
||||||
|
userDataRepository.userData.map { it.followedTopics }.distinctUntilChanged()
|
||||||
|
.flatMapLatest { followedTopics ->
|
||||||
|
when {
|
||||||
|
followedTopics.isEmpty() -> flowOf(emptyList())
|
||||||
|
else -> observeAll(NewsResourceQuery(filterTopicIds = followedTopics))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun observeAllBookmarked(): Flow<List<UserNewsResource>> =
|
||||||
|
userDataRepository.userData.map { it.bookmarkedNewsResources }.distinctUntilChanged()
|
||||||
|
.flatMapLatest { bookmarkedNewsResources ->
|
||||||
|
when {
|
||||||
|
bookmarkedNewsResources.isEmpty() -> flowOf(emptyList())
|
||||||
|
else -> observeAll(NewsResourceQuery(filterNewsIds = bookmarkedNewsResources))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* 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.data.model.RecentSearchQuery
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.model.asExternalModel
|
||||||
|
import com.google.samples.apps.nowinandroid.core.database.dao.RecentSearchQueryDao
|
||||||
|
import com.google.samples.apps.nowinandroid.core.database.model.RecentSearchQueryEntity
|
||||||
|
import com.google.samples.apps.nowinandroid.core.network.Dispatcher
|
||||||
|
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.datetime.Clock
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DefaultRecentSearchRepository @Inject constructor(
|
||||||
|
private val recentSearchQueryDao: RecentSearchQueryDao,
|
||||||
|
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
|
||||||
|
) : RecentSearchRepository {
|
||||||
|
override suspend fun insertOrReplaceRecentSearch(searchQuery: String) {
|
||||||
|
withContext(ioDispatcher) {
|
||||||
|
recentSearchQueryDao.insertOrReplaceRecentSearchQuery(
|
||||||
|
RecentSearchQueryEntity(
|
||||||
|
query = searchQuery,
|
||||||
|
queriedDate = Clock.System.now(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRecentSearchQueries(limit: Int): Flow<List<RecentSearchQuery>> =
|
||||||
|
recentSearchQueryDao.getRecentSearchQueryEntities(limit).map { searchQueries ->
|
||||||
|
searchQueries.map {
|
||||||
|
it.asExternalModel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun clearRecentSearches() = recentSearchQueryDao.clearRecentSearchQueries()
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.dao.NewsResourceDao
|
||||||
|
import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceFtsDao
|
||||||
|
import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao
|
||||||
|
import com.google.samples.apps.nowinandroid.core.database.dao.TopicFtsDao
|
||||||
|
import com.google.samples.apps.nowinandroid.core.database.model.asExternalModel
|
||||||
|
import com.google.samples.apps.nowinandroid.core.database.model.asFtsEntity
|
||||||
|
import com.google.samples.apps.nowinandroid.core.model.data.SearchResult
|
||||||
|
import com.google.samples.apps.nowinandroid.core.network.Dispatcher
|
||||||
|
import com.google.samples.apps.nowinandroid.core.network.NiaDispatchers.IO
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DefaultSearchContentsRepository @Inject constructor(
|
||||||
|
private val newsResourceDao: NewsResourceDao,
|
||||||
|
private val newsResourceFtsDao: NewsResourceFtsDao,
|
||||||
|
private val topicDao: TopicDao,
|
||||||
|
private val topicFtsDao: TopicFtsDao,
|
||||||
|
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
|
||||||
|
) : SearchContentsRepository {
|
||||||
|
|
||||||
|
override suspend fun populateFtsData() {
|
||||||
|
withContext(ioDispatcher) {
|
||||||
|
newsResourceFtsDao.insertAll(
|
||||||
|
newsResourceDao.getOneOffNewsResources().map { it.asFtsEntity() },
|
||||||
|
)
|
||||||
|
topicFtsDao.insertAll(topicDao.getOneOffTopicEntities().map { it.asFtsEntity() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchContents(searchQuery: String): Flow<SearchResult> {
|
||||||
|
// Surround the query by asterisks to match the query when it's in the middle of
|
||||||
|
// a word
|
||||||
|
val newsResourceIds = newsResourceFtsDao.searchAllNewsResources("*$searchQuery*")
|
||||||
|
val topicIds = topicFtsDao.searchAllTopics("*$searchQuery*")
|
||||||
|
|
||||||
|
val newsResourcesFlow = newsResourceIds
|
||||||
|
.mapLatest { it.toSet() }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.flatMapLatest {
|
||||||
|
newsResourceDao.getNewsResources(useFilterNewsIds = true, filterNewsIds = it)
|
||||||
|
}
|
||||||
|
val topicsFlow = topicIds
|
||||||
|
.mapLatest { it.toSet() }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.flatMapLatest(topicDao::getTopicEntities)
|
||||||
|
return combine(newsResourcesFlow, topicsFlow) { newsResources, topics ->
|
||||||
|
SearchResult(
|
||||||
|
topics = topics.map { it.asExternalModel() },
|
||||||
|
newsResources = newsResources.map { it.asExternalModel() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getSearchContentsCount(): Flow<Int> =
|
||||||
|
combine(
|
||||||
|
newsResourceFtsDao.getCount(),
|
||||||
|
topicFtsDao.getCount(),
|
||||||
|
) { newsResourceCount, topicsCount ->
|
||||||
|
newsResourceCount + topicsCount
|
||||||
|
}
|
||||||
|
}
|
@ -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.data.repository
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.model.RecentSearchQuery
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data layer interface for the recent searches.
|
||||||
|
*/
|
||||||
|
interface RecentSearchRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the recent search queries up to the number of queries specified as [limit].
|
||||||
|
*/
|
||||||
|
fun getRecentSearchQueries(limit: Int): Flow<List<RecentSearchQuery>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert or replace the [searchQuery] as part of the recent searches.
|
||||||
|
*/
|
||||||
|
suspend fun insertOrReplaceRecentSearch(searchQuery: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the recent searches.
|
||||||
|
*/
|
||||||
|
suspend fun clearRecentSearches()
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.samples.apps.nowinandroid.core.data.repository
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.core.model.data.SearchResult
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data layer interface for the search feature.
|
||||||
|
*/
|
||||||
|
interface SearchContentsRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populate the fts tables for the search contents.
|
||||||
|
*/
|
||||||
|
suspend fun populateFtsData()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query the contents matched with the [searchQuery] and returns it as a [Flow] of [SearchResult]
|
||||||
|
*/
|
||||||
|
fun searchContents(searchQuery: String): Flow<SearchResult>
|
||||||
|
|
||||||
|
fun getSearchContentsCount(): Flow<Int>
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* 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.model.data.UserNewsResource
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data layer implementation for [UserNewsResource]
|
||||||
|
*/
|
||||||
|
interface UserNewsResourceRepository {
|
||||||
|
/**
|
||||||
|
* Returns available news resources as a stream.
|
||||||
|
*/
|
||||||
|
fun observeAll(
|
||||||
|
query: NewsResourceQuery = NewsResourceQuery(
|
||||||
|
filterTopicIds = null,
|
||||||
|
filterNewsIds = null,
|
||||||
|
),
|
||||||
|
): Flow<List<UserNewsResource>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns available news resources for the user's followed topics as a stream.
|
||||||
|
*/
|
||||||
|
fun observeAllForFollowedTopics(): Flow<List<UserNewsResource>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the user's bookmarked news resources as a stream.
|
||||||
|
*/
|
||||||
|
fun observeAllBookmarked(): Flow<List<UserNewsResource>>
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.samples.apps.nowinandroid.core.data.repository.fake
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.model.RecentSearchQuery
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.RecentSearchRepository
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fake implementation of the [RecentSearchRepository]
|
||||||
|
*/
|
||||||
|
class FakeRecentSearchRepository @Inject constructor() : RecentSearchRepository {
|
||||||
|
override suspend fun insertOrReplaceRecentSearch(searchQuery: String) { /* no-op */ }
|
||||||
|
|
||||||
|
override fun getRecentSearchQueries(limit: Int): Flow<List<RecentSearchQuery>> =
|
||||||
|
flowOf(emptyList())
|
||||||
|
|
||||||
|
override suspend fun clearRecentSearches() { /* no-op */ }
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* 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.fake
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.model.data.SearchResult
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fake implementation of the [SearchContentsRepository]
|
||||||
|
*/
|
||||||
|
class FakeSearchContentsRepository @Inject constructor() : SearchContentsRepository {
|
||||||
|
|
||||||
|
override suspend fun populateFtsData() { /* no-op */ }
|
||||||
|
override fun searchContents(searchQuery: String): Flow<SearchResult> = flowOf()
|
||||||
|
override fun getSearchContentsCount(): Flow<Int> = flowOf(1)
|
||||||
|
}
|
@ -0,0 +1,282 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 13,
|
||||||
|
"identityHash": "b6b299e53da623b16360975581ebfcfe",
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ftsVersion": "FTS4",
|
||||||
|
"ftsOptions": {
|
||||||
|
"tokenizer": "simple",
|
||||||
|
"tokenizerArgs": [],
|
||||||
|
"contentTable": "",
|
||||||
|
"languageIdColumnName": "",
|
||||||
|
"matchInfo": "FTS4",
|
||||||
|
"notIndexedColumns": [],
|
||||||
|
"prefixSizes": [],
|
||||||
|
"preferredOrder": "ASC"
|
||||||
|
},
|
||||||
|
"contentSyncTriggers": [],
|
||||||
|
"tableName": "newsResourcesFts",
|
||||||
|
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`newsResourceId` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "newsResourceId",
|
||||||
|
"columnName": "newsResourceId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": []
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ftsVersion": "FTS4",
|
||||||
|
"ftsOptions": {
|
||||||
|
"tokenizer": "simple",
|
||||||
|
"tokenizerArgs": [],
|
||||||
|
"contentTable": "",
|
||||||
|
"languageIdColumnName": "",
|
||||||
|
"matchInfo": "FTS4",
|
||||||
|
"notIndexedColumns": [],
|
||||||
|
"prefixSizes": [],
|
||||||
|
"preferredOrder": "ASC"
|
||||||
|
},
|
||||||
|
"contentSyncTriggers": [],
|
||||||
|
"tableName": "topicsFts",
|
||||||
|
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`topicId` TEXT NOT NULL, `name` TEXT NOT NULL, `shortDescription` TEXT NOT NULL, `longDescription` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "topicId",
|
||||||
|
"columnName": "topicId",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": []
|
||||||
|
},
|
||||||
|
"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, 'b6b299e53da623b16360975581ebfcfe')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,308 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 14,
|
||||||
|
"identityHash": "51271b81bde7c7997d67fb23c8f31780",
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ftsVersion": "FTS4",
|
||||||
|
"ftsOptions": {
|
||||||
|
"tokenizer": "simple",
|
||||||
|
"tokenizerArgs": [],
|
||||||
|
"contentTable": "",
|
||||||
|
"languageIdColumnName": "",
|
||||||
|
"matchInfo": "FTS4",
|
||||||
|
"notIndexedColumns": [],
|
||||||
|
"prefixSizes": [],
|
||||||
|
"preferredOrder": "ASC"
|
||||||
|
},
|
||||||
|
"contentSyncTriggers": [],
|
||||||
|
"tableName": "newsResourcesFts",
|
||||||
|
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`newsResourceId` TEXT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "newsResourceId",
|
||||||
|
"columnName": "newsResourceId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": []
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ftsVersion": "FTS4",
|
||||||
|
"ftsOptions": {
|
||||||
|
"tokenizer": "simple",
|
||||||
|
"tokenizerArgs": [],
|
||||||
|
"contentTable": "",
|
||||||
|
"languageIdColumnName": "",
|
||||||
|
"matchInfo": "FTS4",
|
||||||
|
"notIndexedColumns": [],
|
||||||
|
"prefixSizes": [],
|
||||||
|
"preferredOrder": "ASC"
|
||||||
|
},
|
||||||
|
"contentSyncTriggers": [],
|
||||||
|
"tableName": "topicsFts",
|
||||||
|
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`topicId` TEXT NOT NULL, `name` TEXT NOT NULL, `shortDescription` TEXT NOT NULL, `longDescription` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "topicId",
|
||||||
|
"columnName": "topicId",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": []
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "recentSearchQueries",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query` TEXT NOT NULL, `queriedDate` INTEGER NOT NULL, PRIMARY KEY(`query`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "query",
|
||||||
|
"columnName": "query",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "queriedDate",
|
||||||
|
"columnName": "queriedDate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"query"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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, '51271b81bde7c7997d67fb23c8f31780')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceFtsEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DAO for [NewsResourceFtsEntity] access.
|
||||||
|
*/
|
||||||
|
@Dao
|
||||||
|
interface NewsResourceFtsDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertAll(newsResources: List<NewsResourceFtsEntity>)
|
||||||
|
|
||||||
|
@Query("SELECT newsResourceId FROM newsResourcesFts WHERE newsResourcesFts MATCH :query")
|
||||||
|
fun searchAllNewsResources(query: String): Flow<List<String>>
|
||||||
|
|
||||||
|
@Query("SELECT count(*) FROM newsResourcesFts")
|
||||||
|
fun getCount(): Flow<Int>
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.samples.apps.nowinandroid.core.database.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Upsert
|
||||||
|
import com.google.samples.apps.nowinandroid.core.database.model.RecentSearchQueryEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DAO for [RecentSearchQueryEntity] access
|
||||||
|
*/
|
||||||
|
@Dao
|
||||||
|
interface RecentSearchQueryDao {
|
||||||
|
@Query(value = "SELECT * FROM recentSearchQueries ORDER BY queriedDate DESC LIMIT :limit")
|
||||||
|
fun getRecentSearchQueryEntities(limit: Int): Flow<List<RecentSearchQueryEntity>>
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
suspend fun insertOrReplaceRecentSearchQuery(recentSearchQuery: RecentSearchQueryEntity)
|
||||||
|
|
||||||
|
@Query(value = "DELETE FROM recentSearchQueries")
|
||||||
|
suspend fun clearRecentSearchQueries()
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.google.samples.apps.nowinandroid.core.database.model.TopicFtsEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DAO for [TopicFtsEntity] access.
|
||||||
|
*/
|
||||||
|
@Dao
|
||||||
|
interface TopicFtsDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertAll(topics: List<TopicFtsEntity>)
|
||||||
|
|
||||||
|
@Query("SELECT topicId FROM topicsFts WHERE topicsFts MATCH :query")
|
||||||
|
fun searchAllTopics(query: String): Flow<List<String>>
|
||||||
|
|
||||||
|
@Query("SELECT count(*) FROM topicsFts")
|
||||||
|
fun getCount(): Flow<Int>
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Fts4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fts entity for the news resources. See https://developer.android.com/reference/androidx/room/Fts4.
|
||||||
|
*/
|
||||||
|
@Entity(tableName = "newsResourcesFts")
|
||||||
|
@Fts4
|
||||||
|
data class NewsResourceFtsEntity(
|
||||||
|
|
||||||
|
@ColumnInfo(name = "newsResourceId")
|
||||||
|
val newsResourceId: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "title")
|
||||||
|
val title: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "content")
|
||||||
|
val content: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun NewsResourceEntity.asFtsEntity() = NewsResourceFtsEntity(
|
||||||
|
newsResourceId = id,
|
||||||
|
title = title,
|
||||||
|
content = content,
|
||||||
|
)
|
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.samples.apps.nowinandroid.core.database.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines an database entity that stored recent search queries.
|
||||||
|
*/
|
||||||
|
@Entity(
|
||||||
|
tableName = "recentSearchQueries",
|
||||||
|
)
|
||||||
|
data class RecentSearchQueryEntity(
|
||||||
|
@PrimaryKey
|
||||||
|
val query: String,
|
||||||
|
@ColumnInfo
|
||||||
|
val queriedDate: Instant,
|
||||||
|
)
|
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Fts4
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fts entity for the topic. See https://developer.android.com/reference/androidx/room/Fts4.
|
||||||
|
*/
|
||||||
|
@Entity(tableName = "topicsFts")
|
||||||
|
@Fts4
|
||||||
|
data class TopicFtsEntity(
|
||||||
|
|
||||||
|
@ColumnInfo(name = "topicId")
|
||||||
|
val topicId: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "name")
|
||||||
|
val name: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "shortDescription")
|
||||||
|
val shortDescription: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "longDescription")
|
||||||
|
val longDescription: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun TopicEntity.asFtsEntity() = TopicFtsEntity(
|
||||||
|
topicId = id,
|
||||||
|
name = name,
|
||||||
|
shortDescription = shortDescription,
|
||||||
|
longDescription = longDescription,
|
||||||
|
)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue