Change-Id: I103c903c936137c41d7d11be0731438d9ee557d9recent_search
parent
83b640a7f0
commit
6f9d2fe3ba
@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* 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 kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DefaultSearchContentsRepository @Inject constructor(
|
||||||
|
private val newsResourceDao: NewsResourceDao,
|
||||||
|
private val newsResourceFtsDao: NewsResourceFtsDao,
|
||||||
|
private val topicDao: TopicDao,
|
||||||
|
private val topicFtsDao: TopicFtsDao,
|
||||||
|
) : SearchContentsRepository {
|
||||||
|
|
||||||
|
override fun populateFtsData() {
|
||||||
|
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}*")
|
||||||
|
|
||||||
|
return combine(
|
||||||
|
newsResourceDao.getNewsResources(filterNewsIds = newsResourceIds.toSet()),
|
||||||
|
topicDao.getTopicEntities(topicIds.toSet())
|
||||||
|
) { newsResources, topics ->
|
||||||
|
SearchResult(
|
||||||
|
topics = topics.map { it.asExternalModel() },
|
||||||
|
newsResources = newsResources.map { it.asExternalModel() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.repository
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
|
||||||
|
import com.google.samples.apps.nowinandroid.core.model.data.Topic
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface SearchContentsRepository {
|
||||||
|
fun populateFtsData()
|
||||||
|
fun searchContents(searchQuery: String): Flow<SearchResult>
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SearchResult(
|
||||||
|
val topics: List<Topic> = emptyList(),
|
||||||
|
val newsResources: List<NewsResource> = emptyList(),
|
||||||
|
)
|
@ -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.data.repository.fake
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.SearchResult
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fake implementation of the [SearchContentsRepository]
|
||||||
|
*/
|
||||||
|
class FakeSearchContentsRepository @Inject constructor(
|
||||||
|
) : SearchContentsRepository {
|
||||||
|
|
||||||
|
override fun populateFtsData() {}
|
||||||
|
override fun searchContents(searchQuery: String): Flow<SearchResult> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
@ -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,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.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceFtsEntity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DAO for [NewsResourceFtsEntity] access.
|
||||||
|
*/
|
||||||
|
@Dao
|
||||||
|
interface NewsResourceFtsDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insertAll(topics: List<NewsResourceFtsEntity>)
|
||||||
|
|
||||||
|
@Query("SELECT newsResourceId FROM newsResourcesFts WHERE newsResourcesFts MATCH :query")
|
||||||
|
fun searchAllNewsResources(query: String): List<String>
|
||||||
|
|
||||||
|
@Query("SELECT count(*) FROM newsResourcesFts")
|
||||||
|
fun getCount(): Int
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DAO for [TopicFtsEntity] access.
|
||||||
|
*/
|
||||||
|
@Dao
|
||||||
|
interface TopicFtsDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insertAll(topics: List<TopicFtsEntity>)
|
||||||
|
@Query("SELECT topicId FROM topicsFts WHERE topicsFts MATCH :query")
|
||||||
|
fun searchAllTopics(query: String): List<String>
|
||||||
|
|
||||||
|
@Query("SELECT count(*) FROM topicsFts")
|
||||||
|
fun getCount(): 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,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
|
||||||
|
)
|
@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* 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.domain
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.SearchResult
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.UserDataRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.domain.model.FollowableTopic
|
||||||
|
import com.google.samples.apps.nowinandroid.core.domain.model.UserNewsResource
|
||||||
|
import com.google.samples.apps.nowinandroid.core.model.data.UserData
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.filterNot
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class GetSearchContentsUseCase @Inject constructor(
|
||||||
|
private val searchContentsRepository: SearchContentsRepository,
|
||||||
|
private val userDataRepository: UserDataRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
operator fun invoke(
|
||||||
|
searchQuery: String,
|
||||||
|
): Flow<UserSearchResult> =
|
||||||
|
searchContentsRepository.searchContents(searchQuery)
|
||||||
|
.mapToUserSearchResult(userDataRepository.userData)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Flow<SearchResult>.mapToUserSearchResult(userDataStream: Flow<UserData>): Flow<UserSearchResult> =
|
||||||
|
combine(userDataStream) { searchResult, userData ->
|
||||||
|
UserSearchResult(
|
||||||
|
topics = searchResult.topics.map { topic ->
|
||||||
|
FollowableTopic(
|
||||||
|
topic = topic,
|
||||||
|
isFollowed = topic.id in userData.followedTopics,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
newsResources = searchResult.newsResources.map { news ->
|
||||||
|
UserNewsResource(
|
||||||
|
newsResource = news,
|
||||||
|
userData = userData,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UserSearchResult(
|
||||||
|
val topics: List<FollowableTopic> = emptyList(),
|
||||||
|
val newsResources: List<UserNewsResource> = emptyList(),
|
||||||
|
)
|
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.samples.apps.nowinandroid.core.testing.repository
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.data.repository.SearchResult
|
||||||
|
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
|
||||||
|
import com.google.samples.apps.nowinandroid.core.model.data.Topic
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
|
||||||
|
class TestSearchContentsRepository : SearchContentsRepository {
|
||||||
|
|
||||||
|
private val cachedTopics: MutableList<Topic> = mutableListOf()
|
||||||
|
private val cachedNewsResources: MutableList<NewsResource> = mutableListOf()
|
||||||
|
|
||||||
|
override fun populateFtsData() {}
|
||||||
|
|
||||||
|
override fun searchContents(searchQuery: String): Flow<SearchResult> {
|
||||||
|
return flowOf(
|
||||||
|
SearchResult(
|
||||||
|
topics = cachedTopics.filter {
|
||||||
|
it.name.contains(searchQuery) ||
|
||||||
|
it.shortDescription.contains(searchQuery) ||
|
||||||
|
it.longDescription.contains(searchQuery)
|
||||||
|
},
|
||||||
|
newsResources = cachedNewsResources.filter {
|
||||||
|
it.content.contains(searchQuery) ||
|
||||||
|
it.title.contains(searchQuery)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test only method to add the topics to the stored list in memory
|
||||||
|
*/
|
||||||
|
fun addTopics(topics: List<Topic>) {
|
||||||
|
cachedTopics.addAll(topics)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test only method to add the news resources to the stored list in memory
|
||||||
|
*/
|
||||||
|
fun addNewsResources(newsResources: List<NewsResource>) {
|
||||||
|
cachedNewsResources.addAll(newsResources)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* 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.feature.search
|
||||||
|
|
||||||
|
import com.google.samples.apps.nowinandroid.core.domain.GetSearchContentsUseCase
|
||||||
|
import com.google.samples.apps.nowinandroid.core.testing.data.newsResourcesTestData
|
||||||
|
import com.google.samples.apps.nowinandroid.core.testing.data.topicsTestData
|
||||||
|
import com.google.samples.apps.nowinandroid.core.testing.repository.TestSearchContentsRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.testing.repository.TestUserDataRepository
|
||||||
|
import com.google.samples.apps.nowinandroid.core.testing.util.MainDispatcherRule
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.search.SearchResultUiState.EmptyQuery
|
||||||
|
import com.google.samples.apps.nowinandroid.feature.search.SearchResultUiState.Loading
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To learn more about how this test handles Flows created with stateIn, see
|
||||||
|
* https://developer.android.com/kotlin/flow/test#statein
|
||||||
|
*/
|
||||||
|
class SearchViewModelTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val dispatcherRule = MainDispatcherRule()
|
||||||
|
|
||||||
|
private val userDataRepository = TestUserDataRepository()
|
||||||
|
private val searchContentsRepository = TestSearchContentsRepository()
|
||||||
|
private val getSearchContentsUseCase = GetSearchContentsUseCase(
|
||||||
|
searchContentsRepository = searchContentsRepository,
|
||||||
|
userDataRepository = userDataRepository,
|
||||||
|
)
|
||||||
|
private lateinit var viewModel: SearchViewModel
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
viewModel = SearchViewModel(
|
||||||
|
getSearchContentsUseCase = getSearchContentsUseCase,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun stateIsInitiallyLoading() = runTest {
|
||||||
|
assertEquals(Loading, viewModel.searchResultUiState.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun stateIsEmptyQuery_withEmptySearchQuery() = runTest {
|
||||||
|
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.searchResultUiState.collect() }
|
||||||
|
|
||||||
|
viewModel.onSearchQueryChanged("")
|
||||||
|
assertEquals(EmptyQuery, viewModel.searchResultUiState.value)
|
||||||
|
|
||||||
|
collectJob.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun emptyResultIsReturned_withNotMatchingQuery() = runTest {
|
||||||
|
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.searchQuery.collect() }
|
||||||
|
|
||||||
|
viewModel.onSearchQueryChanged("XXX")
|
||||||
|
searchContentsRepository.addNewsResources(newsResourcesTestData)
|
||||||
|
searchContentsRepository.addTopics(topicsTestData)
|
||||||
|
|
||||||
|
val result = viewModel.searchResultUiState.value
|
||||||
|
// TODO: Figure out to get the latest emitted ui State? The result is emitted as EmptyQuery
|
||||||
|
// assertIs<Success>(result)
|
||||||
|
|
||||||
|
collectJob.cancel()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue