/*
* Copyright (C) 2016 Simon Vig Therkildsen
*
* 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.
*/
package net.simonvt.cathode.search;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import net.simonvt.cathode.CathodeApp;
import net.simonvt.cathode.api.entity.Movie;
import net.simonvt.cathode.api.entity.SearchResult;
import net.simonvt.cathode.api.entity.Show;
import net.simonvt.cathode.api.enumeration.Enums;
import net.simonvt.cathode.api.enumeration.Extended;
import net.simonvt.cathode.api.enumeration.ItemType;
import net.simonvt.cathode.api.service.SearchService;
import net.simonvt.cathode.images.ImageUri;
import net.simonvt.cathode.provider.DatabaseContract.MovieColumns;
import net.simonvt.cathode.provider.DatabaseContract.ShowColumns;
import net.simonvt.cathode.provider.MovieDatabaseHelper;
import net.simonvt.cathode.provider.ProviderSchematic.Movies;
import net.simonvt.cathode.provider.ProviderSchematic.Shows;
import net.simonvt.cathode.provider.ShowDatabaseHelper;
import net.simonvt.cathode.scheduler.MovieTaskScheduler;
import net.simonvt.cathode.scheduler.ShowTaskScheduler;
import net.simonvt.cathode.images.ImageType;
import net.simonvt.cathode.util.MainHandler;
import net.simonvt.schematic.Cursors;
import retrofit2.Call;
import retrofit2.Response;
import timber.log.Timber;
public class SearchHandler {
public interface SearchListener {
void onSearchResult(List<Result> results, boolean localResults);
}
private static final int LIMIT = 50;
@Inject SearchService searchService;
@Inject ShowTaskScheduler showScheduler;
@Inject MovieTaskScheduler movieScheduler;
@Inject ShowDatabaseHelper showHelper;
@Inject MovieDatabaseHelper movieHelper;
private final List<WeakReference<SearchListener>> listeners = new ArrayList<>();
private List<Result> results;
private boolean localResults;
private Context context;
private SearchExecutor executor = new SearchExecutor();
public SearchHandler(Context context) {
this.context = context;
CathodeApp.inject(context, this);
}
public void addListener(SearchListener listener) {
WeakReference<SearchListener> listenerRef = new WeakReference<>(listener);
listeners.add(listenerRef);
if (results != null) {
listener.onSearchResult(results, localResults);
}
}
public void removeListener(SearchListener listener) {
for (int i = listeners.size() - 1; i >= 0; i--) {
WeakReference<SearchListener> listenerRef = listeners.get(i);
SearchListener searchListener = listenerRef.get();
if (searchListener == null || listener == searchListener) {
listeners.remove(listenerRef);
}
}
}
public void clear() {
executor.cancelRunning();
results = null;
localResults = false;
}
public void publishResult(final List<Result> results, final boolean localResults) {
Timber.d("Publishing %d results", results.size());
SearchHandler.this.results = results;
SearchHandler.this.localResults = localResults;
for (int i = listeners.size() - 1; i >= 0; i--) {
WeakReference<SearchListener> listenerRef = listeners.get(i);
SearchListener listener = listenerRef.get();
if (listener == null) {
listeners.remove(listenerRef);
} else {
listener.onSearchResult(results, localResults);
}
}
}
public void forceSearch(final String query) {
executor.cancelRunning();
search(query);
}
public void search(final String query) {
executor.execute(new SearchRunnable(query));
}
class SearchRunnable implements Runnable {
volatile boolean canceled;
final String query;
private List<Result> results;
public SearchRunnable(String query) {
this.query = query;
}
public void cancel() {
synchronized (this) {
canceled = true;
}
}
@Override public void run() {
Enums<ItemType> types = Enums.of(ItemType.SHOW, ItemType.MOVIE);
try {
Call<List<SearchResult>> call =
searchService.search(types, query, Extended.FULL, LIMIT);
Response<List<SearchResult>> response = call.execute();
if (response.isSuccessful()) {
int relevance = 0;
List<SearchResult> searchResults = response.body();
final List<Result> results = new ArrayList<>(searchResults.size());
for (SearchResult searchResult : searchResults) {
if (searchResult.getType() == ItemType.SHOW) {
Show show = searchResult.getShow();
if (!TextUtils.isEmpty(show.getTitle())) {
final long showId = showHelper.partialUpdate(show);
String title = show.getTitle();
String overview = show.getOverview();
float rating = show.getRating() == null ? 0.0f : show.getRating();
Result result =
new Result(ItemType.SHOW, showId, title, overview, rating, relevance++);
results.add(result);
}
} else if (searchResult.getType() == ItemType.MOVIE) {
Movie movie = searchResult.getMovie();
if (!TextUtils.isEmpty(movie.getTitle())) {
final long movieId = movieHelper.partialUpdate(movie);
String title = movie.getTitle();
String overview = movie.getOverview();
float rating = movie.getRating() == null ? 0.0f : movie.getRating();
Result result =
new Result(ItemType.MOVIE, movieId, title, overview, rating, relevance++);
results.add(result);
}
}
}
publish(results, false);
return;
}
} catch (IOException e) {
Timber.d(e, "Search failed");
} catch (Throwable t) {
Timber.e(t, "Search failed");
}
int relevance = 0;
List<Result> results = new ArrayList<>();
Cursor shows = context.getContentResolver().query(Shows.SHOWS, new String[] {
ShowColumns.ID, ShowColumns.TITLE, ShowColumns.OVERVIEW, ShowColumns.RATING,
}, ShowColumns.TITLE + " LIKE ?", new String[] {
"%" + query + "%",
}, null);
while (shows.moveToNext()) {
final long id = Cursors.getLong(shows, ShowColumns.ID);
final String title = Cursors.getString(shows, ShowColumns.TITLE);
final String overview = Cursors.getString(shows, ShowColumns.OVERVIEW);
final float rating = Cursors.getFloat(shows, ShowColumns.RATING);
final String poster =
ImageUri.create(ImageUri.ITEM_SHOW, ImageType.POSTER, id);
Result result = new Result(ItemType.SHOW, id, title, overview, rating, relevance++);
results.add(result);
}
shows.close();
Cursor movies = context.getContentResolver().query(Movies.MOVIES, new String[] {
MovieColumns.ID, MovieColumns.TITLE, MovieColumns.OVERVIEW, MovieColumns.RATING,
}, MovieColumns.TITLE + " LIKE ?", new String[] {
"%" + query + "%",
}, null);
while (movies.moveToNext()) {
final long id = Cursors.getLong(movies, MovieColumns.ID);
final String title = Cursors.getString(movies, MovieColumns.TITLE);
final String overview = Cursors.getString(movies, MovieColumns.OVERVIEW);
final float rating = Cursors.getFloat(movies, MovieColumns.RATING);
final String poster =
ImageUri.create(ImageUri.ITEM_MOVIE, ImageType.POSTER, id);
Result result =
new Result(ItemType.MOVIE, id, title, overview, rating, relevance++);
results.add(result);
}
movies.close();
publish(results, true);
}
private void publish(final List<Result> results, final boolean localResults) {
MainHandler.post(new Runnable() {
@Override public void run() {
synchronized (this) {
if (!canceled) {
publishResult(results, localResults);
}
}
}
});
}
}
}