Skip to content
Permalink
Browse files

Include speakers for search result

Bug: 130516731

Change-Id: I5fd902fceeb29f739ecb69f1a8d78cd67ca5810b
  • Loading branch information...
thagikura committed Apr 20, 2019
1 parent 9a78bea commit 365174615264c5ea99bbd72e29fe9230eb796807
Showing with 296 additions and 45 deletions.
  1. +2 −0 mobile/build.gradle
  2. +7 −4 mobile/src/main/java/com/google/samples/apps/iosched/ui/search/SearchBindingAdapters.kt
  3. +4 −1 mobile/src/main/java/com/google/samples/apps/iosched/ui/search/SearchFragment.kt
  4. +8 −2 mobile/src/main/java/com/google/samples/apps/iosched/ui/search/SearchResult.kt
  5. +49 −14 mobile/src/main/java/com/google/samples/apps/iosched/ui/search/SearchViewModel.kt
  6. +27 −0 mobile/src/main/res/drawable/ic_account_box.xml
  7. +3 −0 mobile/src/main/res/navigation/nav_graph.xml
  8. +5 −0 mobile/src/test/java/com/google/samples/apps/iosched/test/util/fakes/FakeAppDatabase.kt
  9. +12 −4 shared/src/main/java/com/google/samples/apps/iosched/shared/data/ConferenceDataRepository.kt
  10. +7 −1 shared/src/main/java/com/google/samples/apps/iosched/shared/data/db/AppDatabase.kt
  11. +34 −0 shared/src/main/java/com/google/samples/apps/iosched/shared/data/db/SpeakerFtsDao.kt
  12. +57 −0 shared/src/main/java/com/google/samples/apps/iosched/shared/data/db/SpeakerFtsEntity.kt
  13. +16 −6 shared/src/main/java/com/google/samples/apps/iosched/shared/domain/search/SearchDbUseCase.kt
  14. +7 −5 shared/src/main/java/com/google/samples/apps/iosched/shared/domain/search/SearchUseCase.kt
  15. +29 −0 shared/src/main/java/com/google/samples/apps/iosched/shared/domain/search/Searchable.kt
  16. +11 −5 shared/src/test/java/com/google/samples/apps/iosched/shared/domain/search/SearchUseCaseTest.kt
  17. +5 −0 shared/src/test/java/com/google/samples/apps/iosched/test/util/FakeAppDatabase.kt
  18. +7 −3 tv/src/main/java/com/google/samples/apps/iosched/tv/search/SessionContentProvider.kt
  19. +6 −0 tv/src/test/java/com/google/samples/apps/iosched/tv/util/FakeAppDatabase.kt
@@ -151,6 +151,8 @@ dependencies {
implementation "androidx.room:room-ktx:$rootProject.roomVersion"
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
testImplementation "androidx.room:room-ktx:$rootProject.roomVersion"
testImplementation "androidx.room:room-runtime:$rootProject.roomVersion"

// Dagger
implementation "com.google.dagger:dagger-android:$rootProject.dagger"
@@ -22,13 +22,16 @@ import androidx.core.view.isVisible
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.samples.apps.iosched.R
import com.google.samples.apps.iosched.ui.search.SearchResultType.CODELAB
import com.google.samples.apps.iosched.ui.search.SearchResultType.SESSION
import com.google.samples.apps.iosched.ui.search.SearchResultType.SPEAKER

@BindingAdapter("searchResultIcon")
fun searchResultIcon(imageView: ImageView, type: String) {
fun searchResultIcon(imageView: ImageView, type: SearchResultType) {
val iconId = when (type) {
"session" -> R.drawable.ic_event
"codelab" -> R.drawable.ic_agenda_codelab
else -> R.drawable.ic_event
SESSION -> R.drawable.ic_event
SPEAKER -> R.drawable.ic_account_box
CODELAB -> R.drawable.ic_agenda_codelab
}
imageView.setImageDrawable(AppCompatResources.getDrawable(imageView.context, iconId))
}
@@ -34,6 +34,7 @@ import com.google.samples.apps.iosched.shared.result.EventObserver
import com.google.samples.apps.iosched.shared.util.viewModelProvider
import com.google.samples.apps.iosched.ui.MainNavigationFragment
import com.google.samples.apps.iosched.ui.search.SearchFragmentDirections.Companion.toSessionDetail
import com.google.samples.apps.iosched.ui.search.SearchFragmentDirections.Companion.toSpeakerDetail
import com.google.samples.apps.iosched.util.doOnApplyWindowInsets
import kotlinx.android.synthetic.main.fragment_search.view.searchView
import javax.inject.Inject
@@ -65,7 +66,9 @@ class SearchFragment : MainNavigationFragment() {
viewModel.navigateToSessionAction.observe(this, EventObserver { sessionId ->
findNavController().navigate(toSessionDetail(sessionId))
})

viewModel.navigateToSpeakerAction.observe(this, EventObserver { speakerId ->
findNavController().navigate(toSpeakerDetail(speakerId))
})
analyticsHelper.sendScreenView("Search", requireActivity())
}

@@ -19,6 +19,12 @@ package com.google.samples.apps.iosched.ui.search
data class SearchResult(
val title: String,
val subtitle: String,
val type: String,
val type: SearchResultType,
val objectId: String
)
)

enum class SearchResultType {
SESSION,
SPEAKER,
CODELAB
}
@@ -19,17 +19,23 @@ package com.google.samples.apps.iosched.ui.search
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.google.samples.apps.iosched.model.Session
import com.google.samples.apps.iosched.model.SessionId
import com.google.samples.apps.iosched.model.SpeakerId
import com.google.samples.apps.iosched.shared.analytics.AnalyticsActions
import com.google.samples.apps.iosched.shared.analytics.AnalyticsHelper
import com.google.samples.apps.iosched.shared.di.SearchUsingRoomEnabledFlag
import com.google.samples.apps.iosched.shared.domain.search.SearchDbUseCase
import com.google.samples.apps.iosched.shared.domain.search.SearchUseCase
import com.google.samples.apps.iosched.shared.domain.search.Searchable
import com.google.samples.apps.iosched.shared.domain.search.Searchable.SearchedSession
import com.google.samples.apps.iosched.shared.domain.search.Searchable.SearchedSpeaker
import com.google.samples.apps.iosched.shared.result.Event
import com.google.samples.apps.iosched.shared.result.Result
import com.google.samples.apps.iosched.shared.result.successOr
import com.google.samples.apps.iosched.shared.util.map
import com.google.samples.apps.iosched.ui.search.SearchResultType.CODELAB
import com.google.samples.apps.iosched.ui.search.SearchResultType.SESSION
import com.google.samples.apps.iosched.ui.search.SearchResultType.SPEAKER
import timber.log.Timber
import javax.inject.Inject

@@ -45,23 +51,40 @@ class SearchViewModel @Inject constructor(
var searchUsingRoomFeatureEnabled: Boolean = false

private val _navigateToSessionAction = MutableLiveData<Event<SessionId>>()
val navigateToSessionAction: LiveData<Event<SessionId>>
get() = _navigateToSessionAction
val navigateToSessionAction: LiveData<Event<SessionId>> = _navigateToSessionAction

private val loadSearchResults = MutableLiveData<Result<List<Session>>>()
private val _navigateToSpeakerAction = MutableLiveData<Event<SpeakerId>>()
val navigateToSpeakerAction: LiveData<Event<SpeakerId>> = _navigateToSpeakerAction

private val loadSearchResults = MutableLiveData<Result<List<Searchable>>>()
val searchResults: LiveData<List<SearchResult>>

val isEmpty: LiveData<Boolean>

init {
searchResults = loadSearchResults.map {
val result = it as? Result.Success ?: return@map emptyList<SearchResult>()
result.data.map { session ->
SearchResult(session.title,
session.type.displayName,
// TODO: SessionType needs to be passed instead of hard-coded string
"session",
session.id)
result.data.map { searched ->
when (searched) {
is SearchedSession -> {
val session = searched.session
SearchResult(
session.title,
session.type.displayName,
SearchResultType.SESSION,
session.id
)
}
is SearchedSpeaker -> {
val speaker = searched.speaker
SearchResult(
speaker.name,
"Speaker",
SearchResultType.SPEAKER,
speaker.id
)
}
}
}
}

@@ -71,10 +94,22 @@ class SearchViewModel @Inject constructor(
}

override fun openSearchResult(searchResult: SearchResult) {
if (searchResult.type == "session") {
val sessionId = searchResult.objectId
analyticsHelper.logUiEvent("Session: $sessionId", AnalyticsActions.SEARCH_RESULT_CLICK)
_navigateToSessionAction.value = Event(sessionId)
when (searchResult.type) {
SESSION -> {
val sessionId = searchResult.objectId
analyticsHelper.logUiEvent("Session: $sessionId",
AnalyticsActions.SEARCH_RESULT_CLICK)
_navigateToSessionAction.value = Event(sessionId)
}
SPEAKER -> {
val speakerId = searchResult.objectId
analyticsHelper.logUiEvent("Speaker: $speakerId",
AnalyticsActions.SEARCH_RESULT_CLICK)
_navigateToSpeakerAction.value = Event(speakerId)
}
CODELAB -> {
// not implemented
}
}
}

@@ -0,0 +1,27 @@
<!--
~ Copyright 2019 Google LLC
~
~ 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.
-->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?colorControlNormal">
<path
android:fillColor="#000"
android:fillType="nonZero"
android:pathData="M19,3L5,3a2,2 0,0 0,-2 2v14a2,2 0,0 0,2 2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,5v10.79C16.52,14.37 13.23,14 12,14c-1.23,0 -4.52,0.37 -7,1.79L5,5h14zM5,19v-0.77C6.74,16.66 10.32,16 12,16c1.68,0 5.26,0.66 7,2.23L19,19L5,19zM12,13c1.94,0 3.5,-1.56 3.5,-3.5S13.94,6 12,6 8.5,7.56 8.5,9.5 10.06,13 12,13zM12,8c0.83,0 1.5,0.67 1.5,1.5S12.83,11 12,11s-1.5,-0.67 -1.5,-1.5S11.17,8 12,8z" />
</vector>
@@ -145,6 +145,9 @@
<action
android:id="@+id/to_session_detail"
app:destination="@id/navigation_session_detail" />
<action
android:id="@+id/to_speaker_detail"
app:destination="@id/navigation_speaker_detail" />
</fragment>

<fragment
@@ -21,13 +21,18 @@ import androidx.room.InvalidationTracker
import androidx.sqlite.db.SupportSQLiteOpenHelper
import com.google.samples.apps.iosched.shared.data.db.AppDatabase
import com.google.samples.apps.iosched.shared.data.db.SessionFtsDao
import com.google.samples.apps.iosched.shared.data.db.SpeakerFtsDao
import org.mockito.Mockito

class FakeAppDatabase : AppDatabase() {
override fun sessionFtsDao(): SessionFtsDao {
return Mockito.mock(SessionFtsDao::class.java)
}

override fun speakerFtsDao(): SpeakerFtsDao {
return Mockito.mock(SpeakerFtsDao::class.java)
}

override fun createOpenHelper(config: DatabaseConfiguration?): SupportSQLiteOpenHelper {
return Mockito.mock(SupportSQLiteOpenHelper::class.java)
}
@@ -22,6 +22,7 @@ import com.google.samples.apps.iosched.model.ConferenceData
import com.google.samples.apps.iosched.model.ConferenceDay
import com.google.samples.apps.iosched.shared.data.db.AppDatabase
import com.google.samples.apps.iosched.shared.data.db.SessionFtsEntity
import com.google.samples.apps.iosched.shared.data.db.SpeakerFtsEntity
import com.google.samples.apps.iosched.shared.util.TimeUtils
import java.io.IOException
import javax.inject.Inject
@@ -118,16 +119,23 @@ open class ConferenceDataRepository @Inject constructor(
}

open fun populateSearchData(conferenceData: ConferenceData) {
val sessionFtsEntities = mutableListOf<SessionFtsEntity>()
conferenceData.sessions.forEach { session ->
sessionFtsEntities.add(SessionFtsEntity(
val sessionFtsEntities = conferenceData.sessions.map { session ->
SessionFtsEntity(
sessionId = session.id,
title = session.title,
description = session.abstract,
speakers = session.speakers.joinToString { it.name }
))
)
}
appDatabase.sessionFtsDao().insertAll(sessionFtsEntities)
val speakers = conferenceData.speakers.map {
SpeakerFtsEntity(
speakerId = it.id,
name = it.name,
description = it.abstract
)
}
appDatabase.speakerFtsDao().insertAll(speakers)
}

open fun getConferenceDays(): List<ConferenceDay> = TimeUtils.ConferenceDays
@@ -24,9 +24,15 @@ import androidx.room.RoomDatabase
/**
* The [Room] database for this app.
*/
@Database(entities = [SessionFtsEntity::class], version = 1, exportSchema = false)
@Database(entities = [
SessionFtsEntity::class,
SpeakerFtsEntity::class
],
version = 2,
exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun sessionFtsDao(): SessionFtsDao
abstract fun speakerFtsDao(): SpeakerFtsDao

companion object {
private const val databaseName = "iosched-db"
@@ -0,0 +1,34 @@
/*
* Copyright 2019 Google LLC
*
* 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.iosched.shared.data.db

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query

/**
* The Data Access Object for the [SpeakerFtsEntity] class.
*/
@Dao
interface SpeakerFtsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(speakers: List<SpeakerFtsEntity>)

@Query("SELECT speakerId FROM speakersFts WHERE speakersFts MATCH :query")
fun searchAllSpeakers(query: String): List<String>
}
@@ -0,0 +1,57 @@
/*
* Copyright 2019 Google LLC
*
* 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.iosched.shared.data.db

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Fts4

/**
* This class represents Speaker data for searching with FTS.
*
* The [ColumnInfo] name is explicitly declared to allow flexibility for renaming the data class
* properties without requiring changing the column name.
*/
@Entity(tableName = "speakersFts")
@Fts4
data class SpeakerFtsEntity(

/**
* An FTS entity table always has a column named rowid that is the equivalent of an
* INTEGER PRIMARY KEY index. Therefore, an FTS entity can only have a single field
* annotated with PrimaryKey, it must be named rowid and must be of INTEGER affinity.
*
* The field can be optionally omitted in the class (as is done here),
* but can still be used in queries.
*/

@ColumnInfo(name = "speakerId")
val speakerId: String,

@ColumnInfo(name = "name")
val name: String,

/**
* Body of text with the speaker's description.
*
* Note that this is named "abstract" in Speaker. This causes an issue with kapt, which
* in turn causes issues with Room. For more details, see
* https://youtrack.jetbrains.com/issue/KT-27804.
*/
@ColumnInfo(name = "description")
val description: String
)

0 comments on commit 3651746

Please sign in to comment.
You can’t perform that action at this time.