Photo Search
The Photo SDK provides a search engine that is described in this article.
#
Search For Photos And Albums By SuggestionsTo search for photos and/or albums, the steps are the following:
- Get text input from the user;
- Supply entered text to the Photos SDK;
- In return, Photos SDK provides list of Suggestions;
- Pass one or all of the suggestions to the Photo SDK;
- Receive list of photos and/or albums which are relevant to given suggestion(s).
Here is the explanation of these steps in the code.
#
Chaining User Input To SuggestionsThe following snippet shows how to create fragment and convert user input to suggestions.
class SuggestionsFragment : Fragment() {
private val disposable = CompositeDisposable() private val search: Search = PhotoManager.search
private val adapter = SuggestionsAdapter() // see source code at the end of the chapter private var currentSuggestions: SuggestionContainer? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState)
// Initialize recycler view view.recyclerView.layoutManager = LinearLayoutManager(context) view.recyclerView.adapter = adapter
// Prepare observable that emits new string every time user changes the text val queryObservable = RxTextView.textChanges(view.queryView) .map { it.toString() } .startWith("")
// Chain text observable to suggestions observable disposable += search.chainSuggestionsObservable( queryObservable, SuggestionQueryConfig("en") ) .observeOn(AndroidSchedulers.mainThread()) .subscribe { suggestionContainer -> // We receive new suggestion container every time user changes their input currentSuggestions = suggestionContainer adapter.data = suggestionContainer.toList() }
// Handle tap on SuggestionItem in recycler view adapter.clickListener = object : SearchSugAdapter.ClickListener { override fun onItemClicked(sugItem: SuggestionItem, position: Int) { onSuggestionClicked(sugItem) // defined later } }
// Handle 'enter' or 'search' key of virtual keyboard disposable += RxTextView.editorActions(view.queryView) { actionId -> actionId == EditorInfo.IME_ACTION_SEARCH }.subscribe { // User pressed enter (or 'Search' button) currentSuggestions?.let { onSearchClicked(it) } // defined later } }
override fun onDestroyView() { // Dispose all subscriptions disposable.clear() super.onDestroyView() }}
By chaining user input to suggestions using search.chainSuggestionsObservable(observable, config)
, app makes Photos SDK to send query to backend every time when user's input is changed. In response backend sends list of suggestions which match given string, then Photos SDK converts them to SuggestionContainer
and returns it to the app.
#
Querying By SuggestionsThe following code snippet shows how to make search query using suggestions:
private fun onSuggestionClicked(suggestionItem: SuggestionItem) { disposable += search.searchBySuggestion(suggestionItem) .subscribeBy( onComplete = { // Search result is read from backend and ready for display }, onError = { // Handle it } )}
private fun onSearchClicked(container: SuggestionContainer) { disposable += search.searchBySuggestions(container) .subscribeBy( onComplete = { // Search results are read from backend and ready for display }, onError = { // Handle it } )}
Note these functions return Completable
s which do not emit any photos nor albums. Completion indicates that fetching of search results is done, nothing more.
Next chapter explains how to get and display those results.
#
Displaying The Search ResultSuggestion item can be one of the following types:
AddressSuggestionItem
- based on geo-location of mediaCalendarSuggestionItem
- based on date/time when photo or video was takenLabelSuggestionItem
- based on media item description (this is not album description!)AlbumSuggestionItem
- based on album description
Note that only AlbumSuggestionItem
is related to albums, while other 3 types are related solely to media items (photos and videos).
When your app makes a search by calling of searchBySuggestion(suggestionItem)
or searchBySuggestions(container)
, depending on which type of suggestion(s) was given, Photos SDK provides list of either media items (photos/videos) or albums or both of them (when SuggestionContainer
holds suggestions of both media- and album-related types).
Getting the search results is quite similar to getting list of media items from Timeline and getting album list from Albums components of the Photos SDK.
The following snippet shows how to get search result of media items:
private class SearchPhotosViewModel( private val search: Search) : ViewModel() {
val contentObservable: Observable<PagingData<PhotoItem>> by lazy { search.createPagingPhotosObservable() .cachedIn(viewModelScope) }}
private class SearchPhotosViewModelFactory( private val search: Search) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T { return SearchPhotosViewModel(search) as T }}
class SearchResultPhotoFrg : Fragment() {
private var disposable: Disposable? = null private val adapter: TimelineAdapter // derived from PagingDataAdapter private val search: Search = PhotoManager.search private val viewModel: SearchPhotosViewModel by viewModels { SearchPhotosViewModelFactory(search) }
override fun onStart() { super.onStart() // You have to subscribe to the returned Observable to receive updates. disposable = viewModel.contentObservable .subscribe { pagingData -> // Update adapter adapter.submitData(this.lifecycle, pagingData) } }
override fun onStop() { super.onStop() // You must call dispose() on disposable that returned by the subscribe() method, // when it is no longer needed, for example in your fragment’s onStop() or onPause(). disposable?.dispose() }}
Note that Observable
returned by the method search.createPagingPhotosObservable()
emits non-empty PagingData
when fetching of search results from backend is completed. Before that moment Observable
might emit empty content.
Search
component of Photos SDK provides a number of methods for getting search results.
The following methods can be used to get albums after querying by album-related suggestion(s):
createPagingAlbumsObservable(): Observable<PagingData<AlbumItem>>
- returnsObservable
createPagingAlbumsFlow(): Flow<PagingData<AlbumItem>>
- returnsFlow
The following methods can be used to get media items (photos and videos) after querying by media items-related suggestions(s):
createPagingPhotosObservable(): Observable<PagingData<PhotoItem>>
- returnsObservable
createPagingPhotosFlow(): Flow<PagingData<PhotoItem>>
- returnsFlow
Observable
s and Flow
s returned by those methods should be cached in view model as shown in example above.
#
Adapter For Suggestion List FragmentTo demonstrate, here is SuggestionsAdapter
:
class SuggestionsAdapter : RecyclerView.Adapter<SearchSugAdapter.SuggestionVH>() {
var data: List<SuggestionItem> = emptyList() set(d) { field = d notifyDataSetChanged() }
interface ClickListener { fun onItemClicked(sugItem: SuggestionItem, position: Int) }
// Must be initialized in fragment lateinit var clickListener: ClickListener
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = SuggestionVH(LayoutInflater.from(parent.context).inflate(R.layout.item_suggestion_item, parent, false))
override fun onBindViewHolder(holder: SuggestionVH, position: Int) { holder.bind(data[position], position, clickListener) }
override fun getItemCount() = data.size
override fun getItemId(position: Int): Long = data[position].id.toLong()
class SuggestionVH(private val view: View) : RecyclerView.ViewHolder(view) {
fun bind(sug: SuggestionItem, position: Int, clickListener: ClickListener) { // Item view has only 3 fields: name, date and type view.nameView.text = sug.name view.dateView.text = "updated: " + SimpleDateFormat.getDateTimeInstance().format(Date(sug.updatedAt)) view.typeView.text = when (sug) { is AddressSuggestionItem -> "address" is AlbumSuggestionItem -> "album" is CalendarSuggestionItem -> "calendar" is LabelSuggestionItem -> "label" else -> throw IllegalArgumentException("Unknown suggestion type $sug") } view.setOnClickListener { clickListener.onItemClicked(sug, position) } } }}
#
Search By FacesTo search for a photo by faces, the first step is to get a list of faces from the photo. Then, get a list of photos for the selected person.
#
Get Faces From PhotoTo extract faces from photo item, use the following code example.
var disposable: Disposable? = null
// PhotoItems to download to your local device.var photoItem: PhotoItem? = null
// ...
// Returns Single, which completes when the operation ends.// You have to subscribe to returned Single in order to receive updates.disposable = PhotoManager.timeline.getFacesFromPhoto(photoItem) .observeOn(AndroidSchedulers.mainThread()) .subscribeBy( onSuccess = { // it: List<FaceItem>! // Handle it }, onError = { // it: Throwable // Handle it } )
// ...
// You must call dispose() on disposable that returned by subscribe() method,// when it is no longer needed, for example in your fragment’s onStop() or onPause().disposable?.dispose()
#
Find Photos By FaceTo find a photo by face, use the following code example.
var disposable: Disposable? = null
// FaceItem to getting photo list.var faceItem: FaceItem? = null
// ...
// Returns Single, which completes when the operation ends.// You have to subscribe to returned Single in order to receive updates.disposable = timeline.search.findPhotosByFace(faceItem) .observeOn(AndroidSchedulers.mainThread()) .subscribeBy( onSuccess = { // it: List<FaceItem>! // Handle it }, onError = { // it: Throwable // Handle it } )
// ...
// You must call dispose() on disposable that returned by subscribe() method,// when it is no longer needed, for example in your fragment’s onStop() or onPause().disposable?.dispose()
Links: FaceItem.